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 a87decc..683c0e2 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,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")
@@ -463,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
@@ -646,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 {
@@ -671,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);
}
}
@@ -684,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);
}
}
}
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 ea5e25d..d20a373 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();
}
@@ -668,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...");
@@ -747,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
}
@@ -1222,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() {
@@ -1245,6 +1260,9 @@ Page {
positionViewAtIndex(index, (mode === undefined) ? ListView.Contain : mode)
if(index === chatView.count - 1) {
manuallyScrolledToBottom = true;
+ if(!chatView.atYEnd) {
+ chatView.positionViewAtEnd();
+ }
}
}
}
@@ -1277,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
@@ -1310,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;
@@ -1326,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));
@@ -1334,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
@@ -1389,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: {
@@ -1413,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/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/translations/harbour-fernschreiber-es.ts b/translations/harbour-fernschreiber-es.ts
index cdd1e62..f12be43 100644
--- a/translations/harbour-fernschreiber-es.ts
+++ b/translations/harbour-fernschreiber-es.ts
@@ -499,13 +499,6 @@
No hay charlas.
-
- ContactSync
-
-
- No se puede sincronizar los contactos con Telegrama.
-
-
CoverPage
diff --git a/translations/harbour-fernschreiber-sk.ts b/translations/harbour-fernschreiber-sk.ts
index 8bc17ec..a794119 100644
--- a/translations/harbour-fernschreiber-sk.ts
+++ b/translations/harbour-fernschreiber-sk.ts
@@ -1634,34 +1634,6 @@
Citovanú správu otvoriť v čete namiesto v náhľade.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
SettingsPage