diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro
index d2a4e00..0a72651 100644
--- a/harbour-fernschreiber.pro
+++ b/harbour-fernschreiber.pro
@@ -22,6 +22,7 @@ DEFINES += QT_STATICPLUGIN
SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \
+ src/boolfiltermodel.cpp \
src/chatpermissionfiltermodel.cpp \
src/chatlistmodel.cpp \
src/chatmodel.cpp \
@@ -105,14 +106,21 @@ DISTFILES += qml/harbour-fernschreiber.qml \
qml/components/messageContent/MessageGame.qml \
qml/components/messageContent/MessageLocation.qml \
qml/components/messageContent/MessagePhoto.qml \
+ qml/components/messageContent/MessagePhotoAlbum.qml \
qml/components/messageContent/MessagePoll.qml \
qml/components/messageContent/MessageSticker.qml \
qml/components/messageContent/MessageVenue.qml \
+ qml/components/messageContent/MessageVideoAlbum.qml \
qml/components/messageContent/MessageVideoNote.qml \
qml/components/messageContent/MessageVideo.qml \
qml/components/messageContent/MessageVoiceNote.qml \
qml/components/messageContent/SponsoredMessage.qml \
qml/components/messageContent/WebPagePreview.qml \
+ qml/components/messageContent/mediaAlbumPage/FullscreenOverlay.qml \
+ qml/components/messageContent/mediaAlbumPage/PhotoComponent.qml \
+ qml/components/messageContent/mediaAlbumPage/VideoComponent.qml \
+ qml/components/messageContent/mediaAlbumPage/ZoomArea.qml \
+ qml/components/messageContent/mediaAlbumPage/ZoomImage.qml \
qml/components/settingsPage/Accordion.qml \
qml/components/settingsPage/AccordionItem.qml \
qml/components/settingsPage/ResponsiveGrid.qml \
@@ -130,6 +138,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \
qml/pages/CoverPage.qml \
qml/pages/DebugPage.qml \
qml/pages/InitializationPage.qml \
+ qml/pages/MediaAlbumPage.qml \
qml/pages/NewChatPage.qml \
qml/pages/OverviewPage.qml \
qml/pages/AboutPage.qml \
@@ -211,6 +220,7 @@ INSTALLS += telegram 86.png 108.png 128.png 172.png 256.png \
HEADERS += \
src/appsettings.h \
+ src/boolfiltermodel.h \
src/chatpermissionfiltermodel.h \
src/chatlistmodel.h \
src/chatmodel.h \
diff --git a/qml/components/MessageListViewItem.qml b/qml/components/MessageListViewItem.qml
index 50813ef..b066bcb 100644
--- a/qml/components/MessageListViewItem.qml
+++ b/qml/components/MessageListViewItem.qml
@@ -32,6 +32,7 @@ ListItem {
property int messageIndex
property int messageViewCount
property var myMessage
+ property var messageAlbumMessageIds
property var reactions
property bool canReplyToMessage
readonly property bool isAnonymous: myMessage.sender_id["@type"] === "messageSenderChat"
@@ -68,7 +69,7 @@ ListItem {
property var chatReactions
property var messageReactions
- highlighted: (down || isSelected || additionalOptionsOpened || wasNavigatedTo) && !menuOpen
+ highlighted: (down || (isSelected && messageAlbumMessageIds.length === 0) || additionalOptionsOpened || wasNavigatedTo) && !menuOpen
openMenuOnPressAndHold: !messageListItem.precalculatedValues.pageIsSelecting
signal replyToMessage()
@@ -159,15 +160,7 @@ ListItem {
}
onDoubleClicked: {
- if (messageListItem.chatReactions) {
- Debug.log("Using chat reactions")
- messageListItem.messageReactions = chatReactions
- showItemCompletelyTimer.requestedIndex = index;
- showItemCompletelyTimer.start();
- } else {
- Debug.log("Obtaining message reactions")
- tdLibWrapper.getMessageAvailableReactions(messageListItem.chatId, messageListItem.messageId);
- }
+ openReactions();
}
onPressAndHold: {
@@ -276,20 +269,20 @@ ListItem {
Connections {
target: chatModel
onMessagesReceived: {
- messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
+ messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
}
onMessagesIncrementalUpdate: {
- messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
+ messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
}
onNewMessageReceived: {
- messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
+ messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
}
onUnreadCountUpdated: {
- messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
+ messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
}
onLastReadSentMessageUpdated: {
- Debug.log("[ChatModel] Messages in this chat were read, new last read: ", lastReadSentIndex, ", updating description for index ", index, ", status: ", (index <= lastReadSentIndex));
- messageDateText.text = getMessageStatusText(myMessage, index, lastReadSentIndex, messageDateText.useElapsed);
+ Debug.log("[ChatModel] Messages in this chat were read, new last read: ", lastReadSentIndex, ", updating description for index ", index, ", status: ", (messageIndex <= lastReadSentIndex));
+ messageDateText.text = getMessageStatusText(myMessage, messageIndex, lastReadSentIndex, messageDateText.useElapsed);
}
}
@@ -310,7 +303,7 @@ ListItem {
pageStack.currentPage === chatPage) {
Debug.log("Available reactions for this message: " + reactions);
messageListItem.messageReactions = reactions;
- showItemCompletelyTimer.requestedIndex = index;
+ showItemCompletelyTimer.requestedIndex = messageIndex;
showItemCompletelyTimer.start();
} else {
messageListItem.messageReactions = null;
@@ -331,6 +324,13 @@ ListItem {
interval: 200
triggeredOnStart: false
onTriggered: {
+ if (requestedIndex === messageIndex) {
+ chatView.highlightMoveDuration = -1;
+ chatView.highlightResizeDuration = -1;
+ chatView.scrollToIndex(requestedIndex);
+ chatView.highlightMoveDuration = 0;
+ chatView.highlightResizeDuration = 0;
+ }
Debug.log("Show item completely timer triggered, requested index: " + requestedIndex + ", current index: " + index)
if (requestedIndex === index) {
var p = chatView.contentItem.mapFromItem(reactionsColumn, 0, 0)
@@ -384,8 +384,10 @@ ListItem {
onTriggered: {
if (messageListItem.hasContentComponent) {
var type = myMessage.content["@type"];
+ var albumComponentPart = (myMessage.media_album_id !== "0" && ['messagePhoto', 'messageVideo'].indexOf(type) !== -1) ? 'Album' : '';
+ console.log('delegateComponentLoadingTimer', myMessage.media_album_id, albumComponentPart)
extraContentLoader.setSource(
- "../components/messageContent/" + type.charAt(0).toUpperCase() + type.substring(1) + ".qml",
+ "../components/messageContent/" + type.charAt(0).toUpperCase() + type.substring(1) + albumComponentPart + ".qml",
{
messageListItem: messageListItem
})
@@ -449,8 +451,10 @@ ListItem {
}
height: messageTextColumn.height + precalculatedValues.paddingMediumDouble
width: precalculatedValues.backgroundWidth
- property bool isUnread: index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"
+
+ property bool isUnread: messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"
color: Theme.colorScheme === Theme.LightOnDark ? (isOwnMessage ? Theme.highlightBackgroundColor : (isUnread ? Theme.secondaryHighlightColor : Theme.secondaryColor)) : (isOwnMessage ? Theme.highlightBackgroundColor : (isUnread ? Theme.backgroundGlowColor : Theme.overlayBackgroundColor))
+
radius: parent.width / 50
opacity: isUnread ? 0.5 : 0.2
visible: appSettings.showStickersAsImages || (myMessage.content['@type'] !== "messageSticker" && myMessage.content['@type'] !== "messageAnimatedEmoji")
@@ -471,7 +475,13 @@ ListItem {
id: userText
width: parent.width
- text: messageListItem.isOwnMessage ? qsTr("You") : Emoji.emojify( myMessage['@type'] === "sponsoredMessage" ? tdLibWrapper.getChat(myMessage.sponsor_chat_id).title : ( messageListItem.isAnonymous ? page.chatInformation.title : Functions.getUserName(messageListItem.userInformation) ), font.pixelSize)
+ text: messageListItem.isOwnMessage
+ ? qsTr("You")
+ : Emoji.emojify( myMessage['@type'] === "sponsoredMessage"
+ ? tdLibWrapper.getChat(myMessage.sponsor_chat_id).title
+ : ( messageListItem.isAnonymous
+ ? page.chatInformation.title
+ : Functions.getUserName(messageListItem.userInformation) ), font.pixelSize)
font.pixelSize: Theme.fontSizeExtraSmall
font.weight: Font.ExtraBold
color: messageListItem.textColor
@@ -654,7 +664,8 @@ ListItem {
id: extraContentLoader
width: parent.width * getContentWidthMultiplier()
asynchronous: true
- height: item ? item.height : (messageListItem.hasContentComponent ? chatView.getContentComponentHeight(model.content_type, myMessage.content, width) : 0)
+ readonly property var defaultExtraContentHeight: messageListItem.hasContentComponent ? chatView.getContentComponentHeight(model.content_type, myMessage.content, width, model.album_message_ids.length) : 0
+ height: item ? item.height : defaultExtraContentHeight
}
Binding {
@@ -679,7 +690,7 @@ ListItem {
running: true
repeat: true
onTriggered: {
- messageDateText.text = getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed);
+ messageDateText.text = getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed);
}
}
@@ -692,13 +703,13 @@ ListItem {
font.pixelSize: Theme.fontSizeTiny
color: messageListItem.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor
horizontalAlignment: messageListItem.textAlign
- text: getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed)
+ text: getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed)
MouseArea {
anchors.fill: parent
enabled: !messageListItem.precalculatedValues.pageIsSelecting
onClicked: {
messageDateText.useElapsed = !messageDateText.useElapsed;
- messageDateText.text = getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed);
+ messageDateText.text = getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed);
}
}
}
@@ -719,12 +730,50 @@ ListItem {
textFormat: Text.StyledText
maximumLineCount: 1
elide: Text.ElideRight
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (messageListItem.messageReactions) {
+ messageListItem.messageReactions = null;
+ selectReactionBubble.visible = false;
+ } else {
+ openReactions();
+ }
+ }
+ }
}
}
}
}
+ Rectangle {
+ id: selectReactionBubble
+ visible: false
+ opacity: visible ? 0.5 : 0.0
+ Behavior on opacity { NumberAnimation {} }
+ anchors {
+ horizontalCenter: messageListItem.isOwnMessage ? messageBackground.left : messageBackground.right
+ verticalCenter: messageBackground.verticalCenter
+ }
+ height: Theme.itemSizeExtraSmall
+ width: Theme.itemSizeExtraSmall
+ color: Theme.primaryColor
+ radius: parent.width / 2
+ }
+
+ IconButton {
+ id: selectReactionButton
+ visible: selectReactionBubble.visible
+ opacity: visible ? 1.0 : 0.0
+ Behavior on opacity { NumberAnimation {} }
+ icon.source: "image://theme/icon-s-favorite"
+ anchors.centerIn: selectReactionBubble
+ onClicked: {
+ openReactions();
+ }
+ }
+
}
}
@@ -733,7 +782,7 @@ ListItem {
id: reactionsColumn
width: parent.width - ( 2 * Theme.horizontalPageMargin )
anchors.top: messageTextRow.bottom
- anchors.topMargin: Theme.paddingSmall
+ anchors.topMargin: Theme.paddingMedium
anchors.horizontalCenter: parent.horizontalCenter
visible: messageListItem.messageReactions ? ( messageListItem.messageReactions.length > 0 ? true : false ) : false
opacity: messageListItem.messageReactions ? ( messageListItem.messageReactions.length > 0 ? 1 : 0 ) : 0
@@ -742,7 +791,7 @@ ListItem {
Flickable {
width: parent.width
- height: reactionsResultRow.height + Theme.paddingSmall
+ height: reactionsResultRow.height + 2 * Theme.paddingMedium
anchors.horizontalCenter: parent.horizontalCenter
contentWidth: reactionsResultRow.width
clip: true
@@ -758,13 +807,13 @@ ListItem {
Row {
id: singleReactionRow
- spacing: Theme.paddingSmall
+ spacing: Theme.paddingMedium
Image {
id: emojiPicture
source: Emoji.getEmojiPath(modelData)
- width: status === Image.Ready ? Theme.fontSizeLarge : 0
- height: Theme.fontSizeLarge
+ width: status === Image.Ready ? Theme.fontSizeExtraLarge : 0
+ height: Theme.fontSizeExtraLarge
}
}
@@ -772,12 +821,26 @@ ListItem {
MouseArea {
anchors.fill: parent
onClicked: {
- tdLibWrapper.setMessageReaction(messageListItem.chatId, messageListItem.messageId, modelData);
- messageListItem.messageReactions = null;
+ for (var i = 0; i < reactions.length; i++) {
+ var reaction = reactions[i]
+ var reactionText = reaction.reaction ? reaction.reaction : (reaction.type && reaction.type.emoji) ? reaction.type.emoji : ""
+ if (reactionText === modelData) {
+ if (reaction.is_chosen) {
+ // Reaction is already selected
+ tdLibWrapper.removeMessageReaction(chatId, messageId, reactionText)
+ messageReactions = null
+ return
+ }
+ break
+ }
+ }
+ // Reaction is not yet selected
+ tdLibWrapper.addMessageReaction(chatId, messageId, modelData)
+ messageReactions = null
+ selectReactionBubble.visible = false
}
}
}
-
}
}
}
diff --git a/qml/components/TDLibMinithumbnail.qml b/qml/components/TDLibMinithumbnail.qml
index c2b8220..22b2aad 100644
--- a/qml/components/TDLibMinithumbnail.qml
+++ b/qml/components/TDLibMinithumbnail.qml
@@ -24,6 +24,7 @@ Loader {
id: loader
property var minithumbnail
property bool highlighted
+ property int fillMode: tdLibImage.fillMode
anchors.fill: parent
active: !!minithumbnail
sourceComponent: Component {
@@ -32,7 +33,7 @@ Loader {
id: minithumbnailImage
anchors.fill: parent
source: "data:image/jpg;base64,"+minithumbnail.data
- fillMode: tdLibImage.fillMode
+ fillMode: loader.fillMode
opacity: status === Image.Ready ? 1.0 : 0.0
cache: false
visible: opacity > 0
@@ -43,12 +44,12 @@ Loader {
effect: PressEffect { source: minithumbnailImage }
}
}
-
- FastBlur {
- anchors.fill: parent
- source: minithumbnailImage
- radius: Theme.paddingLarge
- }
+ // this had a visible impact on performance
+// FastBlur {
+// anchors.fill: parent
+// source: minithumbnailImage
+// radius: Theme.paddingLarge
+// }
}
}
}
diff --git a/qml/components/TDLibThumbnail.qml b/qml/components/TDLibThumbnail.qml
index 291f507..b1aa0dc 100644
--- a/qml/components/TDLibThumbnail.qml
+++ b/qml/components/TDLibThumbnail.qml
@@ -59,7 +59,7 @@ Item {
readonly property bool hasVisibleThumbnail: thumbnailImage.opacity !== 1.0
&& !(videoThumbnailLoader.item && videoThumbnailLoader.item.opacity === 1.0)
-
+ property alias fillMode: thumbnailImage.fillMode
layer {
enabled: highlighted
effect: PressEffect { source: tdlibThumbnail }
@@ -67,6 +67,7 @@ Item {
TDLibMinithumbnail {
id: minithumbnailLoader
+ fillMode: thumbnailImage.fillMode
active: !!minithumbnail && thumbnailImage.opacity < 1.0
}
BackgroundImage {
@@ -103,6 +104,7 @@ Item {
sourceSize.width: width
sourceSize.height: height
mimeType: tdlibThumbnail.videoMimeType
+ fillMode: thumbnailImage.fillMode == Image.PreserveAspectFit ? Thumbnail.PreserveAspectFit : Thumbnail.PreserveAspectCrop
visible: opacity > 0
opacity: status === Thumbnail.Ready ? 1.0 : 0.0
Behavior on opacity { FadeAnimation {} }
diff --git a/qml/components/messageContent/MessageContentBase.qml b/qml/components/messageContent/MessageContentBase.qml
index f85bbfd..2ba2878 100644
--- a/qml/components/messageContent/MessageContentBase.qml
+++ b/qml/components/messageContent/MessageContentBase.qml
@@ -20,7 +20,6 @@ import QtQuick 2.6
import Sailfish.Silica 1.0
import QtMultimedia 5.6
import "../"
-import "../../js/functions.js" as Functions
import "../../js/debug.js" as Debug
Item {
diff --git a/qml/components/messageContent/MessagePhoto.qml b/qml/components/messageContent/MessagePhoto.qml
index beb2381..d561dab 100644
--- a/qml/components/messageContent/MessagePhoto.qml
+++ b/qml/components/messageContent/MessagePhoto.qml
@@ -22,28 +22,25 @@ import "../"
MessageContentBase {
- function calculateBiggest() {
- var candidateBiggest = rawMessage.content.photo.sizes[rawMessage.content.photo.sizes.length - 1];
- if (candidateBiggest.width === 0 && rawMessage.content.photo.sizes.length > 1) {
- for (var i = (rawMessage.content.photo.sizes.length - 2); i >= 0; i--) {
- candidateBiggest = rawMessage.content.photo.sizes[i];
- if (candidateBiggest.width > 0) {
+ height: Math.max(Theme.itemSizeExtraSmall, Math.min(Math.round(width * 0.66666666), width / getAspectRatio()))
+ readonly property alias photoData: photo.photo;
+
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("../../pages/MediaAlbumPage.qml"), {
+ "messages" : [rawMessage],
+ })
+ }
+ function getAspectRatio() {
+ var candidate = photoData.sizes[photoData.sizes.length - 1];
+ if (candidate.width === 0 && photoData.sizes.length > 1) {
+ for (var i = (photoData.sizes.length - 2); i >= 0; i--) {
+ candidate = photoData.sizes[i];
+ if (candidate.width > 0) {
break;
}
}
}
- return candidateBiggest;
- }
-
- height: Math.max(Theme.itemSizeExtraSmall, Math.min(defaultHeight, width / (biggest.width/biggest.height)))
- readonly property int defaultHeight: Math.round(width * 0.66666666)
- readonly property var biggest: calculateBiggest();
-
- onClicked: {
- pageStack.push(Qt.resolvedUrl("../../pages/ImagePage.qml"), {
- "photoData" : photo.photo,
-// "pictureFileInformation" : photo.fileInformation
- })
+ return candidate.width / candidate.height;
}
TDLibPhoto {
id: photo
@@ -51,7 +48,4 @@ MessageContentBase {
photo: rawMessage.content.photo
highlighted: parent.highlighted
}
- BackgroundImage {
- visible: !rawMessage.content.photo.minithumbnail && photo.image.status !== Image.Ready
- }
}
diff --git a/qml/components/messageContent/MessagePhotoAlbum.qml b/qml/components/messageContent/MessagePhotoAlbum.qml
new file mode 100644
index 0000000..bf3a942
--- /dev/null
+++ b/qml/components/messageContent/MessagePhotoAlbum.qml
@@ -0,0 +1,207 @@
+/*
+ Copyright (C) 2020 Sebastian J. Wolf and other contributors
+
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import "../"
+
+MessageContentBase {
+ id: messageContent
+ property string chatId
+ readonly property int heightUnit: Math.round(width * 0.66666666)
+ readonly property var albumId: rawMessage.media_album_id
+ property var albumMessageIds: messageListItem ? messageListItem.messageAlbumMessageIds : []//overlayFlickable.messageAlbumMessageIds
+ onAlbumMessageIdsChanged: albumMessages = getMessages() //chatModel.getMessagesForAlbum(messageContent.albumId)
+ property var albumMessages: getMessages()//chatModel.getMessagesForAlbum(messageContent.albumId)
+ property bool firstLarge: albumMessages.length % 2 !== 0;
+
+ clip: true
+ height: defaultExtraContentHeight//(firstLarge ? heightUnit * 0.75 : 0 ) + heightUnit * 0.25 * albumMessageIds.length
+
+
+ onClicked: {
+ if(messageListItem.precalculatedValues.pageIsSelecting) {
+ page.toggleMessageSelection(rawMessage);
+ return;
+ }
+ openDetail(-1);
+ }
+ function getMessages() {
+ var msgs = [rawMessage];
+ if(messageContent.albumId === '0' || messageContent.albumMessageIds.length < 2) {
+ return msgs;
+ }
+// var othermsgIds =
+ // getMessages from tdlib isn't faster
+// if(rawMessage && rawMessage.chat_id) {
+// var messages = [];
+// return albumMessageIds.map(function(msgId){
+// if(msgId === rawMessage.id) {
+// return rawMessage;
+// }
+// return tdLibWrapper.getMessage(rawMessage.chat_id, msgId);
+// })
+// }
+ chatModel.getMessagesForAlbum(messageContent.albumId, 1).forEach(function(msg){
+ msgs.push(msg);
+ });
+ //
+ return msgs; //chatModel.getMessagesForAlbum(messageContent.albumId);
+ }
+
+ function openDetail(index) {
+ console.log('open detail', index || 0);
+
+
+ pageStack.push(Qt.resolvedUrl("../../pages/MediaAlbumPage.qml"), {
+ "messages" : albumMessages,
+ "index": index || 0
+ })
+ }
+ Connections { // TODO: needed?
+ target: tdLibWrapper
+
+ onReceivedMessage: {
+ if (albumMessageIds.indexOf(messageId)) {
+// albumMessages = getMessages()
+ }
+ }
+ }
+
+ Component {
+ id: photoPreviewComponent
+ MessagePhoto {
+// width: parent.width
+// height: parent.height
+ messageListItem: messageContent.messageListItem
+ overlayFlickable: messageContent.overlayFlickable
+ rawMessage: albumMessages[modelIndex]
+ highlighted: mediaBackgroundItem.highlighted
+ }
+ }
+ Component {
+ id: videoPreviewComponent
+ Item {
+ property bool highlighted: mediaBackgroundItem.highlighted
+ anchors.fill: parent
+ clip: true
+ TDLibThumbnail {
+ id: tdLibImage
+ width: parent.width //don't use anchors here for easier custom scaling
+ height: parent.height
+ highlighted: parent.highlighted
+ thumbnail: albumMessages[modelIndex].content.video.thumbnail
+ minithumbnail: albumMessages[modelIndex].content.video.minithumbnail
+ }
+ Rectangle {
+ anchors {
+ fill: videoIcon
+ leftMargin: -Theme.paddingSmall
+ topMargin: -Theme.paddingSmall
+ bottomMargin: -Theme.paddingSmall
+ rightMargin: -Theme.paddingLarge
+
+ }
+
+ radius: Theme.paddingSmall
+ color: Theme.rgba(Theme.overlayBackgroundColor, 0.4)
+
+ }
+
+ Icon {
+ id: videoIcon
+ source: "image://theme/icon-m-video"
+ width: Theme.iconSizeSmall
+ height: Theme.iconSizeSmall
+ highlighted: parent.highlighted
+ anchors {
+ right: parent.right
+ rightMargin: Theme.paddingSmall
+ bottom: parent.bottom
+ }
+ }
+ }
+ }
+
+ Flow {
+ id: contentGrid
+ property int firstWidth: firstLarge ? contentGrid.width : normalWidth
+ property int firstHeight: firstLarge ? heightUnit - contentGrid.spacing : normalHeight
+ property int normalWidth: (contentGrid.width - contentGrid.spacing) / 2
+ property int normalHeight: (heightUnit / 2) - contentGrid.spacing
+
+ anchors.fill: parent
+ spacing: Theme.paddingMedium
+
+ Repeater {
+ model: albumMessages
+ delegate: BackgroundItem {
+ id: mediaBackgroundItem
+ property bool isLarge: firstLarge && model.index === 0
+ width: model.index === 0 ? contentGrid.firstWidth : contentGrid.normalWidth
+ height: model.index === 0 ? contentGrid.firstHeight : contentGrid.normalHeight
+
+ readonly property bool isSelected: messageListItem.precalculatedValues.pageIsSelecting && page.selectedMessages.some(function(existingMessage) {
+ return existingMessage.id === albumMessages[index].id
+ });
+ highlighted: isSelected || down || messageContent.highlighted
+ onClicked: {
+ if(messageListItem.precalculatedValues.pageIsSelecting) {
+ page.toggleMessageSelection(albumMessages[index]);
+ return;
+ }
+
+ openDetail(index);
+ }
+ onPressAndHold: {
+ page.toggleMessageSelection(albumMessages[index]);
+ }
+
+ Loader {
+ anchors.fill: parent
+// asynchronous: true
+
+ readonly property int modelIndex: index
+ sourceComponent: albumMessages[index].content["@type"] === 'messageVideo' ? videoPreviewComponent : photoPreviewComponent
+ opacity: status === Loader.Ready
+ Behavior on opacity {FadeAnimator{}}
+ }
+
+ /*
+ TODO video:
+ rawMessage.content.video.thumbnail
+ TDLibPhoto {
+ id: photo
+ anchors.fill: parent
+ photo: rawMessage.content.photo
+ highlighted: parent.highlighted
+ }
+ */
+ Rectangle {
+ visible: mediaBackgroundItem.isSelected
+ anchors {
+ fill: parent
+ }
+ color: 'transparent'
+ border.color: Theme.highlightColor
+ border.width: Theme.paddingSmall
+ }
+ }
+ }
+ }
+}
diff --git a/qml/components/messageContent/MessageVideo.qml b/qml/components/messageContent/MessageVideo.qml
index 8bec2ca..fe90397 100644
--- a/qml/components/messageContent/MessageVideo.qml
+++ b/qml/components/messageContent/MessageVideo.qml
@@ -26,7 +26,12 @@ import "../../js/debug.js" as Debug
MessageContentBase {
id: videoMessageComponent
- property var videoData: ( rawMessage.content['@type'] === "messageVideo" ) ? rawMessage.content.video : ( ( rawMessage.content['@type'] === "messageAnimation" ) ? rawMessage.content.animation : rawMessage.content.video_note )
+ property var videoData: ( rawMessage.content['@type'] === "messageVideo" )
+ ? rawMessage.content.video
+ : (
+ ( rawMessage.content['@type'] === "messageAnimation" )
+ ? rawMessage.content.animation
+ : rawMessage.content.video_note )
property string videoUrl;
property int previewFileId;
property int videoFileId;
diff --git a/qml/components/messageContent/MessageVideoAlbum.qml b/qml/components/messageContent/MessageVideoAlbum.qml
new file mode 100644
index 0000000..11b5798
--- /dev/null
+++ b/qml/components/messageContent/MessageVideoAlbum.qml
@@ -0,0 +1,19 @@
+/*
+ Copyright (C) 2020 Sebastian J. Wolf and other contributors
+
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+MessagePhotoAlbum {}
diff --git a/qml/components/messageContent/mediaAlbumPage/FullscreenOverlay.qml b/qml/components/messageContent/mediaAlbumPage/FullscreenOverlay.qml
new file mode 100644
index 0000000..8a069f1
--- /dev/null
+++ b/qml/components/messageContent/mediaAlbumPage/FullscreenOverlay.qml
@@ -0,0 +1,279 @@
+/*
+ Copyright (C) 2020 Sebastian J. Wolf and other contributors
+
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+import QtQuick 2.6
+import QtGraphicalEffects 1.0
+import Sailfish.Silica 1.0
+import "../../../js/functions.js" as Functions
+
+
+Item {
+ // id
+ id: overlay
+ // property declarations
+ property int pageCount
+ property int currentIndex
+ property alias text: captionLabel.text
+ property bool active: true
+ property var message
+ readonly property color gradientColor: '#bb000000'
+ readonly property int gradientPadding: Theme.itemSizeMedium
+ // signal declarations
+ // JavaScript functions
+ // object properties
+ anchors.fill: parent
+ opacity: active ? 1 : 0
+ Behavior on opacity { FadeAnimator {} }
+ // large property bindings
+ // child objects
+ // states
+ // transitions
+
+ onActiveChanged: {
+ console.log('overlay active', active)
+ }
+
+ function forwardMessage() {
+ var neededPermissions = Functions.getMessagesNeededForwardPermissions([message]);
+ pageStack.push(Qt.resolvedUrl("../../../pages/ChatSelectionPage.qml"), {
+ myUserId: tdLibWrapper.getUserInformation().id,
+ headerDescription: qsTr("Forward %Ln messages", "dialog header", 1),
+ payload: {fromChatId: message.chat_id, messageIds:[message.id], neededPermissions: neededPermissions},
+ state: "forwardMessages"
+ });
+ }
+
+ // "header"
+
+ LinearGradient {
+ id: topGradient
+ property int startY: 0;
+// Behavior on startY { NumberAnimation {duration: 2000} }
+ start: Qt.point(0, Math.min(height-gradientPadding*2, startY))
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ bottom: closeButton.bottom
+
+ bottomMargin: -gradientPadding
+ }
+
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: gradientColor }
+ GradientStop { position: 1.0; color: 'transparent' }
+ }
+ }
+
+
+ IconButton {
+ id: closeButton
+ icon.source: "image://theme/icon-m-cancel?" + (pressed
+ ? Theme.highlightColor
+ : Theme.lightPrimaryColor)
+ onClicked: pageStack.pop()
+ anchors {
+ right: parent.right
+ top: parent.top
+ margins: Theme.horizontalPageMargin
+ }
+ }
+
+ SilicaFlickable {
+ id: captionFlickable
+ anchors {
+ left: parent.left
+// leftMargin: Theme.horizontalPageMargin
+ right: closeButton.left
+ top: parent.top
+// topMargin: Theme.horizontalPageMargin
+ }
+ interactive: captionLabel.expanded && contentHeight > height
+ clip: true
+ height: Math.min(contentHeight, parent.height / 4)
+ contentHeight: captionLabel.height + Theme.horizontalPageMargin
+ flickableDirection: Flickable.VerticalFlick
+ VerticalScrollDecorator {
+ opacity: visible ? 1.0 : 0.0
+ flickable: captionFlickable
+ }
+
+ Label {
+ id: captionLabel
+ property bool expandable: expanded || height < contentHeight
+ property bool expanded
+
+ height: text ?
+ expanded
+ ? contentHeight
+ : Theme.itemSizeMedium
+ : 0;
+ // maximumLineCount: expanded ? 0 : 3
+ color: Theme.primaryColor
+// text: model.modelData.content.caption.text
+ text: Emoji.emojify(Functions.enhanceMessageText(message.content.caption, false), Theme.fontSizeExtraSmall)
+ onTextChanged: expanded = false
+ font.pixelSize: Theme.fontSizeExtraSmall
+ wrapMode: Text.WrapAnywhere
+ bottomPadding: expanded ? Theme.paddingLarge : 0
+ anchors {
+ left: parent.left
+ leftMargin: Theme.horizontalPageMargin
+ rightMargin: Theme.paddingLarge
+ right: parent.right
+ top: parent.top
+ topMargin: Theme.horizontalPageMargin
+ }
+
+ Behavior on height { NumberAnimation {duration: 300} }
+ Behavior on text {
+ SequentialAnimation {
+ FadeAnimation {
+ target: captionLabel
+ to: 0.0
+ duration: 300
+ }
+ PropertyAction {}
+ FadeAnimation {
+ target: captionLabel
+ to: 1.0
+ duration: 300
+ }
+ }
+ }
+
+ }
+
+ OpacityRampEffect {
+ sourceItem: captionLabel
+ enabled: !captionLabel.expanded
+ direction: OpacityRamp.TopToBottom
+ }
+ MouseArea {
+ anchors.fill: captionLabel
+ enabled: captionLabel.expandable
+ onClicked: {
+ captionLabel.expanded = !captionLabel.expanded
+ }
+ }
+ }
+
+ // "footer"
+ LinearGradient {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: buttons.top
+ bottom: parent.bottom
+ topMargin: -gradientPadding
+ }
+
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: 'transparent' }
+ GradientStop { position: 1.0; color: gradientColor }
+ }
+ }
+ Loader {
+ asynchronous: true
+ active: overlay.pageCount > 1
+
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: buttons.bottom
+ }
+ sourceComponent: Component {
+
+ Row {
+ id: pageIndicatorRow
+ height: Theme.paddingSmall
+ spacing: height
+ Repeater {
+ id: pageIndicator
+ model: overlay.pageCount
+ Rectangle {
+ property bool active: model.index === overlay.currentIndex
+ width: pageIndicatorRow.height
+ height: pageIndicatorRow.height
+ color: active ? Theme.lightPrimaryColor : Theme.rgba(Theme.lightSecondaryColor, Theme.opacityLow)
+ Behavior on color { ColorAnimation {} }
+ radius: Theme.paddingSmall
+ }
+ }
+ }
+ }
+ }
+
+
+ Row {
+ id: buttons
+ height: Theme.itemSizeSmall
+ width: childrenRect.width
+ spacing: Theme.paddingLarge
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ bottom: parent.bottom
+ bottomMargin: Theme.paddingLarge
+ }
+
+// IconButton {
+// icon.source: "image://theme/icon-m-cancel?" + (pressed
+// ? Theme.highlightColor
+// : Theme.lightPrimaryColor)
+// onClicked: pageStack.pop()
+
+// }
+ IconButton {
+ icon.source: "image://theme/icon-m-downloads?" + (pressed
+ ? Theme.highlightColor
+ : Theme.lightPrimaryColor)
+ onClicked: pageStack.pop()
+ }
+ Item {
+ width: Theme.itemSizeSmall
+ height: Theme.itemSizeSmall
+ }
+
+ IconButton {
+ enabled: message.can_be_forwarded
+ opacity: enabled ? 1.0 : 0.2
+ icon.source: "image://theme/icon-m-share?" + (pressed
+ ? Theme.highlightColor
+ : Theme.lightPrimaryColor)
+ onClicked: forwardMessage()
+ }
+ }
+ states: [
+ State {
+ name: 'hasCaption'
+ when: captionLabel.height > 0
+ PropertyChanges { target: topGradient;
+ startY: captionFlickable.height
+ }
+ AnchorChanges {
+ target: topGradient
+// anchors.top: captionLabel.verticalCenter
+ anchors.bottom: captionFlickable.bottom
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ AnchorAnimation { duration: 200 }
+ NumberAnimation { properties: "startY"; duration: 200 }
+ }
+}
diff --git a/qml/components/messageContent/mediaAlbumPage/PhotoComponent.qml b/qml/components/messageContent/mediaAlbumPage/PhotoComponent.qml
new file mode 100644
index 0000000..71d31b5
--- /dev/null
+++ b/qml/components/messageContent/mediaAlbumPage/PhotoComponent.qml
@@ -0,0 +1,16 @@
+
+import QtQuick 2.6
+
+ZoomImage {
+ photoData: model.modelData.content.photo
+ onClicked: {
+ console.log('clicked', zoomed)
+ if(zoomed) {
+ zoomOut(true)
+ page.overlayActive = true
+ } else {
+ page.overlayActive = !page.overlayActive
+ }
+ }
+
+}
diff --git a/qml/components/messageContent/mediaAlbumPage/VideoComponent.qml b/qml/components/messageContent/mediaAlbumPage/VideoComponent.qml
new file mode 100644
index 0000000..a3a01ac
--- /dev/null
+++ b/qml/components/messageContent/mediaAlbumPage/VideoComponent.qml
@@ -0,0 +1,181 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import WerkWolf.Fernschreiber 1.0
+import QtMultimedia 5.6
+import QtGraphicalEffects 1.0
+import "../../"
+
+Video {
+ id: video
+ property var videoData: model.modelData.content.video
+ readonly property bool isPlaying: playbackState === MediaPlayer.PlayingState
+ readonly property bool isCurrent: index === page.index
+ property bool shouldPlay
+ autoLoad: true
+ source: file.isDownloadingCompleted ? file.path : ''
+ onIsCurrentChanged: {
+ if(!isCurrent) {
+ pause()
+ }
+ }
+ onStatusChanged: {
+ if(status === MediaPlayer.EndOfMedia) {
+ page.overlayActive = true
+ }
+ }
+ TDLibThumbnail {
+ id: tdLibImage
+
+ property bool active: !file.isDownloadingCompleted || (!video.isPlaying && (video.position === 0 || video.status === MediaPlayer.EndOfMedia))
+ opacity: active ? 1 : 0
+ visible: active || opacity > 0
+
+ width: parent.width //don't use anchors here for easier custom scaling
+ height: parent.height
+// highlighted: parent.highlighted
+ thumbnail: videoData.thumbnail
+ minithumbnail: videoData.minithumbnail
+ fillMode: Image.PreserveAspectFit
+
+
+ }
+
+ TDLibFile {
+ id: file
+ autoLoad: false
+ tdlib: tdLibWrapper
+ fileInformation: videoData.video
+ property real progress: isDownloadingCompleted ? 1.0 : (downloadedSize / size)
+ onDownloadingCompletedChanged: {
+ if(isDownloadingCompleted) {
+ video.source = file.path
+ if(video.shouldPlay) {
+ video.play()
+ delayedOverlayHide.start()
+ video.shouldPlay = false
+ }
+ }
+ }
+ }
+ Label {
+ anchors.centerIn: parent
+ text: 'dl: '+file.downloadedSize
+ + ' \ns: '+file.size
+ + ' \nes: '+file.expectedSize
+ + ' \nd:'+file.isDownloadingActive
+ + ' \nc:'+file.isDownloadingCompleted
+
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: page.overlayActive = !page.overlayActive
+ }
+
+ RadialGradient { // white videos = invisible button. I can't tell since which SFOS version the opaque button is available, so:
+ id: buttonBg
+ anchors.centerIn: parent
+ width: Theme.itemSizeLarge; height: Theme.itemSizeLarge
+ property color baseColor: Theme.rgba(palette.overlayBackgroundColor, 0.2)
+
+ enabled: videoUI.active || !file.isDownloadingCompleted
+ opacity: enabled ? 1 : 0
+ Behavior on opacity { FadeAnimator {} }
+ gradient: Gradient {
+
+ GradientStop { position: 0.0; color: buttonBg.baseColor }
+ GradientStop { position: 0.3; color: buttonBg.baseColor }
+ GradientStop { position: 0.5; color: 'transparent' }
+ }
+
+ IconButton {
+ anchors.fill: parent
+ icon.source: "image://theme/icon-l-"+(video.isPlaying || video.shouldPlay ? 'pause' : 'play')+"?" + (pressed
+ ? Theme.highlightColor
+ : Theme.lightPrimaryColor)
+ onClicked: {
+ if (!file.isDownloadingCompleted) {
+ video.shouldPlay = !video.shouldPlay;
+ if(video.shouldPlay) {
+ file.load()
+ } else {
+ file.cancel()
+ }
+ return;
+ }
+
+ if (video.isPlaying) {
+ video.pause()
+ } else {
+ video.play()
+ delayedOverlayHide.start()
+ }
+ }
+ }
+ }
+
+ ProgressCircle {
+ property bool active: file.isDownloadingActive
+ opacity: active ? 1 : 0
+ Behavior on opacity { FadeAnimator {} }
+ anchors.centerIn: parent
+ value: file.progress
+ }
+ Item {
+ id: videoUI
+ property bool active: overlay.active// && file.isDownloadingCompleted
+ anchors.fill: parent
+ opacity: active ? 1 : 0
+ Behavior on opacity { FadeAnimator {} }
+
+ Slider {
+ id: slider
+ value: video.position
+ minimumValue: 0
+ maximumValue: video.duration || 0.1
+ enabled: parent.active && video.seekable
+ width: parent.width
+ handleVisible: false
+ animateValue: true
+ stepSize: 500
+ anchors {
+ bottom: parent.bottom
+ bottomMargin: Theme.itemSizeMedium
+ }
+ valueText: value > 0 || down ? Format.formatDuration((value)/1000, Formatter.Duration) : ''
+ leftMargin: Theme.horizontalPageMargin
+ rightMargin: Theme.horizontalPageMargin
+ onDownChanged: {
+ if(!down) {
+ video.seek(value)
+ value = Qt.binding(function() { return video.position })
+ }
+ }
+ Label {
+ anchors {
+ right: parent.right
+ rightMargin: Theme.horizontalPageMargin
+ bottom: parent.bottom
+ topMargin: Theme.paddingSmall
+ }
+ font.pixelSize: Theme.fontSizeExtraSmall
+ text: file.isDownloadingCompleted
+ ? Format.formatDuration((parent.maximumValue - parent.value)/1000, Formatter.Duration)
+ : (video.videoData.duration
+ ? Format.formatDuration(video.videoData.duration, Formatter.Duration) + ', '
+ : '') + Format.formatFileSize(file.size || file.expectedSize)
+ color: Theme.secondaryColor
+ }
+ }
+
+ Timer {
+ id: delayedOverlayHide
+ interval: 500
+ onTriggered: {
+ if(video.isPlaying) {
+ page.overlayActive = false
+ }
+ }
+ }
+ }
+}
diff --git a/qml/components/messageContent/mediaAlbumPage/ZoomArea.qml b/qml/components/messageContent/mediaAlbumPage/ZoomArea.qml
new file mode 100644
index 0000000..0fb04bc
--- /dev/null
+++ b/qml/components/messageContent/mediaAlbumPage/ZoomArea.qml
@@ -0,0 +1,148 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+
+SilicaFlickable {
+ // id
+ id: flickable
+ // property declarations
+ property real zoom
+ property bool zoomed
+ // override if needed
+ property bool zoomEnabled: true
+ property real minimumZoom: fitZoom
+ property real maximumZoom: 4 //Math.max(fitZoom, 1) * 3
+
+ default property alias zoomContentItem: zoomContentItem.data
+ property alias implicitContentWidth: zoomContentItem.implicitWidth
+ property alias implicitContentHeight: zoomContentItem.implicitHeight
+ // factor for "PreserveAspectFit"
+ readonly property real fitZoom: implicitContentWidth > 0 && implicitContentHeight > 0
+ ? Math.min(maximumZoom, width / implicitContentWidth, height / implicitContentHeight)
+ : 1.0
+ readonly property int minimumBoundaryAxis: (implicitContentWidth / implicitContentHeight) > (width / height) ? Qt.Horizontal : Qt.Vertical
+
+ // JavaScript functions
+ function zoomOut(animated) {
+ if (zoomed) {
+ if(animated) { zoomOutAnimation.start() }
+ else {
+ zoom = fitZoom
+ zoomed = false
+ }
+ }
+ }
+
+ // object properties
+ contentWidth: Math.max(width, zoomContentItem.width)
+ contentHeight: Math.max(height, zoomContentItem.height)
+ enabled: !zoomOutAnimation.running && implicitContentWidth > 0 && implicitContentHeight > 0
+ flickableDirection: Flickable.HorizontalAndVerticalFlick
+ interactive: zoomed
+ // According to Jolla, otherwise pinching would sometimes not work:
+ pressDelay: 0
+ Binding { // Update zoom on orientation changes and set as default
+ target: flickable
+ when: !zoomed
+ property: "zoom"
+ value: minimumZoom
+ }
+ // child objects
+
+ PinchArea {
+ id: pinchArea
+ parent: flickable.contentItem
+ width: flickable.contentWidth
+ height: flickable.contentHeight
+ enabled: zoomEnabled && minimumZoom !== maximumZoom && flickable.enabled
+ onPinchUpdated: {
+ scrollDecoratorTimer.restart()
+ var f = flickable;
+ var requestedZoomFactor = 1.0 + pinch.scale - pinch.previousScale;
+ var previousWidth = f.contentWidth
+ var previousHeight = f.contentHeight
+ var targetWidth
+ var targetHeight
+ var targetZoom = requestedZoomFactor * f.zoom;
+ if (targetZoom < f.minimumZoom) {
+ f.zoom = f.minimumZoom;
+ f.zoomed = false;
+ f.contentX = 0;
+ f.contentY = 0;
+ return
+ } else if(targetZoom >= f.maximumZoom) {
+ f.zoom = f.maximumZoom;
+ targetHeight = f.implicitContentHeight * f.zoom
+ targetWidth = f.implicitContentWidth * f.zoom
+ }
+ else if(targetZoom < f.maximumZoom) {
+ if (f.minimumBoundaryAxis == Qt.Horizontal) {
+ targetWidth = f.contentWidth * requestedZoomFactor
+ f.zoom = targetWidth / f.implicitContentWidth
+ targetHeight = f.implicitContentHeight * f.zoom
+ } else {
+ targetHeight = f.contentHeight * requestedZoomFactor
+ f.zoom = targetHeight / f.implicitContentHeight
+ targetWidth = f.implicitContentWidth * f.zoom
+ }
+ }
+ // calculate center difference
+ f.contentX += pinch.previousCenter.x - pinch.center.x
+ f.contentY += pinch.previousCenter.y - pinch.center.y
+ // move to new (zoomed) center. this jumps a tiny bit, but is bearable:
+ if (targetWidth > f.width)
+ f.contentX -= (previousWidth - targetWidth)/(previousWidth/pinch.previousCenter.x)
+ if (targetHeight > f.height)
+ f.contentY -= (previousHeight - targetHeight)/(previousHeight/pinch.previousCenter.y)
+
+ f.zoomed = true
+ }
+ onPinchFinished: {
+ returnToBounds()
+ }
+ Item {
+ id: zoomContentItem
+ anchors.centerIn: parent
+ implicitWidth: flickable.width
+ implicitHeight: flickable.height
+ width: Math.ceil(implicitWidth * zoom)
+ height: Math.ceil(implicitHeight * zoom)
+ }
+ }
+ // enable zoom to minimumZoom on click
+ ParallelAnimation {
+ id: zoomOutAnimation
+ NumberAnimation {
+ target: flickable
+ properties: "contentX, contentY"
+ to: 0
+ }
+ NumberAnimation {
+ target: flickable
+ property: "zoom"
+ to: fitZoom
+ }
+ onRunningChanged: {
+ if(!running) {
+ zoomed = false
+ }
+ }
+ }
+
+ // show scroll decorators when scrolling OR zooming
+ Timer {
+ id: scrollDecoratorTimer
+ readonly property bool moving: flickable.moving
+ readonly property bool showing: moving || running
+ onMovingChanged: restart()
+ interval: 300
+ }
+
+ VerticalScrollDecorator {
+ flickable: flickable
+ opacity: scrollDecoratorTimer.showing ? 1.0 : 0.0
+ }
+ HorizontalScrollDecorator {
+ flickable: flickable
+ opacity: scrollDecoratorTimer.showing ? 1.0 : 0.0
+ }
+}
diff --git a/qml/components/messageContent/mediaAlbumPage/ZoomImage.qml b/qml/components/messageContent/mediaAlbumPage/ZoomImage.qml
new file mode 100644
index 0000000..bc3418b
--- /dev/null
+++ b/qml/components/messageContent/mediaAlbumPage/ZoomImage.qml
@@ -0,0 +1,127 @@
+import QtQuick 2.0
+import Sailfish.Silica 1.0
+import WerkWolf.Fernschreiber 1.0
+import "../../"
+
+ZoomArea {
+ // id
+ id: zoomArea
+ property var photoData //albumMessages[index].content.photo
+ property bool active: true
+ property alias image: image
+
+ signal clicked
+
+ maximumZoom: Math.max(Screen.width, Screen.height) / 200
+// maximumZoom: Math.max(fitZoom, 1) * 3
+ implicitContentWidth: image.implicitWidth
+ implicitContentHeight: image.implicitHeight
+ zoomEnabled: image.status == Image.Ready
+
+ onActiveChanged: {
+ if (!active) {
+ zoomOut()
+ }
+ }
+
+ Component.onCompleted: {
+// var photoData = albumMessages[index].content.photo;
+ if (photoData) {
+
+ var biggestIndex = -1
+ for (var i = 0; i < photoData.sizes.length; i++) {
+ if (biggestIndex === -1 || photoData.sizes[i].width > photoData.sizes[biggestIndex].width) {
+ biggestIndex = i;
+ }
+ }
+ if (biggestIndex > -1) {
+// imageDelegate.imageWidth = photoData.sizes[biggestIndex].width;
+// imageDelegate.imageHeight = photoData.sizes[biggestIndex].height;
+ image.sourceSize.width = photoData.sizes[biggestIndex].width
+ image.sourceSize.height = photoData.sizes[biggestIndex].height
+ image.fileInformation = photoData.sizes[biggestIndex].photo
+
+ console.log('loading photo', JSON.stringify(image.fileInformation))
+ }
+ }
+ }
+ TDLibImage {
+ id: image
+
+ width: parent.width
+ height: parent.height
+ source: file.isDownloadingCompleted ? file.path : ""
+// enabled: true //!!file.fileId
+// anchors.fill: parent
+ anchors.centerIn: parent
+
+ fillMode: Image.PreserveAspectFit
+ asynchronous: true
+ smooth: !(movingVertically || movingHorizontally)
+
+// sourceSize.width: Screen.height
+// visible: opacity > 0
+// opacity: status === Image.Ready ? 1 : 0
+
+ Behavior on opacity { FadeAnimator{} }
+ }
+// Label {
+// anchors.fill: parent
+// text: 'ok?' + image.enabled +' fileid:' +!!(image.file.fileId)
+// + '\n - dl?' + image.file.isDownloadingActive
+// + '\n completed?' + image.file.isDownloadingCompleted + ' path:'+ image.file.path
+// + '\n ' + image.source
+// wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+// }
+// Rectangle {
+// color: 'green'
+// anchors.fill: image
+// opacity: 0.3
+
+// }
+
+// Image {
+// id: image
+// anchors.fill: parent
+// smooth: !(movingVertically || movingHorizontally)
+// sourceSize.width: Screen.height
+// fillMode: Image.PreserveAspectFit
+// asynchronous: true
+// cache: false
+
+// onSourceChanged: {
+// zoomOut()
+// }
+
+// opacity: status == Image.Ready ? 1 : 0
+// Behavior on opacity { FadeAnimator{} }
+// }
+ Item {
+ anchors.fill: parent
+
+ }
+ MouseArea {
+ anchors.centerIn: parent
+ width: zoomArea.contentWidth
+ height: zoomArea.contentHeight
+ onClicked: zoomArea.clicked()
+ }
+
+
+ BusyIndicator {
+ running: image.file.isDownloadingActive && !delayBusyIndicator.running
+ size: BusyIndicatorSize.Large
+ anchors.centerIn: parent
+ parent: zoomArea
+ Timer {
+ id: delayBusyIndicator
+ running: image.file.isDownloadingActive
+ interval: 1000
+ }
+ }
+// Rectangle {
+// color: 'green'
+// anchors.fill: parent
+// parent: zoomArea
+// }
+}
diff --git a/qml/components/settingsPage/SettingsBehavior.qml b/qml/components/settingsPage/SettingsBehavior.qml
index 2232791..c1d15ab 100644
--- a/qml/components/settingsPage/SettingsBehavior.qml
+++ b/qml/components/settingsPage/SettingsBehavior.qml
@@ -205,6 +205,38 @@ AccordionItem {
}
}
+ TextSwitch {
+ checked: appSettings.notificationSoundsEnabled && enabled
+ text: qsTr("Enable notification sounds")
+ description: qsTr("When sounds are enabled, Fernschreiber will use the current Sailfish OS notification sound for chats, which can be configured in the system settings.")
+ enabled: parent.enabled
+ automaticCheck: false
+ onClicked: {
+ appSettings.notificationSoundsEnabled = !checked
+ }
+ }
+ }
+
+ TextSwitch {
+ checked: appSettings.notificationSuppressContent && enabled
+ text: qsTr("Hide content in notifications")
+ enabled: parent.enabled
+ automaticCheck: false
+ onClicked: {
+ appSettings.notificationSuppressContent = !checked
+ }
+ }
+
+ TextSwitch {
+ checked: appSettings.notificationTurnsDisplayOn && enabled
+ text: qsTr("Notification turns on the display")
+ enabled: parent.enabled
+ automaticCheck: false
+ onClicked: {
+ appSettings.notificationTurnsDisplayOn = !checked
+ }
+ }
+
TextSwitch {
checked: appSettings.notificationSoundsEnabled && enabled
text: qsTr("Enable notification sounds")
@@ -218,4 +250,3 @@ AccordionItem {
}
}
}
-}
diff --git a/qml/js/functions.js b/qml/js/functions.js
index 5bdd751..f9454d3 100644
--- a/qml/js/functions.js
+++ b/qml/js/functions.js
@@ -448,7 +448,12 @@ function handleLink(link) {
} else if (link.indexOf(tMePrefixHttp) === 0) {
handleTMeLink(link, tMePrefixHttp);
} else {
- Qt.openUrlExternally(link);
+ Debug.log("Trying to open URL externally: " + link)
+ if (link.indexOf("://") === -1) {
+ Qt.openUrlExternally("https://" + link)
+ } else {
+ Qt.openUrlExternally(link);
+ }
}
}
}
@@ -512,7 +517,7 @@ function handleErrorMessage(code, message) {
}
function getMessagesNeededForwardPermissions(messages) {
- var neededPermissions = ["can_send_messages"]
+ var neededPermissions = ["can_send_basic_messages"]
var mediaMessageTypes = ["messageAudio", "messageDocument", "messagePhoto", "messageVideo", "messageVideoNote", "messageVoiceNote"]
var otherMessageTypes = ["messageAnimation", "messageGame", "messageSticker"]
diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml
index 2c8f522..d20a373 100644
--- a/qml/pages/ChatPage.qml
+++ b/qml/pages/ChatPage.qml
@@ -601,12 +601,16 @@ Page {
onSponsoredMessageReceived: {
chatPage.containsSponsoredMessages = true;
}
+ onReactionsUpdated: {
+ availableReactions = tdLibWrapper.getChatReactions(chatInformation.id);
+ }
}
Connections {
target: chatModel
onMessagesReceived: {
- Debug.log("[ChatPage] Messages received, view has ", chatView.count, " messages, last known message index ", modelIndex, ", own messages were read before index ", lastReadSentIndex);
+ var proxyIndex = chatProxyModel.mapRowFromSource(modelIndex, -1);
+ Debug.log("[ChatPage] Messages received, view has ", chatView.count, " messages, last known message index ", proxyIndex, "("+modelIndex+"), own messages were read before index ", lastReadSentIndex);
if (totalCount === 0) {
if (chatPage.iterativeInitialization) {
chatPage.iterativeInitialization = false;
@@ -620,9 +624,9 @@ Page {
}
chatView.lastReadSentIndex = lastReadSentIndex;
- chatView.scrollToIndex(modelIndex);
+ chatView.scrollToIndex(proxyIndex);
chatPage.loading = false;
- if (chatOverviewItem.visible && modelIndex >= (chatView.count - 10)) {
+ if (chatOverviewItem.visible && proxyIndex >= (chatView.count - 10)) {
chatView.inCooldown = true;
chatModel.triggerLoadMoreFuture();
}
@@ -635,6 +639,8 @@ Page {
chatViewCooldownTimer.restart();
chatViewStartupReadTimer.restart();
+ /*
+ // Double-tap for reactions is currently disabled, let's see if we'll ever need it again
var remainingDoubleTapHints = appSettings.remainingDoubleTapHints;
Debug.log("Remaining double tap hints: " + remainingDoubleTapHints);
if (remainingDoubleTapHints > 0) {
@@ -643,6 +649,7 @@ Page {
tapHintLabel.visible = true;
appSettings.remainingDoubleTapHints = remainingDoubleTapHints - 1;
}
+ */
}
onNewMessageReceived: {
if (( chatView.manuallyScrolledToBottom && Qt.application.state === Qt.ApplicationActive ) || message.sender_id.user_id === chatPage.myUserId) {
@@ -662,10 +669,13 @@ Page {
chatView.lastReadSentIndex = lastReadSentIndex;
}
onMessagesIncrementalUpdate: {
- Debug.log("Incremental update received. View now has ", chatView.count, " messages, view is on index ", modelIndex, ", own messages were read before index ", lastReadSentIndex);
+ var proxyIndex = chatProxyModel.mapRowFromSource(modelIndex, -1);
+ Debug.log("Incremental update received. View now has ", chatView.count, " messages, view is on index ", proxyIndex, "("+modelIndex+"), own messages were read before index ", lastReadSentIndex);
chatView.lastReadSentIndex = lastReadSentIndex;
if (!chatPage.isInitialized) {
- chatView.scrollToIndex(modelIndex);
+ if (proxyIndex > -1) {
+ chatView.scrollToIndex(proxyIndex);
+ }
}
if (chatView.height > chatView.contentHeight) {
Debug.log("[ChatPage] Chat content quite small...");
@@ -741,14 +751,26 @@ Page {
onTriggered: {
Debug.log("scroll position changed, message index: ", lastQueuedIndex);
Debug.log("unread count: ", chatInformation.unread_count);
- var messageToRead = chatModel.getMessage(lastQueuedIndex);
+ var modelIndex = chatProxyModel.mapRowToSource(lastQueuedIndex);
+ var messageToRead = chatModel.getMessage(modelIndex);
if (messageToRead['@type'] === "sponsoredMessage") {
Debug.log("sponsored message to read: ", messageToRead.id);
tdLibWrapper.viewMessage(chatInformation.id, messageToRead.message_id, false);
} else if (chatInformation.unread_count > 0 && lastQueuedIndex > -1) {
- Debug.log("message to read: ", messageToRead.id);
- if (messageToRead && messageToRead.id) {
- tdLibWrapper.viewMessage(chatInformation.id, messageToRead.id, false);
+ if (messageToRead) {
+ Debug.log("message to read: ", messageToRead.id);
+ var messageId = messageToRead.id;
+ var type = messageToRead.content["@type"];
+ if (messageToRead.media_album_id !== '0') {
+ var albumIds = chatModel.getMessageIdsForAlbum(messageToRead.media_album_id);
+ if (albumIds.length > 0) {
+ messageId = albumIds[albumIds.length - 1];
+ Debug.log("message to read last album message id: ", messageId);
+ }
+ }
+ if (messageId) {
+ tdLibWrapper.viewMessage(chatInformation.id, messageId, false);
+ }
}
lastQueuedIndex = -1
}
@@ -1216,7 +1238,6 @@ Page {
readonly property int messageInReplyToHeight: Theme.fontSizeExtraSmall * 2.571428571 + Theme.paddingSmall;
readonly property int webPagePreviewHeight: ( (textColumnWidth * 2 / 3) + (6 * Theme.fontSizeExtraSmall) + ( 7 * Theme.paddingSmall) )
readonly property bool pageIsSelecting: chatPage.isSelecting
-
}
function handleScrollPositionChanged() {
@@ -1239,6 +1260,9 @@ Page {
positionViewAtIndex(index, (mode === undefined) ? ListView.Contain : mode)
if(index === chatView.count - 1) {
manuallyScrolledToBottom = true;
+ if(!chatView.atYEnd) {
+ chatView.positionViewAtEnd();
+ }
}
}
}
@@ -1271,7 +1295,13 @@ Page {
}
}
- model: chatModel
+ BoolFilterModel {
+ id: chatProxyModel
+ sourceModel: chatModel
+ filterRoleName: "album_entry_filter"
+ filterValue: false
+ }
+ model: chatProxyModel
header: Component {
Loader {
active: !!chatPage.botInformation
@@ -1304,7 +1334,8 @@ Page {
}
}
- function getContentComponentHeight(contentType, content, parentWidth) {
+ function getContentComponentHeight(contentType, content, parentWidth, albumEntries) {
+ var unit;
switch(contentType) {
case "messageAnimatedEmoji":
return content.animated_emoji.sticker.height;
@@ -1320,6 +1351,10 @@ Page {
case "messageVenue":
return parentWidth * 0.66666666; // 2 / 3;
case "messagePhoto":
+ if(albumEntries > 0) {
+ unit = (parentWidth * 0.66666666)
+ return (albumEntries % 2 !== 0 ? unit * 0.75 : 0) + unit * albumEntries * 0.25
+ }
var biggest = content.photo.sizes[content.photo.sizes.length - 1];
var aspectRatio = biggest.width/biggest.height;
return Math.max(Theme.itemSizeExtraSmall, Math.min(parentWidth * 0.66666666, parentWidth / aspectRatio));
@@ -1328,6 +1363,10 @@ Page {
case "messageSticker":
return content.sticker.height;
case "messageVideo":
+ if(albumEntries > 0) {
+ unit = (parentWidth * 0.66666666)
+ return (albumEntries % 2 !== 0 ? unit * 0.75 : 0) + unit * albumEntries * 0.25
+ }
return Functions.getVideoHeight(parentWidth, content.video);
case "messageVideoNote":
return parentWidth
@@ -1383,10 +1422,11 @@ Page {
chatId: chatModel.chatId
myMessage: model.display
messageId: model.message_id
+ messageAlbumMessageIds: model.album_message_ids
messageViewCount: model.view_count
reactions: model.reactions
chatReactions: availableReactions
- messageIndex: model.index
+ messageIndex: chatProxyModel.mapRowToSource(model.index)
hasContentComponent: !!myMessage.content && chatView.delegateMessagesContent.indexOf(model.content_type) > -1
canReplyToMessage: chatPage.canSendMessages
onReplyToMessage: {
@@ -1407,9 +1447,21 @@ Page {
id: messageListViewItemSimpleComponent
MessageListViewItemSimple {}
}
- sourceComponent: chatView.simpleDelegateMessages.indexOf(model.content_type) > -1 ? messageListViewItemSimpleComponent : messageListViewItemComponent
+ Component {
+ id: messageListViewItemHiddenComponent
+ Item {
+ property var myMessage: display
+ property bool senderIsUser: myMessage.sender_id["@type"] === "messageSenderUser"
+ property var userInformation: senderIsUser ? tdLibWrapper.getUserInformation(myMessage.sender_id.user_id) : null
+ property bool isOwnMessage: senderIsUser && chatPage.myUserId === myMessage.sender_id.user_id
+ height: 1
+ }
+ }
+ sourceComponent: chatView.simpleDelegateMessages.indexOf(model.content_type) > -1
+ ? messageListViewItemSimpleComponent
+ : messageListViewItemComponent
}
- VerticalScrollDecorator {}
+ VerticalScrollDecorator { flickable: chatView }
ViewPlaceholder {
id: chatViewPlaceholder
diff --git a/qml/pages/MediaAlbumPage.qml b/qml/pages/MediaAlbumPage.qml
new file mode 100644
index 0000000..77a5caa
--- /dev/null
+++ b/qml/pages/MediaAlbumPage.qml
@@ -0,0 +1,109 @@
+/*
+ Copyright (C) 2020 Sebastian J. Wolf and other contributors
+
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+// jolla-gallery/pages/FlickableImageView.qml
+/*
+
+FullscreenContentPage
+ - PagedView (jolla-gallery/FlickableImageView)
+ - delegate: Loader
+ - SilicaFlickable (Silica.private/ZoomableFlickable) (Sailfish.Gallery/ImageViewer)
+ - PinchArea
+ - dragDetector(?)
+ - image
+ - Item (Sailfish.Gallery/GalleryOverlay)
+
+*/
+
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import WerkWolf.Fernschreiber 1.0
+import "../components"
+
+import "../components/messageContent/mediaAlbumPage"
+import "../js/twemoji.js" as Emoji
+import "../js/functions.js" as Functions
+
+Page {
+ // id
+ id: page
+ // property declarations
+
+ property alias index: pagedView.currentIndex
+ property alias overlayActive: overlay.active
+ property alias delegate: pagedView.delegate
+ property var messages: [];
+ // message.content.caption.text
+ palette.colorScheme: Theme.LightOnDark
+ clip: status !== PageStatus.Active || pageStack.dragInProgress
+ navigationStyle: PageNavigation.Vertical
+ backgroundColor: 'black'
+ allowedOrientations: Orientation.All
+ // signal declarations
+ // JavaScript functions
+
+ // object (parent) properties
+ // large property bindings
+ // child objects
+ // states
+ // transitions
+
+
+
+ // content
+ PagedView {
+ id: pagedView
+ anchors.fill: parent
+ model: messages
+ delegate: Component {
+ Loader {
+ id: loader
+ asynchronous: true
+ visible: status == Loader.Ready
+ width: PagedView.contentWidth
+ height: PagedView.contentHeight
+
+ states: [
+ State {
+ when: model.modelData.content['@type'] === 'messagePhoto'
+ PropertyChanges {
+ target: loader
+ source: "../components/messageContent/mediaAlbumPage/PhotoComponent.qml"
+ }
+ },
+ State {
+ when: model.modelData.content['@type'] === 'messageVideo'
+ PropertyChanges {
+ target: loader
+ source: "../components/messageContent/mediaAlbumPage/VideoComponent.qml"
+ }
+ }
+ ]
+ }
+ }
+ }
+
+ // overlay
+ FullscreenOverlay {
+ id: overlay
+ pageCount: messages.length
+ currentIndex: page.index
+ message: messages[currentIndex]
+//
+ }
+}
diff --git a/rpm/harbour-fernschreiber.spec b/rpm/harbour-fernschreiber.spec
index 71e512d..41e8918 100644
--- a/rpm/harbour-fernschreiber.spec
+++ b/rpm/harbour-fernschreiber.spec
@@ -6,9 +6,9 @@
Name: harbour-fernschreiber
# >> macros
+# << macros
%define __provides_exclude_from ^%{_datadir}/.*$
%define __requires_exclude ^lib(tdjson|ssl|crypto).*$
-# << macros
Summary: Fernschreiber is a Telegram client for Aurora OS
Version: 0.17
@@ -70,7 +70,7 @@ desktop-file-install --delete-original \
%files
%defattr(-,root,root,-)
-%{_bindir}
+%{_bindir}/%{name}
%{_datadir}/%{name}
%{_datadir}/applications/%{name}.desktop
%{_datadir}/icons/hicolor/*/apps/%{name}.png
diff --git a/src/appsettings.cpp b/src/appsettings.cpp
index 1a4b376..6df3f38 100644
--- a/src/appsettings.cpp
+++ b/src/appsettings.cpp
@@ -303,7 +303,7 @@ void AppSettings::setDelayMessageRead(bool enable)
bool AppSettings::highlightUnreadConversations() const
{
- return settings.value(KEY_HIGHLIGHT_UNREADCONVS, true).toBool();
+ return settings.value(KEY_HIGHLIGHT_UNREADCONVS, false).toBool();
}
void AppSettings::setHighlightUnreadConversations(bool enable)
diff --git a/src/boolfiltermodel.cpp b/src/boolfiltermodel.cpp
new file mode 100644
index 0000000..0882292
--- /dev/null
+++ b/src/boolfiltermodel.cpp
@@ -0,0 +1,153 @@
+/*
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+
+#include "boolfiltermodel.h"
+
+#define DEBUG_MODULE BoolFilterModel
+#include "debuglog.h"
+
+BoolFilterModel::BoolFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ setDynamicSortFilter(true);
+// setFilterCaseSensitivity(Qt::CaseInsensitive);
+// setFilterFixedString(QString());
+ filterValue = true;
+}
+
+void BoolFilterModel::setSource(QObject *model)
+{
+ setSourceModel(qobject_cast(model));
+}
+
+void BoolFilterModel::setSourceModel(QAbstractItemModel *model)
+{
+ if (sourceModel() != model) {
+ LOG(model);
+ QSortFilterProxyModel::setSourceModel(model);
+ updateFilterRole();
+ emit sourceChanged();
+ }
+}
+
+QString BoolFilterModel::getFilterRoleName() const
+{
+ return filterRoleName;
+}
+
+void BoolFilterModel::setFilterRoleName(QString role)
+{
+ if (filterRoleName != role) {
+ filterRoleName = role;
+ LOG(role);
+ updateFilterRole();
+ emit filterRoleNameChanged();
+ }
+}
+
+bool BoolFilterModel::getFilterValue() const
+{
+ return filterValue;
+}
+
+void BoolFilterModel::setFilterValue(bool value)
+{
+ if(value != filterValue) {
+ filterValue = value;
+ invalidateFilter();
+ }
+}
+
+int BoolFilterModel::mapRowFromSource(int i, int fallbackDirection)
+{
+ QModelIndex myIndex = mapFromSource(sourceModel()->index(i, 0));
+ LOG("mapping index" << i << "to source model:" << myIndex.row() << "valid?" << myIndex.isValid());
+ if(myIndex.isValid()) {
+ return myIndex.row();
+ }
+
+ if(fallbackDirection > 0) {
+ int max = sourceModel()->rowCount();
+ i += 1;
+ while (i < max) {
+ myIndex = mapFromSource(sourceModel()->index(i, 0));
+
+ LOG("fallback ++ " << i << "to source model:" << myIndex.row() << "valid?" << myIndex.isValid());
+ if(myIndex.isValid()) {
+ return myIndex.row();
+ }
+ i += 1;
+ }
+ } else if(fallbackDirection < 0) {
+ i -= 1;
+ while (i > -1) {
+ myIndex = mapFromSource(sourceModel()->index(i, 0));
+ LOG("fallback -- " << i << "to source model:" << myIndex.row() << "valid?" << myIndex.isValid());
+ if(myIndex.isValid()) {
+ return myIndex.row();
+ }
+ i -= 1;
+ }
+ }
+
+ return myIndex.row(); // may still be -1
+}
+
+int BoolFilterModel::mapRowToSource(int i)
+{
+ QModelIndex sourceIndex = mapToSource(index(i, 0));
+ return sourceIndex.row();
+}
+bool BoolFilterModel::filterAcceptsRow(int sourceRow,
+ const QModelIndex &sourceParent) const
+ {
+// sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(); //.toString().contains( /*string for column 0*/ ))
+// LOG("Filter Role " << filterRole());
+// QModelIndex index = this->sourceModel()->index(sourceRow,1,sourceParent);
+// sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(filterRole()).toBool();
+// LOG("Filter index DATA"<< sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(filterRole())); //<< index << index.isValid());
+// LOG("Filter parent " << sourceParent << sourceParent.isValid());
+// LOG("Filter Model Value" << sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(filterRole()).toBool());
+// LOG("Filter Model filterValue" << filterValue);
+// LOG("Filter Model result" << (sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(filterRole()).toBool() == filterValue));
+// LOG("Filter Model MESSAGE" << sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data());
+ return sourceModel()->index(sourceRow, 0, sourceParent.child(sourceRow, 0)).data(filterRole()).toBool() == filterValue;
+ }
+
+int BoolFilterModel::findRole(QAbstractItemModel *model, QString role)
+{
+ if (model && !role.isEmpty()) {
+ const QByteArray roleName(role.toUtf8());
+ const QHash roleMap(model->roleNames());
+ const QList roles(roleMap.keys());
+ const int n = roles.count();
+ for (int i = 0; i < n; i++) {
+ const QByteArray name(roleMap.value(roles.at(i)));
+ if (name == roleName) {
+ LOG(role << roles.at(i));
+ return roles.at(i);
+ }
+ }
+ LOG("Unknown role" << role);
+ }
+ return -1;
+}
+
+void BoolFilterModel::updateFilterRole()
+{
+ const int role = findRole(sourceModel(), filterRoleName);
+ setFilterRole((role >= 0) ? role : Qt::DisplayRole);
+}
diff --git a/src/boolfiltermodel.h b/src/boolfiltermodel.h
new file mode 100644
index 0000000..770b8d3
--- /dev/null
+++ b/src/boolfiltermodel.h
@@ -0,0 +1,63 @@
+/*
+ This file is part of Fernschreiber.
+
+ Fernschreiber is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Fernschreiber is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Fernschreiber. If not, see .
+*/
+
+#ifndef BOOLFILTERMODEL_H
+#define BOOLFILTERMODEL_H
+
+#include
+
+class BoolFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString filterRoleName READ getFilterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged)
+ Q_PROPERTY(bool filterValue READ getFilterValue WRITE setFilterValue NOTIFY filterValueChanged)
+ Q_PROPERTY(QObject* sourceModel READ sourceModel WRITE setSource NOTIFY sourceChanged)
+
+public:
+ BoolFilterModel(QObject *parent = Q_NULLPTR);
+
+ void setSource(QObject* model);
+ void setSourceModel(QAbstractItemModel *model) Q_DECL_OVERRIDE;
+
+
+ QString getFilterRoleName() const;
+ void setFilterRoleName(QString role);
+
+ bool getFilterValue() const;
+ void setFilterValue(bool value);
+ Q_INVOKABLE int mapRowFromSource(int i, int fallbackDirection);
+ Q_INVOKABLE int mapRowToSource(int i);
+
+signals:
+ void sourceChanged();
+ void filterRoleNameChanged();
+ void filterValueChanged();
+
+private slots:
+ void updateFilterRole();
+
+private:
+ static int findRole(QAbstractItemModel *model, QString role);
+
+private:
+ QString filterRoleName;
+ bool filterValue;
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+};
+
+#endif // BOOLFILTERMODEL_H
diff --git a/src/chatmodel.cpp b/src/chatmodel.cpp
index 96c40ce..7dad231 100644
--- a/src/chatmodel.cpp
+++ b/src/chatmodel.cpp
@@ -30,6 +30,7 @@ namespace {
const QString ID("id");
const QString CONTENT("content");
const QString CHAT_ID("chat_id");
+ const QString DATE("date");
const QString PHOTO("photo");
const QString SMALL("small");
const QString UNREAD_COUNT("unread_count");
@@ -48,6 +49,7 @@ namespace {
// "view_count": 47
// }
const QString TYPE_MESSAGE_INTERACTION_INFO("messageInteractionInfo");
+ const QString MEDIA_ALBUM_ID("media_album_id");
const QString INTERACTION_INFO("interaction_info");
const QString VIEW_COUNT("view_count");
const QString REACTIONS("reactions");
@@ -63,7 +65,9 @@ public:
RoleMessageId,
RoleMessageContentType,
RoleMessageViewCount,
- RoleMessageReactions
+ RoleMessageReactions,
+ RoleMessageAlbumEntryFilter,
+ RoleMessageAlbumMessageIds,
};
enum RoleFlag {
@@ -71,7 +75,9 @@ public:
RoleFlagMessageId = 0x02,
RoleFlagMessageContentType = 0x04,
RoleFlagMessageViewCount = 0x08,
- RoleFlagMessageReactions = 0x16
+ RoleFlagMessageReactions = 0x16,
+ RoleFlagMessageAlbumEntryFilter = 0x32,
+ RoleFlagMessageAlbumMessageIds = 0x64
};
MessageData(const QVariantMap &data, qlonglong msgid);
@@ -86,12 +92,16 @@ public:
uint updateViewCount(const QVariantMap &interactionInfo);
uint updateInteractionInfo(const QVariantMap &interactionInfo);
uint updateReactions(const QVariantMap &interactionInfo);
+ uint updateAlbumEntryFilter(const bool isAlbumChild);
+ uint updateAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds);
QVector diff(const MessageData *message) const;
QVector setMessageData(const QVariantMap &data);
QVector setContent(const QVariantMap &content);
QVector setReplyMarkup(const QVariantMap &replyMarkup);
QVector setInteractionInfo(const QVariantMap &interactionInfo);
+ QVector setAlbumEntryFilter(bool isAlbumChild);
+ QVector setAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds);
int senderUserId() const;
qlonglong senderChatId() const;
@@ -104,6 +114,8 @@ public:
QString messageContentType;
int viewCount;
QVariantList reactions;
+ bool albumEntryFilter;
+ QVariantList albumMessageIds;
};
ChatModel::MessageData::MessageData(const QVariantMap &data, qlonglong msgid) :
@@ -112,7 +124,9 @@ ChatModel::MessageData::MessageData(const QVariantMap &data, qlonglong msgid) :
messageType(data.value(_TYPE).toString()),
messageContentType(data.value(CONTENT).toMap().value(_TYPE).toString()),
viewCount(data.value(INTERACTION_INFO).toMap().value(VIEW_COUNT).toInt()),
- reactions(data.value(INTERACTION_INFO).toMap().value(REACTIONS).toList())
+ reactions(data.value(INTERACTION_INFO).toMap().value(REACTIONS).toList()),
+ albumEntryFilter(false),
+ albumMessageIds(QVariantList())
{
}
@@ -134,6 +148,12 @@ QVector ChatModel::MessageData::flagsToRoles(uint flags)
if (flags & RoleFlagMessageReactions) {
roles.append(RoleMessageReactions);
}
+ if (flags & RoleFlagMessageAlbumEntryFilter) {
+ roles.append(RoleMessageAlbumEntryFilter);
+ }
+ if (flags & RoleFlagMessageAlbumMessageIds) {
+ roles.append(RoleMessageAlbumMessageIds);
+ }
return roles;
}
@@ -169,6 +189,12 @@ QVector ChatModel::MessageData::diff(const MessageData *message) const
if (message->reactions != reactions) {
roles.append(RoleMessageReactions);
}
+ if (message->albumEntryFilter != albumEntryFilter) {
+ roles.append(RoleMessageAlbumEntryFilter);
+ }
+ if (message->albumMessageIds != albumMessageIds) {
+ roles.append(RoleMessageAlbumMessageIds);
+ }
}
return roles;
}
@@ -237,6 +263,37 @@ uint ChatModel::MessageData::updateReactions(const QVariantMap &interactionInfo)
return (reactions == oldReactions) ? 0 : RoleFlagMessageReactions;
}
+uint ChatModel::MessageData::updateAlbumEntryFilter(const bool isAlbumChild)
+{
+ LOG("Updating album filter... for id " << messageId << " value:" << isAlbumChild << "previously" << albumEntryFilter);
+ const bool oldAlbumFiltered = albumEntryFilter;
+ albumEntryFilter = isAlbumChild;
+ return (isAlbumChild == oldAlbumFiltered) ? 0 : RoleFlagMessageAlbumEntryFilter;
+}
+
+
+QVector ChatModel::MessageData::setAlbumEntryFilter(bool isAlbumChild)
+{
+ LOG("setAlbumEntryFilter");
+ return flagsToRoles(updateAlbumEntryFilter(isAlbumChild));
+}
+
+uint ChatModel::MessageData::updateAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds)
+{
+ LOG("Updating albumMessageIds... id" << messageId);
+ LOG(" Updating albumMessageIds..." << newAlbumMessageIds << "previously" << albumMessageIds << "same?" << (newAlbumMessageIds == albumMessageIds));
+ const QVariantList oldAlbumMessageIds = albumMessageIds;
+ albumMessageIds = newAlbumMessageIds;
+
+ LOG(" Updating albumMessageIds... same again?" << (newAlbumMessageIds == oldAlbumMessageIds));
+ return (newAlbumMessageIds == oldAlbumMessageIds) ? 0 : RoleFlagMessageAlbumMessageIds;
+}
+
+QVector ChatModel::MessageData::setAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds)
+{
+ return flagsToRoles(updateAlbumEntryMessageIds(newAlbumMessageIds));
+}
+
QVector ChatModel::MessageData::setInteractionInfo(const QVariantMap &info)
{
return flagsToRoles(updateInteractionInfo(info));
@@ -295,6 +352,8 @@ QHash ChatModel::roleNames() const
roles.insert(MessageData::RoleMessageContentType, "content_type");
roles.insert(MessageData::RoleMessageViewCount, "view_count");
roles.insert(MessageData::RoleMessageReactions, "reactions");
+ roles.insert(MessageData::RoleMessageAlbumEntryFilter, "album_entry_filter");
+ roles.insert(MessageData::RoleMessageAlbumMessageIds, "album_message_ids");
return roles;
}
@@ -314,6 +373,8 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case MessageData::RoleMessageContentType: return message->messageContentType;
case MessageData::RoleMessageViewCount: return message->viewCount;
case MessageData::RoleMessageReactions: return message->reactions;
+ case MessageData::RoleMessageAlbumEntryFilter: return message->albumEntryFilter;
+ case MessageData::RoleMessageAlbumMessageIds: return message->albumMessageIds;
}
}
return QVariant();
@@ -331,6 +392,7 @@ void ChatModel::clear(bool contentOnly)
qDeleteAll(messages);
messages.clear();
messageIndexMap.clear();
+ albumMessageMap.clear();
endResetModel();
}
@@ -356,6 +418,7 @@ void ChatModel::initialize(const QVariantMap &chatInformation)
this->chatId = chatId;
this->messages.clear();
this->messageIndexMap.clear();
+ this->albumMessageMap.clear();
this->searchQuery.clear();
endResetModel();
emit chatIdChanged();
@@ -420,6 +483,36 @@ int ChatModel::getMessageIndex(qlonglong messageId)
return -1;
}
+QVariantList ChatModel::getMessageIdsForAlbum(qlonglong albumId)
+{
+ QVariantList foundMessages;
+ if(albumMessageMap.contains(albumId)) { // there should be only one in here
+ QHash< qlonglong, QVariantList >::iterator i = albumMessageMap.find(albumId);
+ return i.value();
+ }
+ return foundMessages;
+}
+
+QVariantList ChatModel::getMessagesForAlbum(qlonglong albumId, int startAt)
+{
+ LOG("getMessagesForAlbumId" << albumId);
+ QVariantList messageIds = getMessageIdsForAlbum(albumId);
+ int count = messageIds.size();
+ if ( count == 0) {
+ return messageIds;
+ }
+ QVariantList foundMessages;
+ for (int messageNum = startAt; messageNum < count; ++messageNum) {
+ const int position = messageIndexMap.value(messageIds.at(messageNum).toLongLong(), -1);
+ if(position >= 0 && position < messages.size()) {
+ foundMessages.append(messages.at(position)->messageData);
+ } else {
+ LOG("Not found in messages: #"<< messageNum);
+ }
+ }
+ return foundMessages;
+}
+
int ChatModel::getLastReadMessageIndex()
{
LOG("Obtaining last read message index");
@@ -477,7 +570,8 @@ void ChatModel::handleMessagesReceived(const QVariantList &messages, int totalCo
const qlonglong messageId = messageData.value(ID).toLongLong();
if (messageId && messageData.value(CHAT_ID).toLongLong() == chatId && !messageIndexMap.contains(messageId)) {
LOG("New message will be added:" << messageId);
- messagesToBeAdded.append(new MessageData(messageData, messageId));
+ MessageData* message = new MessageData(messageData, messageId);
+ messagesToBeAdded.append(message);
}
}
@@ -485,6 +579,7 @@ void ChatModel::handleMessagesReceived(const QVariantList &messages, int totalCo
if (!messagesToBeAdded.isEmpty()) {
insertMessages(messagesToBeAdded);
+ setMessagesAlbum(messagesToBeAdded);
}
// First call only returns a few messages, we need to get a little more than that...
@@ -540,6 +635,7 @@ void ChatModel::handleNewMessageReceived(qlonglong chatId, const QVariantMap &me
QList messagesToBeAdded;
messagesToBeAdded.append(new MessageData(message, messageId));
insertMessages(messagesToBeAdded);
+ setMessagesAlbum(messagesToBeAdded);
emit newMessageReceived(message);
} else {
LOG("New message in this chat, but not relevant as less recent messages need to be loaded first!");
@@ -591,6 +687,7 @@ void ChatModel::handleMessageSendSucceeded(qlonglong messageId, qlonglong oldMes
messages.replace(pos, newMessage);
messageIndexMap.remove(oldMessageId);
messageIndexMap.insert(messageId, pos);
+ // TODO when we support sending album messages, handle ID change in albumMessageMap
const QVector changedRoles(newMessage->diff(oldMessage));
delete oldMessage;
LOG("Message was replaced at index" << pos);
@@ -635,7 +732,8 @@ void ChatModel::handleMessageContentUpdated(qlonglong chatId, qlonglong messageI
LOG("We know the message that was updated" << messageId);
const int pos = messageIndexMap.value(messageId, -1);
if (pos >= 0) {
- const QVector changedRoles(messages.at(pos)->setContent(newContent));
+ MessageData* messageData = messages.at(pos);
+ const QVector changedRoles(messageData->setContent(newContent));
LOG("Message was updated at index" << pos);
const QModelIndex messageIndex(index(pos));
emit dataChanged(messageIndex, messageIndex, changedRoles);
@@ -664,7 +762,8 @@ void ChatModel::handleMessageEditedUpdated(qlonglong chatId, qlonglong messageId
LOG("We know the message that was updated" << messageId);
const int pos = messageIndexMap.value(messageId, -1);
if (pos >= 0) {
- const QVector changedRoles(messages.at(pos)->setReplyMarkup(replyMarkup));
+ MessageData* messageData = messages.at(pos);
+ const QVector changedRoles(messageData->setReplyMarkup(replyMarkup));
LOG("Message was edited at index" << pos);
const QModelIndex messageIndex(index(pos));
emit dataChanged(messageIndex, messageIndex, changedRoles);
@@ -709,18 +808,31 @@ void ChatModel::handleMessagesDeleted(qlonglong chatId, const QList &
}
}
+
void ChatModel::removeRange(int firstDeleted, int lastDeleted)
{
if (firstDeleted >= 0 && firstDeleted <= lastDeleted) {
LOG("Removing range" << firstDeleted << "..." << lastDeleted << "| current messages size" << messages.size());
beginRemoveRows(QModelIndex(), firstDeleted, lastDeleted);
+ QList rescanAlbumIds;
for (int i = firstDeleted; i <= lastDeleted; i++) {
MessageData *message = messages.at(i);
messageIndexMap.remove(message->messageId);
+
+ qlonglong albumId = message->messageData.value(MEDIA_ALBUM_ID).toLongLong();
+ if(albumId != 0 && albumMessageMap.contains(albumId)) {
+ rescanAlbumIds.append(albumId);
+ }
delete message;
}
messages.erase(messages.begin() + firstDeleted, messages.begin() + (lastDeleted + 1));
+ // rebuild following messageIndexMap
+ for(int i = firstDeleted; i < messages.size(); ++i) {
+ messageIndexMap.insert(messages.at(i)->messageId, i);
+ }
endRemoveRows();
+
+ updateAlbumMessages(rescanAlbumIds, true);
}
}
@@ -757,7 +869,7 @@ void ChatModel::appendMessages(const QList newMessages)
beginInsertRows(QModelIndex(), oldSize, oldSize + count - 1);
messages.append(newMessages);
for (int i = 0; i < count; i++) {
- // Appens new indeces to the map
+ // Append new indices to the map
messageIndexMap.insert(newMessages.at(i)->messageId, oldSize + i);
}
endInsertRows();
@@ -785,6 +897,90 @@ void ChatModel::prependMessages(const QList newMessages)
endInsertRows();
}
+void ChatModel::updateAlbumMessages(qlonglong albumId, bool checkDeleted)
+{
+ if(albumMessageMap.contains(albumId)) {
+ const QVariantList empty;
+ QHash< qlonglong, QVariantList >::iterator album = albumMessageMap.find(albumId);
+ QVariantList messageIds = album.value();
+ std::sort(messageIds.begin(), messageIds.end());
+ int count;
+ // first: clear deleted messageIds:
+ if(checkDeleted) {
+ QVariantList::iterator it = messageIds.begin();
+ while (it != messageIds.end()) {
+ if (!messageIndexMap.contains(it->toLongLong())) {
+ it = messageIds.erase(it);
+ }
+ else {
+ ++it;
+ }
+ }
+ }
+ // second: remaining ones still exist
+ count = messageIds.size();
+ if(count == 0) {
+ albumMessageMap.remove(albumId);
+ } else {
+ for (int i = 0; i < count; i++) {
+ const int position = messageIndexMap.value(messageIds.at(i).toLongLong(), -1);
+ if(position > -1) {
+ // set list for first entry, empty for all others
+ QVector changedRolesFilter;
+ QVector changedRolesIds;
+
+ QModelIndex messageIndex(index(position));
+ if(i == 0) {
+ changedRolesFilter = messages.at(position)->setAlbumEntryFilter(false);
+ changedRolesIds = messages.at(position)->setAlbumEntryMessageIds(messageIds);
+ } else {
+ changedRolesFilter = messages.at(position)->setAlbumEntryFilter(true);
+ changedRolesIds = messages.at(position)->setAlbumEntryMessageIds(empty);
+ }
+ emit dataChanged(messageIndex, messageIndex, changedRolesIds);
+ emit dataChanged(messageIndex, messageIndex, changedRolesFilter);
+ }
+ }
+ }
+ albumMessageMap.insert(albumId, messageIds);
+ }
+}
+
+void ChatModel::updateAlbumMessages(QList albumIds, bool checkDeleted)
+{
+ const int albumsCount = albumIds.size();
+ for (int i = 0; i < albumsCount; i++) {
+ updateAlbumMessages(albumIds.at(i), checkDeleted);
+ }
+}
+
+void ChatModel::setMessagesAlbum(const QList newMessages)
+{
+ const int count = newMessages.size();
+ for (int i = 0; i < count; i++) {
+ setMessagesAlbum(newMessages.at(i));
+ }
+}
+
+void ChatModel::setMessagesAlbum(MessageData *message)
+{
+ qlonglong albumId = message->messageData.value(MEDIA_ALBUM_ID).toLongLong();
+ if (albumId > 0 && (message->messageContentType != "messagePhoto" || message->messageContentType != "messageVideo")) {
+ qlonglong messageId = message->messageId;
+
+ if(albumMessageMap.contains(albumId)) {
+ // find message id within album:
+ QHash< qlonglong, QVariantList >::iterator i = albumMessageMap.find(albumId);
+ if(!i.value().contains(messageId)) {
+ i.value().append(messageId);
+ }
+ } else { // new album id
+ albumMessageMap.insert(albumId, QVariantList() << messageId);
+ }
+ updateAlbumMessages(albumId, false);
+ }
+}
+
QVariantMap ChatModel::enhanceMessage(const QVariantMap &message)
{
QVariantMap enhancedMessage = message;
diff --git a/src/chatmodel.h b/src/chatmodel.h
index bbf1b4a..e2e8bbc 100644
--- a/src/chatmodel.h
+++ b/src/chatmodel.h
@@ -44,6 +44,8 @@ public:
Q_INVOKABLE void triggerLoadMoreFuture();
Q_INVOKABLE QVariantMap getChatInformation();
Q_INVOKABLE QVariantMap getMessage(int index);
+ Q_INVOKABLE QVariantList getMessageIdsForAlbum(qlonglong albumId);
+ Q_INVOKABLE QVariantList getMessagesForAlbum(qlonglong albumId, int startAt);
Q_INVOKABLE int getLastReadMessageIndex();
Q_INVOKABLE void setSearchQuery(const QString newSearchQuery);
@@ -85,6 +87,10 @@ private:
void insertMessages(const QList newMessages);
void appendMessages(const QList newMessages);
void prependMessages(const QList newMessages);
+ void updateAlbumMessages(qlonglong albumId, bool checkDeleted);
+ void updateAlbumMessages(QList albumIds, bool checkDeleted);
+ void setMessagesAlbum(const QList newMessages);
+ void setMessagesAlbum(MessageData *message);
QVariantMap enhanceMessage(const QVariantMap &message);
int calculateLastKnownMessageId();
int calculateLastReadSentMessageId();
@@ -95,6 +101,7 @@ private:
TDLibWrapper *tdLibWrapper;
QList messages;
QHash messageIndexMap;
+ QHash albumMessageMap;
QVariantMap chatInformation;
qlonglong chatId;
bool inReload;
diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp
index a584ea7..a6dfd66 100644
--- a/src/harbour-fernschreiber.cpp
+++ b/src/harbour-fernschreiber.cpp
@@ -51,6 +51,7 @@
#include "processlauncher.h"
#include "stickermanager.h"
#include "textfiltermodel.h"
+#include "boolfiltermodel.h"
#include "tgsplugin.h"
#include "fernschreiberutils.h"
#include "knownusersmodel.h"
@@ -130,6 +131,7 @@ int main(int argc, char *argv[])
qmlRegisterType(uri, 1, 0, "TDLibFile");
qmlRegisterType(uri, 1, 0, "NamedAction");
qmlRegisterType(uri, 1, 0, "TextFilterModel");
+ qmlRegisterType(uri, 1, 0, "BoolFilterModel");
qmlRegisterType(uri, 1, 0, "ChatPermissionFilterModel");
qmlRegisterSingletonType(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton);
diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp
index 7a95e2a..70dcf65 100644
--- a/src/tdlibwrapper.cpp
+++ b/src/tdlibwrapper.cpp
@@ -1464,9 +1464,8 @@ void TDLibWrapper::getPageSource(const QString &address)
connect(reply, SIGNAL(finished()), this, SLOT(handleGetPageSourceFinished()));
}
-void TDLibWrapper::setMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction)
+void TDLibWrapper::addMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction)
{
- LOG("Set message reaction" << chatId << messageId << reaction);
QVariantMap requestObject;
requestObject.insert(CHAT_ID, chatId);
requestObject.insert(MESSAGE_ID, messageId);
@@ -1481,9 +1480,35 @@ void TDLibWrapper::setMessageReaction(qlonglong chatId, qlonglong messageId, con
reactionType.insert(EMOJI, reaction);
requestObject.insert(REACTION_TYPE, reactionType);
requestObject.insert(_TYPE, "addMessageReaction");
+ LOG("Add message reaction" << chatId << messageId << reaction);
} else {
requestObject.insert("reaction", reaction);
requestObject.insert(_TYPE, "setMessageReaction");
+ LOG("Toggle message reaction" << chatId << messageId << reaction);
+ }
+ this->sendRequest(requestObject);
+}
+
+void TDLibWrapper::removeMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction)
+{
+ QVariantMap requestObject;
+ requestObject.insert(CHAT_ID, chatId);
+ requestObject.insert(MESSAGE_ID, messageId);
+ if (versionNumber > VERSION_NUMBER(1,8,5)) {
+ // "reaction_type": {
+ // "@type": "reactionTypeEmoji",
+ // "emoji": "..."
+ // }
+ QVariantMap reactionType;
+ reactionType.insert(_TYPE, REACTION_TYPE_EMOJI);
+ reactionType.insert(EMOJI, reaction);
+ requestObject.insert(REACTION_TYPE, reactionType);
+ requestObject.insert(_TYPE, "removeMessageReaction");
+ LOG("Remove message reaction" << chatId << messageId << reaction);
+ } else {
+ requestObject.insert("reaction", reaction);
+ requestObject.insert(_TYPE, "setMessageReaction");
+ LOG("Toggle message reaction" << chatId << messageId << reaction);
}
this->sendRequest(requestObject);
}
@@ -2087,6 +2112,7 @@ void TDLibWrapper::handleActiveEmojiReactionsUpdated(const QStringList& emojis)
if (activeEmojiReactions != emojis) {
activeEmojiReactions = emojis;
LOG(emojis.count() << "reaction(s) available");
+ emit reactionsUpdated();
}
}
diff --git a/src/tdlibwrapper.h b/src/tdlibwrapper.h
index b03ae10..2487ae5 100644
--- a/src/tdlibwrapper.h
+++ b/src/tdlibwrapper.h
@@ -249,7 +249,8 @@ public:
Q_INVOKABLE void terminateSession(const QString &sessionId);
Q_INVOKABLE void getMessageAvailableReactions(qlonglong chatId, qlonglong messageId);
Q_INVOKABLE void getPageSource(const QString &address);
- Q_INVOKABLE void setMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction);
+ Q_INVOKABLE void addMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction);
+ Q_INVOKABLE void removeMessageReaction(qlonglong chatId, qlonglong messageId, const QString &reaction);
Q_INVOKABLE void setNetworkType(NetworkType networkType);
Q_INVOKABLE void setInactiveSessionTtl(int days);
@@ -340,6 +341,7 @@ signals:
void chatUnreadMentionCountUpdated(qlonglong chatId, int unreadMentionCount);
void chatUnreadReactionCountUpdated(qlonglong chatId, int unreadReactionCount);
void tgUrlFound(const QString &tgUrl);
+ void reactionsUpdated();
public slots:
void handleVersionDetected(const QString &version);
diff --git a/translations/harbour-fernschreiber-de.ts b/translations/harbour-fernschreiber-de.ts
index ae2ff61..ef14fdf 100644
--- a/translations/harbour-fernschreiber-de.ts
+++ b/translations/harbour-fernschreiber-de.ts
@@ -896,6 +896,17 @@
hat eine Videonachricht geschickt
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ %Ln Nachricht weiterleiten
+ %Ln Nachrichten weiterleiten
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-en.ts b/translations/harbour-fernschreiber-en.ts
index d030fd2..4f9f525 100644
--- a/translations/harbour-fernschreiber-en.ts
+++ b/translations/harbour-fernschreiber-en.ts
@@ -898,6 +898,17 @@ messages
sent a video note
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Forward %Ln message
+ Forward %Ln messages
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-es.ts b/translations/harbour-fernschreiber-es.ts
index 6c1578d..0abb207 100644
--- a/translations/harbour-fernschreiber-es.ts
+++ b/translations/harbour-fernschreiber-es.ts
@@ -45,7 +45,7 @@
- Telegram
+ Telegrama
@@ -173,7 +173,7 @@
- Crear conversación secreta
+ Crear Charla Secreta
@@ -193,7 +193,7 @@
chats you have in common with a user
- Cargando conversaciones comunes…
+ Cargando charlas comunes…
@@ -274,11 +274,11 @@
- Desanclar conversación
+ Desanclar charla
- Anclar conversación
+ Anclar charla
@@ -339,7 +339,7 @@
- Esta conversación está vacía.
+ Esta charla está vacía.
@@ -351,7 +351,7 @@
- Saliendo de conversación
+ Saliendo de charla
@@ -405,15 +405,15 @@
- Esta conversación secreta no está lista. El contacto no está conectado.
+ Esta charla secreta no está lista. El contacto no está conectado.
- Cerrando la conversación
+ Cerrando charla
- Cerrar conversación
+ Cerrar charla
@@ -473,11 +473,11 @@
- Borrando la conversación
+ Borrando charla
- Borrar conversación
+ Borrar Charla
@@ -492,11 +492,11 @@
ChatSelectionPage
- Seleccionar conversación
+ Seleccionar Charla
- No hay conversaciones.
+ No hay charlas.
@@ -529,7 +529,7 @@
conversación
- conversaciones
+ charlas
@@ -570,7 +570,7 @@
member permission
- Cambiar detalles de conversación
+ Cambiar detalles de Charla
@@ -605,7 +605,7 @@
- Establecer cuánto tiempo debe esperar cada miembro de conversación entre mensajes
+ Establecer cuánto tiempo debe esperar cada miembro de charla entre Mensajes
@@ -662,7 +662,7 @@
myself
- registrado a Telegram
+ registrado a Telegrama
@@ -689,11 +689,11 @@
myself
- dejó esta conversación
+ dejó esta charla
- dejó esta conversación
+ dejó esta charla
@@ -748,7 +748,7 @@
myself
- cambió la foto de la conversación
+ cambió foto de charla
@@ -757,7 +757,7 @@
myself
- borró foto de conversación
+ borró foto de charla
@@ -766,11 +766,11 @@
myself
- cambió ajustes TTL en conversación secreta
+ cambió ajustes TTL en charla secreta
- cambió ajustes TTL en conversación secreta
+ cambió ajustes TTL en charla secreta
@@ -789,11 +789,11 @@
myself
- creó pantallazo a esta conversación
+ creó pantallazo a esta charla
- creó pantallazo a esta conversación
+ creó pantallazo a esta charla
@@ -846,21 +846,21 @@
- ha añadido %1 a conversación
+ ha añadido %1 a charla
- ha quitado %1 de conversación
+ ha quitado %1 de charla
myself
- ha añadido %1 a conversación
+ ha añadido %1 a charla
myself
- ha quitado %1 de conversación
+ ha quitado %1 de charla
@@ -896,6 +896,17 @@
envió nota de video
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Reenviar %Ln mensaje
+ Reenviar %Ln mensajes
+
+
+
ImagePage
@@ -1204,7 +1215,7 @@
- cargando lista de conversación...
+ cargando lista de charla...
@@ -1212,11 +1223,11 @@
- No hay conversaciones.
+ No hay charlas.
- Crear conversación
+ Crear Charla
@@ -1482,7 +1493,7 @@
- Mostrar pegatinas como emoticonos
+ Mostrar pegatinas como Emoticonos
@@ -1490,7 +1501,7 @@
- Mostrar pegatinas como imágenes
+ Mostrar pegatinas como Imágenes
@@ -1498,7 +1509,7 @@
- Mostrar pegatinas animadas
+ Mostrar pegatinas Animadas
@@ -1517,15 +1528,15 @@
- Enfocar entrada de texto a conversación
+ Enfocar entrada de texto de Charla
- Enfoca área de entrada de texto al ingresar a conversación
+ Enfoca área de entrada de texto al ingresar a charla
- Enfocar área de entrada de texto
+ Enfocar área de entrada de Texto
@@ -1533,11 +1544,11 @@
- Marcar mensajes como leídos
+ Marcar mensajes como Leídos
- Si esta habilitado, apl espera un segundo hasta que mensaje que está en pantalla se marque como leído. Si deshabilita esta función, mensajes se marcarán inmediatamente como leído una vez que esté en pantalla sin desplazarse a mensaje
+ Si esta habilitado, Apl espera un segundo hasta que mensaje que está en monitor se marque como leído. Si deshabilita esta función, mensajes se marcarán inmediatamente como leído una vez que esté en monitor sin desplazarse a mensaje
@@ -1569,15 +1580,43 @@
- Mostrar notificación por pantalla
+ Mostrar notificación por Monitor
- Habilitar sonidos notificación
+ Habilitar sonidos de Notificación
- Cuando sonidos están habilitados, Ferni utilizará sonido de notificación actual de Sailfish OS para los grupos, que se puede ajustar a configuración del sistema.
+ Cuando los sonidos están habilitados, Ferni utilizará sonido de notificación actual de Sailfish OS para los grupos, que se puede ajustar a configuración del sistema.
+
+
+
+ Vista previa de mensaje en Notificaciones
+
+
+
+ Mostrará cantidad mensajes no leídos, el último mensaje se agregará a notificaciones.
+
+
+
+ Resaltar mensajes no Leídos
+
+
+
+ Resalta la charla en mensajes no leídos
+
+
+
+ Ocultar contenido de notificaciones
+
+
+
+ Ir a mensaje citado
+
+
+
+ Al Pulsar mensaje citado, abrirá en Charla en lugar de mostrarlo en una superposición.
@@ -1599,14 +1638,6 @@
Ocultar contenido a notificaciones
-
-
- Ir a mensaje citado
-
-
-
- Al Pulsar mensaje citado, abrirá en Charla en lugar de mostrarlo en una superposición.
-
SettingsPage
@@ -1623,7 +1654,7 @@
- Permitir invitaciones de grupo
+ Permitir invitaciones de Grupo
@@ -1643,7 +1674,7 @@
- Permitir buscarme por número
+ Permitir buscarme por Número
@@ -1651,7 +1682,7 @@
- Mostrar enlace a mensajes reenviados
+ Mostrar enlace a mensajes Reenviados
@@ -1659,7 +1690,7 @@
- Mostrar número telefónico
+ Mostrar número Telefónico
@@ -1667,7 +1698,7 @@
- Mostrar foto de perfil
+ Mostrar foto de Perfil
@@ -1675,7 +1706,7 @@
- Mostrar estado
+ Mostrar Estado
@@ -1683,7 +1714,7 @@
- Enviar ubicación de Robot enlínea
+ Enviar ubicación de Robot Enlínea
@@ -1760,15 +1791,15 @@
- Modo solo enlínea
+ Modo solo Enlínea
- Deshabilita almacenamiento en caché sin conexión. Algunas funciones pueden estar limitadas o ausentes en este modo. Se requiere reiniciar Ferni para efecto.
+ Deshabilita el almacenamiento en caché sin conexión. Algunas funciones pueden estar limitadas o ausentes en este modo. Se requiere reiniciar Ferni para efecto.
- Optimizador de almacenamiento
+ Optimizar Almacenamiento
@@ -1838,7 +1869,7 @@
- Sincronizar Telegram
+ Sincronizar Telegrama
@@ -2219,12 +2250,12 @@
myself; TTL = Time To Live
- cambió ajustes de TTL de conversación secreta
+ cambió ajustes de TTL de charla secreta
TTL = Time To Live
- cambió ajustes de TTL de conversación secreta
+ cambió ajustes de TTL de charla secreta
@@ -2243,11 +2274,11 @@
myself
- creó pantallazo a conversación
+ creó pantallazo de charla
- creó pantallazo a conversación
+ creó pantallazo a charla
diff --git a/translations/harbour-fernschreiber-fi.ts b/translations/harbour-fernschreiber-fi.ts
index d11cb99..691fae8 100644
--- a/translations/harbour-fernschreiber-fi.ts
+++ b/translations/harbour-fernschreiber-fi.ts
@@ -897,6 +897,17 @@
lähetti videoviestin
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Välitä %Ln viesti
+ Välitä %Ln viestiä
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-fr.ts b/translations/harbour-fernschreiber-fr.ts
index 2c1ad8d..062a5c0 100644
--- a/translations/harbour-fernschreiber-fr.ts
+++ b/translations/harbour-fernschreiber-fr.ts
@@ -896,6 +896,17 @@
a envoyé une note vidéo
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Transférer %Ln message
+ Transférer %Ln messages
+
+
+
ImagePage
@@ -1607,6 +1618,18 @@
+
+
+ Mettre en valeur les messages non-lus
+
+
+
+ Mettre en valeur les conversations avec des messages non-lus
+
+
+
+ Masquer le contenu dans les notifications
+
SettingsPage
diff --git a/translations/harbour-fernschreiber-hu.ts b/translations/harbour-fernschreiber-hu.ts
index ccf965e..60dbe5a 100644
--- a/translations/harbour-fernschreiber-hu.ts
+++ b/translations/harbour-fernschreiber-hu.ts
@@ -882,6 +882,16 @@
+
+ FullscreenOverlay
+
+
+ dialog header
+
+
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-it.ts b/translations/harbour-fernschreiber-it.ts
index 5303829..67d1bdf 100644
--- a/translations/harbour-fernschreiber-it.ts
+++ b/translations/harbour-fernschreiber-it.ts
@@ -896,6 +896,17 @@
ha inviato un videomessaggio
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Inoltra %Ln messaggio
+ Inoltra %Ln messaggi
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-pl.ts b/translations/harbour-fernschreiber-pl.ts
index 040f2bc..261f37d 100644
--- a/translations/harbour-fernschreiber-pl.ts
+++ b/translations/harbour-fernschreiber-pl.ts
@@ -910,6 +910,18 @@
wysłał notatkę video
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Przekaż %Ln wiadomość
+ Przekaż %Ln wiadomości
+ Przekaż %Ln wiadomości
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber-ru.ts b/translations/harbour-fernschreiber-ru.ts
index 4e7d338..b6dcd84 100644
--- a/translations/harbour-fernschreiber-ru.ts
+++ b/translations/harbour-fernschreiber-ru.ts
@@ -913,6 +913,18 @@
отправил(а) видео заметку
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Перенаправить %Ln сообщение
+ Перенаправить %Ln сообщения
+ Перенаправить %Ln сообщений
+
+
+
ImagePage
@@ -1637,6 +1649,18 @@
По нажатию на цитируемое сообщение, переходить к нему в чате вместо отображения во всплывающем окне.
+
+
+ Выделять непрочитанные сообщения
+
+
+
+ Помечать чаты и каналы с непрочитанными сообщениями другим шрифтом и цветом.
+
+
+
+ Не показывать содержимое сообщений в уведомлениях
+
SettingsPage
diff --git a/translations/harbour-fernschreiber-sk.ts b/translations/harbour-fernschreiber-sk.ts
index e8b9dce..36ab490 100644
--- a/translations/harbour-fernschreiber-sk.ts
+++ b/translations/harbour-fernschreiber-sk.ts
@@ -495,7 +495,7 @@
-
+ Dvojitým klepnutím na správu vybrať reakciu
@@ -910,6 +910,18 @@
poslal video-poznámku
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Postúpená %Ln správa
+ Postúpené %Ln správy
+ Postúpených %Ln správ
+
+
+
ImagePage
@@ -1580,7 +1592,7 @@
- Pre upozornenia použiť negrafickú reakciu (zvuk, vibrovanie)
+ Pre oznamy použiť negrafickú reakciu (zvuk, vibrovanie)
@@ -1600,39 +1612,39 @@
- Povoliť zvukové upozornenia
+ Povoliť zvukové oznamy
- Keď sú povolené zvukové upozornenia, Fernschreiber použije aktuálne zvukové upozornenia Sailfish OS pre čety, ktoré môžu byť upravené v nastaveniach systému.
+ Keď sú povolené zvukové oznamy, Fernschreiber použije aktuálne zvukové oznamy Sailfish OS pre čety, ktoré môžu byť upravené v nastaveniach systému.
-
+ K upozorneniam vždy pripojiť ukážku správy
-
+ Okrem zobrazenia počtu neprečítaných správ pripojiť k upozorneniam aj najnovšiu správu.
-
+ Zvýrazniť neprečítané správy
-
+ Zvýrazniť konverzácie s neprečítanými správami
-
+ V upozorneniach skryť obsah
-
+ Prejsť na citovanú správu
-
+ Citovanú správu otvoriť v čete namiesto v náhľade.
@@ -1745,39 +1757,39 @@
-
-
-
-
+
+ %1 deň
+ %1 dni
+ %1 dní
-
+ 1 týždeň
-
+ 1 mesiac
-
+ 3 mesiace
-
+ 6 mesiacov
-
+ 1 rok
-
+ Časový limit relácie
-
+ Neaktívne relácie budú po tomto časovom rámci ukončené
diff --git a/translations/harbour-fernschreiber-sv.ts b/translations/harbour-fernschreiber-sv.ts
index dc5ce5e..16953c3 100644
--- a/translations/harbour-fernschreiber-sv.ts
+++ b/translations/harbour-fernschreiber-sv.ts
@@ -185,7 +185,7 @@
-
+ ID har kopierats till urklipp.
@@ -485,7 +485,7 @@
-
+ Dubbeltryck på ett meddelande för att välja en reaktion
@@ -896,6 +896,17 @@
skickade ett videomeddelande
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Vidarebefordra %Ln meddelande
+ Vidarebefordra %Ln meddelanden
+
+
+
ImagePage
@@ -1163,7 +1174,7 @@
-
+ Inga kontakter hittades.
@@ -1581,31 +1592,31 @@
-
+ Visa alltid förhandsgranskning av meddelanden i aviseringar
-
+ Förutom att visa antalet olästa meddelanden kommer det senaste meddelandet också att visas i aviseringarna.
-
+ Färgmarkera olästa meddelanden
-
+ Färgmarkera konversationer med olästa meddelanden
-
+ Dölj innehåll i aviseringar
-
+ Gå till citerat meddelande
-
+ Vid tryck på ett citerat meddelande öppnas det i chatten istället för att visas i ett överlägg.
@@ -1718,38 +1729,38 @@
-
-
-
+
+ %1 dag
+ %1 dagar
-
+ 1 vecka
-
+ 1 månad
-
+ 3 månader
-
+ 6 månader
-
+ 1 år
-
+ Tidsgräns för session
-
+ Inaktiva sessioner avslutas efter den här tidsramen
diff --git a/translations/harbour-fernschreiber-zh_CN.ts b/translations/harbour-fernschreiber-zh_CN.ts
index 7887ddb..38d819c 100644
--- a/translations/harbour-fernschreiber-zh_CN.ts
+++ b/translations/harbour-fernschreiber-zh_CN.ts
@@ -883,6 +883,16 @@
发送视频消息
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ 转发 %Ln 则消息
+
+
+
ImagePage
diff --git a/translations/harbour-fernschreiber.ts b/translations/harbour-fernschreiber.ts
index 2481b12..134628e 100644
--- a/translations/harbour-fernschreiber.ts
+++ b/translations/harbour-fernschreiber.ts
@@ -896,6 +896,17 @@
sent a video note
+
+ FullscreenOverlay
+
+
+ dialog header
+
+ Forward %Ln message
+ Forward %Ln messages
+
+
+
ImagePage