From 80f76c8eb8022b11b0fcd2453610cbd043b4fb6d Mon Sep 17 00:00:00 2001 From: jgibbon Date: Thu, 28 Dec 2023 13:03:13 +0100 Subject: [PATCH] fix range + message updates; implement album filter --- harbour-fernschreiber.pro | 10 + qml/components/MessageListViewItem.qml | 47 ++- qml/components/TDLibMinithumbnail.qml | 15 +- qml/components/TDLibThumbnail.qml | 4 +- .../messageContent/MessageContentBase.qml | 1 - .../messageContent/MessagePhoto.qml | 36 +-- .../messageContent/MessagePhotoAlbum.qml | 207 +++++++++++++ .../messageContent/MessageVideo.qml | 7 +- .../messageContent/MessageVideoAlbum.qml | 19 ++ .../mediaAlbumPage/FullscreenOverlay.qml | 279 ++++++++++++++++++ .../mediaAlbumPage/PhotoComponent.qml | 16 + .../mediaAlbumPage/VideoComponent.qml | 181 ++++++++++++ .../mediaAlbumPage/ZoomArea.qml | 148 ++++++++++ .../mediaAlbumPage/ZoomImage.qml | 127 ++++++++ qml/pages/ChatPage.qml | 76 ++++- qml/pages/MediaAlbumPage.qml | 109 +++++++ src/boolfiltermodel.cpp | 153 ++++++++++ src/boolfiltermodel.h | 63 ++++ src/chatmodel.cpp | 210 ++++++++++++- src/chatmodel.h | 7 + src/harbour-fernschreiber.cpp | 2 + 21 files changed, 1649 insertions(+), 68 deletions(-) create mode 100644 qml/components/messageContent/MessagePhotoAlbum.qml create mode 100644 qml/components/messageContent/MessageVideoAlbum.qml create mode 100644 qml/components/messageContent/mediaAlbumPage/FullscreenOverlay.qml create mode 100644 qml/components/messageContent/mediaAlbumPage/PhotoComponent.qml create mode 100644 qml/components/messageContent/mediaAlbumPage/VideoComponent.qml create mode 100644 qml/components/messageContent/mediaAlbumPage/ZoomArea.qml create mode 100644 qml/components/messageContent/mediaAlbumPage/ZoomImage.qml create mode 100644 qml/pages/MediaAlbumPage.qml create mode 100644 src/boolfiltermodel.cpp create mode 100644 src/boolfiltermodel.h diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 79af618..a16ad98 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 \ @@ -212,6 +221,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 a0cf5d8..5b597f6 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() @@ -268,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); } } @@ -302,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; @@ -323,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) @@ -376,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 }) @@ -441,7 +451,7 @@ 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 ? (isUnread ? Theme.secondaryHighlightColor : Theme.secondaryColor) : (isUnread ? Theme.backgroundGlowColor : Theme.overlayBackgroundColor) radius: parent.width / 50 opacity: isUnread ? 0.5 : 0.2 @@ -463,7 +473,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 @@ -646,7 +662,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 { @@ -671,7 +688,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); } } @@ -684,13 +701,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); } } } 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/pages/ChatPage.qml b/qml/pages/ChatPage.qml index 1808d58..55278fe 100644 --- a/qml/pages/ChatPage.qml +++ b/qml/pages/ChatPage.qml @@ -609,7 +609,8 @@ Page { 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; @@ -623,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(); } @@ -669,10 +670,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..."); @@ -748,14 +752,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 } @@ -1223,7 +1239,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() { @@ -1246,6 +1261,9 @@ Page { positionViewAtIndex(index, (mode === undefined) ? ListView.Contain : mode) if(index === chatView.count - 1) { manuallyScrolledToBottom = true; + if(!chatView.atYEnd) { + chatView.positionViewAtEnd(); + } } } } @@ -1278,7 +1296,13 @@ Page { } } - model: chatModel + BoolFilterModel { + id: chatProxyModel + sourceModel: chatModel + filterRoleName: "album_entry_filter" + filterValue: false + } + model: chatProxyModel header: Component { Loader { active: !!chatPage.botInformation @@ -1311,7 +1335,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; @@ -1327,6 +1352,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)); @@ -1335,6 +1364,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 @@ -1390,10 +1423,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: { @@ -1414,9 +1448,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/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);