Добавлена поддержка галереии в постах. #16

Open
sprainbrains wants to merge 77 commits from sprainbrains/harbour-fernschreiber:media_gallery into master
23 changed files with 1651 additions and 103 deletions
Showing only changes of commit 42e59badae - Show all commits

View file

@ -22,6 +22,7 @@ DEFINES += QT_STATICPLUGIN
SOURCES += src/harbour-fernschreiber.cpp \ SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \ src/appsettings.cpp \
src/boolfiltermodel.cpp \
src/chatpermissionfiltermodel.cpp \ src/chatpermissionfiltermodel.cpp \
src/chatlistmodel.cpp \ src/chatlistmodel.cpp \
src/chatmodel.cpp \ src/chatmodel.cpp \
@ -105,14 +106,21 @@ DISTFILES += qml/harbour-fernschreiber.qml \
qml/components/messageContent/MessageGame.qml \ qml/components/messageContent/MessageGame.qml \
qml/components/messageContent/MessageLocation.qml \ qml/components/messageContent/MessageLocation.qml \
qml/components/messageContent/MessagePhoto.qml \ qml/components/messageContent/MessagePhoto.qml \
qml/components/messageContent/MessagePhotoAlbum.qml \
qml/components/messageContent/MessagePoll.qml \ qml/components/messageContent/MessagePoll.qml \
qml/components/messageContent/MessageSticker.qml \ qml/components/messageContent/MessageSticker.qml \
qml/components/messageContent/MessageVenue.qml \ qml/components/messageContent/MessageVenue.qml \
qml/components/messageContent/MessageVideoAlbum.qml \
qml/components/messageContent/MessageVideoNote.qml \ qml/components/messageContent/MessageVideoNote.qml \
qml/components/messageContent/MessageVideo.qml \ qml/components/messageContent/MessageVideo.qml \
qml/components/messageContent/MessageVoiceNote.qml \ qml/components/messageContent/MessageVoiceNote.qml \
qml/components/messageContent/SponsoredMessage.qml \ qml/components/messageContent/SponsoredMessage.qml \
qml/components/messageContent/WebPagePreview.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/Accordion.qml \
qml/components/settingsPage/AccordionItem.qml \ qml/components/settingsPage/AccordionItem.qml \
qml/components/settingsPage/ResponsiveGrid.qml \ qml/components/settingsPage/ResponsiveGrid.qml \
@ -130,6 +138,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \
qml/pages/CoverPage.qml \ qml/pages/CoverPage.qml \
qml/pages/DebugPage.qml \ qml/pages/DebugPage.qml \
qml/pages/InitializationPage.qml \ qml/pages/InitializationPage.qml \
qml/pages/MediaAlbumPage.qml \
qml/pages/NewChatPage.qml \ qml/pages/NewChatPage.qml \
qml/pages/OverviewPage.qml \ qml/pages/OverviewPage.qml \
qml/pages/AboutPage.qml \ qml/pages/AboutPage.qml \
@ -211,6 +220,7 @@ INSTALLS += telegram 86.png 108.png 128.png 172.png 256.png \
HEADERS += \ HEADERS += \
src/appsettings.h \ src/appsettings.h \
src/boolfiltermodel.h \
src/chatpermissionfiltermodel.h \ src/chatpermissionfiltermodel.h \
src/chatlistmodel.h \ src/chatlistmodel.h \
src/chatmodel.h \ src/chatmodel.h \

View file

@ -32,6 +32,7 @@ ListItem {
property int messageIndex property int messageIndex
property int messageViewCount property int messageViewCount
property var myMessage property var myMessage
property var messageAlbumMessageIds
property var reactions property var reactions
property bool canReplyToMessage property bool canReplyToMessage
readonly property bool isAnonymous: myMessage.sender_id["@type"] === "messageSenderChat" readonly property bool isAnonymous: myMessage.sender_id["@type"] === "messageSenderChat"
@ -68,7 +69,7 @@ ListItem {
property var chatReactions property var chatReactions
property var messageReactions property var messageReactions
highlighted: (down || isSelected || additionalOptionsOpened || wasNavigatedTo) && !menuOpen highlighted: (down || (isSelected && messageAlbumMessageIds.length === 0) || additionalOptionsOpened || wasNavigatedTo) && !menuOpen
openMenuOnPressAndHold: !messageListItem.precalculatedValues.pageIsSelecting openMenuOnPressAndHold: !messageListItem.precalculatedValues.pageIsSelecting
signal replyToMessage() signal replyToMessage()
@ -268,20 +269,20 @@ ListItem {
Connections { Connections {
target: chatModel target: chatModel
onMessagesReceived: { onMessagesReceived: {
messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"; messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
} }
onMessagesIncrementalUpdate: { onMessagesIncrementalUpdate: {
messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"; messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
} }
onNewMessageReceived: { onNewMessageReceived: {
messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"; messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
} }
onUnreadCountUpdated: { onUnreadCountUpdated: {
messageBackground.isUnread = index > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage"; messageBackground.isUnread = messageIndex > chatModel.getLastReadMessageIndex() && myMessage['@type'] !== "sponsoredMessage";
} }
onLastReadSentMessageUpdated: { onLastReadSentMessageUpdated: {
Debug.log("[ChatModel] Messages in this chat were read, new last read: ", lastReadSentIndex, ", updating description for index ", index, ", status: ", (index <= lastReadSentIndex)); 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, index, lastReadSentIndex, messageDateText.useElapsed); messageDateText.text = getMessageStatusText(myMessage, messageIndex, lastReadSentIndex, messageDateText.useElapsed);
} }
} }
@ -302,7 +303,7 @@ ListItem {
pageStack.currentPage === chatPage) { pageStack.currentPage === chatPage) {
Debug.log("Available reactions for this message: " + reactions); Debug.log("Available reactions for this message: " + reactions);
messageListItem.messageReactions = reactions; messageListItem.messageReactions = reactions;
showItemCompletelyTimer.requestedIndex = index; showItemCompletelyTimer.requestedIndex = messageIndex;
showItemCompletelyTimer.start(); showItemCompletelyTimer.start();
} else { } else {
messageListItem.messageReactions = null; messageListItem.messageReactions = null;
@ -323,6 +324,13 @@ ListItem {
interval: 200 interval: 200
triggeredOnStart: false triggeredOnStart: false
onTriggered: { 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) Debug.log("Show item completely timer triggered, requested index: " + requestedIndex + ", current index: " + index)
if (requestedIndex === index) { if (requestedIndex === index) {
var p = chatView.contentItem.mapFromItem(reactionsColumn, 0, 0) var p = chatView.contentItem.mapFromItem(reactionsColumn, 0, 0)
@ -376,8 +384,10 @@ ListItem {
onTriggered: { onTriggered: {
if (messageListItem.hasContentComponent) { if (messageListItem.hasContentComponent) {
var type = myMessage.content["@type"]; 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( 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 messageListItem: messageListItem
}) })
@ -441,8 +451,10 @@ ListItem {
} }
height: messageTextColumn.height + precalculatedValues.paddingMediumDouble height: messageTextColumn.height + precalculatedValues.paddingMediumDouble
width: precalculatedValues.backgroundWidth 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)) 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 radius: parent.width / 50
opacity: isUnread ? 0.5 : 0.2 opacity: isUnread ? 0.5 : 0.2
visible: appSettings.showStickersAsImages || (myMessage.content['@type'] !== "messageSticker" && myMessage.content['@type'] !== "messageAnimatedEmoji") visible: appSettings.showStickersAsImages || (myMessage.content['@type'] !== "messageSticker" && myMessage.content['@type'] !== "messageAnimatedEmoji")
@ -463,7 +475,13 @@ ListItem {
id: userText id: userText
width: parent.width 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.pixelSize: Theme.fontSizeExtraSmall
font.weight: Font.ExtraBold font.weight: Font.ExtraBold
color: messageListItem.textColor color: messageListItem.textColor
@ -646,7 +664,8 @@ ListItem {
id: extraContentLoader id: extraContentLoader
width: parent.width * getContentWidthMultiplier() width: parent.width * getContentWidthMultiplier()
asynchronous: true 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 { Binding {
@ -671,7 +690,7 @@ ListItem {
running: true running: true
repeat: true repeat: true
onTriggered: { 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 font.pixelSize: Theme.fontSizeTiny
color: messageListItem.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor color: messageListItem.isOwnMessage ? Theme.secondaryHighlightColor : Theme.secondaryColor
horizontalAlignment: messageListItem.textAlign horizontalAlignment: messageListItem.textAlign
text: getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed) text: getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed)
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: !messageListItem.precalculatedValues.pageIsSelecting enabled: !messageListItem.precalculatedValues.pageIsSelecting
onClicked: { onClicked: {
messageDateText.useElapsed = !messageDateText.useElapsed; messageDateText.useElapsed = !messageDateText.useElapsed;
messageDateText.text = getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed); messageDateText.text = getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed);
} }
} }
} }

View file

@ -24,6 +24,7 @@ Loader {
id: loader id: loader
property var minithumbnail property var minithumbnail
property bool highlighted property bool highlighted
property int fillMode: tdLibImage.fillMode
anchors.fill: parent anchors.fill: parent
active: !!minithumbnail active: !!minithumbnail
sourceComponent: Component { sourceComponent: Component {
@ -32,7 +33,7 @@ Loader {
id: minithumbnailImage id: minithumbnailImage
anchors.fill: parent anchors.fill: parent
source: "data:image/jpg;base64,"+minithumbnail.data source: "data:image/jpg;base64,"+minithumbnail.data
fillMode: tdLibImage.fillMode fillMode: loader.fillMode
opacity: status === Image.Ready ? 1.0 : 0.0 opacity: status === Image.Ready ? 1.0 : 0.0
cache: false cache: false
visible: opacity > 0 visible: opacity > 0
@ -43,12 +44,12 @@ Loader {
effect: PressEffect { source: minithumbnailImage } effect: PressEffect { source: minithumbnailImage }
} }
} }
// this had a visible impact on performance
FastBlur { // FastBlur {
anchors.fill: parent // anchors.fill: parent
source: minithumbnailImage // source: minithumbnailImage
radius: Theme.paddingLarge // radius: Theme.paddingLarge
} // }
} }
} }
} }

View file

@ -59,7 +59,7 @@ Item {
readonly property bool hasVisibleThumbnail: thumbnailImage.opacity !== 1.0 readonly property bool hasVisibleThumbnail: thumbnailImage.opacity !== 1.0
&& !(videoThumbnailLoader.item && videoThumbnailLoader.item.opacity === 1.0) && !(videoThumbnailLoader.item && videoThumbnailLoader.item.opacity === 1.0)
property alias fillMode: thumbnailImage.fillMode
layer { layer {
enabled: highlighted enabled: highlighted
effect: PressEffect { source: tdlibThumbnail } effect: PressEffect { source: tdlibThumbnail }
@ -67,6 +67,7 @@ Item {
TDLibMinithumbnail { TDLibMinithumbnail {
id: minithumbnailLoader id: minithumbnailLoader
fillMode: thumbnailImage.fillMode
active: !!minithumbnail && thumbnailImage.opacity < 1.0 active: !!minithumbnail && thumbnailImage.opacity < 1.0
} }
BackgroundImage { BackgroundImage {
@ -103,6 +104,7 @@ Item {
sourceSize.width: width sourceSize.width: width
sourceSize.height: height sourceSize.height: height
mimeType: tdlibThumbnail.videoMimeType mimeType: tdlibThumbnail.videoMimeType
fillMode: thumbnailImage.fillMode == Image.PreserveAspectFit ? Thumbnail.PreserveAspectFit : Thumbnail.PreserveAspectCrop
visible: opacity > 0 visible: opacity > 0
opacity: status === Thumbnail.Ready ? 1.0 : 0.0 opacity: status === Thumbnail.Ready ? 1.0 : 0.0
Behavior on opacity { FadeAnimation {} } Behavior on opacity { FadeAnimation {} }

View file

@ -20,7 +20,6 @@ import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import QtMultimedia 5.6 import QtMultimedia 5.6
import "../" import "../"
import "../../js/functions.js" as Functions
import "../../js/debug.js" as Debug import "../../js/debug.js" as Debug
Item { Item {

View file

@ -22,28 +22,25 @@ import "../"
MessageContentBase { MessageContentBase {
function calculateBiggest() { height: Math.max(Theme.itemSizeExtraSmall, Math.min(Math.round(width * 0.66666666), width / getAspectRatio()))
var candidateBiggest = rawMessage.content.photo.sizes[rawMessage.content.photo.sizes.length - 1]; readonly property alias photoData: photo.photo;
if (candidateBiggest.width === 0 && rawMessage.content.photo.sizes.length > 1) {
for (var i = (rawMessage.content.photo.sizes.length - 2); i >= 0; i--) { onClicked: {
candidateBiggest = rawMessage.content.photo.sizes[i]; pageStack.push(Qt.resolvedUrl("../../pages/MediaAlbumPage.qml"), {
if (candidateBiggest.width > 0) { "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; break;
} }
} }
} }
return candidateBiggest; return candidate.width / candidate.height;
}
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
})
} }
TDLibPhoto { TDLibPhoto {
id: photo id: photo
@ -51,7 +48,4 @@ MessageContentBase {
photo: rawMessage.content.photo photo: rawMessage.content.photo
highlighted: parent.highlighted highlighted: parent.highlighted
} }
BackgroundImage {
visible: !rawMessage.content.photo.minithumbnail && photo.image.status !== Image.Ready
}
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}

View file

@ -26,7 +26,12 @@ import "../../js/debug.js" as Debug
MessageContentBase { MessageContentBase {
id: videoMessageComponent 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 string videoUrl;
property int previewFileId; property int previewFileId;
property int videoFileId; property int videoFileId;

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
MessagePhotoAlbum {}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 }
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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
}
}

View file

@ -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
// }
}

View file

@ -609,7 +609,8 @@ Page {
Connections { Connections {
target: chatModel target: chatModel
onMessagesReceived: { 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 (totalCount === 0) {
if (chatPage.iterativeInitialization) { if (chatPage.iterativeInitialization) {
chatPage.iterativeInitialization = false; chatPage.iterativeInitialization = false;
@ -623,9 +624,9 @@ Page {
} }
chatView.lastReadSentIndex = lastReadSentIndex; chatView.lastReadSentIndex = lastReadSentIndex;
chatView.scrollToIndex(modelIndex); chatView.scrollToIndex(proxyIndex);
chatPage.loading = false; chatPage.loading = false;
if (chatOverviewItem.visible && modelIndex >= (chatView.count - 10)) { if (chatOverviewItem.visible && proxyIndex >= (chatView.count - 10)) {
chatView.inCooldown = true; chatView.inCooldown = true;
chatModel.triggerLoadMoreFuture(); chatModel.triggerLoadMoreFuture();
} }
@ -668,10 +669,13 @@ Page {
chatView.lastReadSentIndex = lastReadSentIndex; chatView.lastReadSentIndex = lastReadSentIndex;
} }
onMessagesIncrementalUpdate: { 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; chatView.lastReadSentIndex = lastReadSentIndex;
if (!chatPage.isInitialized) { if (!chatPage.isInitialized) {
chatView.scrollToIndex(modelIndex); if (proxyIndex > -1) {
chatView.scrollToIndex(proxyIndex);
}
} }
if (chatView.height > chatView.contentHeight) { if (chatView.height > chatView.contentHeight) {
Debug.log("[ChatPage] Chat content quite small..."); Debug.log("[ChatPage] Chat content quite small...");
@ -747,14 +751,26 @@ Page {
onTriggered: { onTriggered: {
Debug.log("scroll position changed, message index: ", lastQueuedIndex); Debug.log("scroll position changed, message index: ", lastQueuedIndex);
Debug.log("unread count: ", chatInformation.unread_count); 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") { if (messageToRead['@type'] === "sponsoredMessage") {
Debug.log("sponsored message to read: ", messageToRead.id); Debug.log("sponsored message to read: ", messageToRead.id);
tdLibWrapper.viewMessage(chatInformation.id, messageToRead.message_id, false); tdLibWrapper.viewMessage(chatInformation.id, messageToRead.message_id, false);
} else if (chatInformation.unread_count > 0 && lastQueuedIndex > -1) { } else if (chatInformation.unread_count > 0 && lastQueuedIndex > -1) {
if (messageToRead) {
Debug.log("message to read: ", messageToRead.id); Debug.log("message to read: ", messageToRead.id);
if (messageToRead && messageToRead.id) { var messageId = messageToRead.id;
tdLibWrapper.viewMessage(chatInformation.id, messageToRead.id, false); 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 lastQueuedIndex = -1
} }
@ -1222,7 +1238,6 @@ Page {
readonly property int messageInReplyToHeight: Theme.fontSizeExtraSmall * 2.571428571 + Theme.paddingSmall; 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 int webPagePreviewHeight: ( (textColumnWidth * 2 / 3) + (6 * Theme.fontSizeExtraSmall) + ( 7 * Theme.paddingSmall) )
readonly property bool pageIsSelecting: chatPage.isSelecting readonly property bool pageIsSelecting: chatPage.isSelecting
} }
function handleScrollPositionChanged() { function handleScrollPositionChanged() {
@ -1245,6 +1260,9 @@ Page {
positionViewAtIndex(index, (mode === undefined) ? ListView.Contain : mode) positionViewAtIndex(index, (mode === undefined) ? ListView.Contain : mode)
if(index === chatView.count - 1) { if(index === chatView.count - 1) {
manuallyScrolledToBottom = true; 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 { header: Component {
Loader { Loader {
active: !!chatPage.botInformation active: !!chatPage.botInformation
@ -1310,7 +1334,8 @@ Page {
} }
} }
function getContentComponentHeight(contentType, content, parentWidth) { function getContentComponentHeight(contentType, content, parentWidth, albumEntries) {
var unit;
switch(contentType) { switch(contentType) {
case "messageAnimatedEmoji": case "messageAnimatedEmoji":
return content.animated_emoji.sticker.height; return content.animated_emoji.sticker.height;
@ -1326,6 +1351,10 @@ Page {
case "messageVenue": case "messageVenue":
return parentWidth * 0.66666666; // 2 / 3; return parentWidth * 0.66666666; // 2 / 3;
case "messagePhoto": 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 biggest = content.photo.sizes[content.photo.sizes.length - 1];
var aspectRatio = biggest.width/biggest.height; var aspectRatio = biggest.width/biggest.height;
return Math.max(Theme.itemSizeExtraSmall, Math.min(parentWidth * 0.66666666, parentWidth / aspectRatio)); return Math.max(Theme.itemSizeExtraSmall, Math.min(parentWidth * 0.66666666, parentWidth / aspectRatio));
@ -1334,6 +1363,10 @@ Page {
case "messageSticker": case "messageSticker":
return content.sticker.height; return content.sticker.height;
case "messageVideo": 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); return Functions.getVideoHeight(parentWidth, content.video);
case "messageVideoNote": case "messageVideoNote":
return parentWidth return parentWidth
@ -1389,10 +1422,11 @@ Page {
chatId: chatModel.chatId chatId: chatModel.chatId
myMessage: model.display myMessage: model.display
messageId: model.message_id messageId: model.message_id
messageAlbumMessageIds: model.album_message_ids
messageViewCount: model.view_count messageViewCount: model.view_count
reactions: model.reactions reactions: model.reactions
chatReactions: availableReactions chatReactions: availableReactions
messageIndex: model.index messageIndex: chatProxyModel.mapRowToSource(model.index)
hasContentComponent: !!myMessage.content && chatView.delegateMessagesContent.indexOf(model.content_type) > -1 hasContentComponent: !!myMessage.content && chatView.delegateMessagesContent.indexOf(model.content_type) > -1
canReplyToMessage: chatPage.canSendMessages canReplyToMessage: chatPage.canSendMessages
onReplyToMessage: { onReplyToMessage: {
@ -1413,9 +1447,21 @@ Page {
id: messageListViewItemSimpleComponent id: messageListViewItemSimpleComponent
MessageListViewItemSimple {} 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
} }
VerticalScrollDecorator {} }
sourceComponent: chatView.simpleDelegateMessages.indexOf(model.content_type) > -1
? messageListViewItemSimpleComponent
: messageListViewItemComponent
}
VerticalScrollDecorator { flickable: chatView }
ViewPlaceholder { ViewPlaceholder {
id: chatViewPlaceholder id: chatViewPlaceholder

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
// 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]
//
}
}

153
src/boolfiltermodel.cpp Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<QAbstractItemModel*>(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<int,QByteArray> roleMap(model->roleNames());
const QList<int> 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);
}

63
src/boolfiltermodel.h Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef BOOLFILTERMODEL_H
#define BOOLFILTERMODEL_H
#include <QSortFilterProxyModel>
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

View file

@ -30,6 +30,7 @@ namespace {
const QString ID("id"); const QString ID("id");
const QString CONTENT("content"); const QString CONTENT("content");
const QString CHAT_ID("chat_id"); const QString CHAT_ID("chat_id");
const QString DATE("date");
const QString PHOTO("photo"); const QString PHOTO("photo");
const QString SMALL("small"); const QString SMALL("small");
const QString UNREAD_COUNT("unread_count"); const QString UNREAD_COUNT("unread_count");
@ -48,6 +49,7 @@ namespace {
// "view_count": 47 // "view_count": 47
// } // }
const QString TYPE_MESSAGE_INTERACTION_INFO("messageInteractionInfo"); const QString TYPE_MESSAGE_INTERACTION_INFO("messageInteractionInfo");
const QString MEDIA_ALBUM_ID("media_album_id");
const QString INTERACTION_INFO("interaction_info"); const QString INTERACTION_INFO("interaction_info");
const QString VIEW_COUNT("view_count"); const QString VIEW_COUNT("view_count");
const QString REACTIONS("reactions"); const QString REACTIONS("reactions");
@ -63,7 +65,9 @@ public:
RoleMessageId, RoleMessageId,
RoleMessageContentType, RoleMessageContentType,
RoleMessageViewCount, RoleMessageViewCount,
RoleMessageReactions RoleMessageReactions,
RoleMessageAlbumEntryFilter,
RoleMessageAlbumMessageIds,
}; };
enum RoleFlag { enum RoleFlag {
@ -71,7 +75,9 @@ public:
RoleFlagMessageId = 0x02, RoleFlagMessageId = 0x02,
RoleFlagMessageContentType = 0x04, RoleFlagMessageContentType = 0x04,
RoleFlagMessageViewCount = 0x08, RoleFlagMessageViewCount = 0x08,
RoleFlagMessageReactions = 0x16 RoleFlagMessageReactions = 0x16,
RoleFlagMessageAlbumEntryFilter = 0x32,
RoleFlagMessageAlbumMessageIds = 0x64
}; };
MessageData(const QVariantMap &data, qlonglong msgid); MessageData(const QVariantMap &data, qlonglong msgid);
@ -86,12 +92,16 @@ public:
uint updateViewCount(const QVariantMap &interactionInfo); uint updateViewCount(const QVariantMap &interactionInfo);
uint updateInteractionInfo(const QVariantMap &interactionInfo); uint updateInteractionInfo(const QVariantMap &interactionInfo);
uint updateReactions(const QVariantMap &interactionInfo); uint updateReactions(const QVariantMap &interactionInfo);
uint updateAlbumEntryFilter(const bool isAlbumChild);
uint updateAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds);
QVector<int> diff(const MessageData *message) const; QVector<int> diff(const MessageData *message) const;
QVector<int> setMessageData(const QVariantMap &data); QVector<int> setMessageData(const QVariantMap &data);
QVector<int> setContent(const QVariantMap &content); QVector<int> setContent(const QVariantMap &content);
QVector<int> setReplyMarkup(const QVariantMap &replyMarkup); QVector<int> setReplyMarkup(const QVariantMap &replyMarkup);
QVector<int> setInteractionInfo(const QVariantMap &interactionInfo); QVector<int> setInteractionInfo(const QVariantMap &interactionInfo);
QVector<int> setAlbumEntryFilter(bool isAlbumChild);
QVector<int> setAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds);
int senderUserId() const; int senderUserId() const;
qlonglong senderChatId() const; qlonglong senderChatId() const;
@ -104,6 +114,8 @@ public:
QString messageContentType; QString messageContentType;
int viewCount; int viewCount;
QVariantList reactions; QVariantList reactions;
bool albumEntryFilter;
QVariantList albumMessageIds;
}; };
ChatModel::MessageData::MessageData(const QVariantMap &data, qlonglong msgid) : 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()), messageType(data.value(_TYPE).toString()),
messageContentType(data.value(CONTENT).toMap().value(_TYPE).toString()), messageContentType(data.value(CONTENT).toMap().value(_TYPE).toString()),
viewCount(data.value(INTERACTION_INFO).toMap().value(VIEW_COUNT).toInt()), 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<int> ChatModel::MessageData::flagsToRoles(uint flags)
if (flags & RoleFlagMessageReactions) { if (flags & RoleFlagMessageReactions) {
roles.append(RoleMessageReactions); roles.append(RoleMessageReactions);
} }
if (flags & RoleFlagMessageAlbumEntryFilter) {
roles.append(RoleMessageAlbumEntryFilter);
}
if (flags & RoleFlagMessageAlbumMessageIds) {
roles.append(RoleMessageAlbumMessageIds);
}
return roles; return roles;
} }
@ -169,6 +189,12 @@ QVector<int> ChatModel::MessageData::diff(const MessageData *message) const
if (message->reactions != reactions) { if (message->reactions != reactions) {
roles.append(RoleMessageReactions); roles.append(RoleMessageReactions);
} }
if (message->albumEntryFilter != albumEntryFilter) {
roles.append(RoleMessageAlbumEntryFilter);
}
if (message->albumMessageIds != albumMessageIds) {
roles.append(RoleMessageAlbumMessageIds);
}
} }
return roles; return roles;
} }
@ -237,6 +263,37 @@ uint ChatModel::MessageData::updateReactions(const QVariantMap &interactionInfo)
return (reactions == oldReactions) ? 0 : RoleFlagMessageReactions; 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<int> 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<int> ChatModel::MessageData::setAlbumEntryMessageIds(const QVariantList &newAlbumMessageIds)
{
return flagsToRoles(updateAlbumEntryMessageIds(newAlbumMessageIds));
}
QVector<int> ChatModel::MessageData::setInteractionInfo(const QVariantMap &info) QVector<int> ChatModel::MessageData::setInteractionInfo(const QVariantMap &info)
{ {
return flagsToRoles(updateInteractionInfo(info)); return flagsToRoles(updateInteractionInfo(info));
@ -295,6 +352,8 @@ QHash<int,QByteArray> ChatModel::roleNames() const
roles.insert(MessageData::RoleMessageContentType, "content_type"); roles.insert(MessageData::RoleMessageContentType, "content_type");
roles.insert(MessageData::RoleMessageViewCount, "view_count"); roles.insert(MessageData::RoleMessageViewCount, "view_count");
roles.insert(MessageData::RoleMessageReactions, "reactions"); roles.insert(MessageData::RoleMessageReactions, "reactions");
roles.insert(MessageData::RoleMessageAlbumEntryFilter, "album_entry_filter");
roles.insert(MessageData::RoleMessageAlbumMessageIds, "album_message_ids");
return roles; return roles;
} }
@ -314,6 +373,8 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case MessageData::RoleMessageContentType: return message->messageContentType; case MessageData::RoleMessageContentType: return message->messageContentType;
case MessageData::RoleMessageViewCount: return message->viewCount; case MessageData::RoleMessageViewCount: return message->viewCount;
case MessageData::RoleMessageReactions: return message->reactions; case MessageData::RoleMessageReactions: return message->reactions;
case MessageData::RoleMessageAlbumEntryFilter: return message->albumEntryFilter;
case MessageData::RoleMessageAlbumMessageIds: return message->albumMessageIds;
} }
} }
return QVariant(); return QVariant();
@ -331,6 +392,7 @@ void ChatModel::clear(bool contentOnly)
qDeleteAll(messages); qDeleteAll(messages);
messages.clear(); messages.clear();
messageIndexMap.clear(); messageIndexMap.clear();
albumMessageMap.clear();
endResetModel(); endResetModel();
} }
@ -356,6 +418,7 @@ void ChatModel::initialize(const QVariantMap &chatInformation)
this->chatId = chatId; this->chatId = chatId;
this->messages.clear(); this->messages.clear();
this->messageIndexMap.clear(); this->messageIndexMap.clear();
this->albumMessageMap.clear();
this->searchQuery.clear(); this->searchQuery.clear();
endResetModel(); endResetModel();
emit chatIdChanged(); emit chatIdChanged();
@ -420,6 +483,36 @@ int ChatModel::getMessageIndex(qlonglong messageId)
return -1; 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() int ChatModel::getLastReadMessageIndex()
{ {
LOG("Obtaining last read message index"); 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(); const qlonglong messageId = messageData.value(ID).toLongLong();
if (messageId && messageData.value(CHAT_ID).toLongLong() == chatId && !messageIndexMap.contains(messageId)) { if (messageId && messageData.value(CHAT_ID).toLongLong() == chatId && !messageIndexMap.contains(messageId)) {
LOG("New message will be added:" << 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()) { if (!messagesToBeAdded.isEmpty()) {
insertMessages(messagesToBeAdded); insertMessages(messagesToBeAdded);
setMessagesAlbum(messagesToBeAdded);
} }
// First call only returns a few messages, we need to get a little more than that... // 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<MessageData*> messagesToBeAdded; QList<MessageData*> messagesToBeAdded;
messagesToBeAdded.append(new MessageData(message, messageId)); messagesToBeAdded.append(new MessageData(message, messageId));
insertMessages(messagesToBeAdded); insertMessages(messagesToBeAdded);
setMessagesAlbum(messagesToBeAdded);
emit newMessageReceived(message); emit newMessageReceived(message);
} else { } else {
LOG("New message in this chat, but not relevant as less recent messages need to be loaded first!"); 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); messages.replace(pos, newMessage);
messageIndexMap.remove(oldMessageId); messageIndexMap.remove(oldMessageId);
messageIndexMap.insert(messageId, pos); messageIndexMap.insert(messageId, pos);
// TODO when we support sending album messages, handle ID change in albumMessageMap
const QVector<int> changedRoles(newMessage->diff(oldMessage)); const QVector<int> changedRoles(newMessage->diff(oldMessage));
delete oldMessage; delete oldMessage;
LOG("Message was replaced at index" << pos); 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); LOG("We know the message that was updated" << messageId);
const int pos = messageIndexMap.value(messageId, -1); const int pos = messageIndexMap.value(messageId, -1);
if (pos >= 0) { if (pos >= 0) {
const QVector<int> changedRoles(messages.at(pos)->setContent(newContent)); MessageData* messageData = messages.at(pos);
const QVector<int> changedRoles(messageData->setContent(newContent));
LOG("Message was updated at index" << pos); LOG("Message was updated at index" << pos);
const QModelIndex messageIndex(index(pos)); const QModelIndex messageIndex(index(pos));
emit dataChanged(messageIndex, messageIndex, changedRoles); 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); LOG("We know the message that was updated" << messageId);
const int pos = messageIndexMap.value(messageId, -1); const int pos = messageIndexMap.value(messageId, -1);
if (pos >= 0) { if (pos >= 0) {
const QVector<int> changedRoles(messages.at(pos)->setReplyMarkup(replyMarkup)); MessageData* messageData = messages.at(pos);
const QVector<int> changedRoles(messageData->setReplyMarkup(replyMarkup));
LOG("Message was edited at index" << pos); LOG("Message was edited at index" << pos);
const QModelIndex messageIndex(index(pos)); const QModelIndex messageIndex(index(pos));
emit dataChanged(messageIndex, messageIndex, changedRoles); emit dataChanged(messageIndex, messageIndex, changedRoles);
@ -709,18 +808,31 @@ void ChatModel::handleMessagesDeleted(qlonglong chatId, const QList<qlonglong> &
} }
} }
void ChatModel::removeRange(int firstDeleted, int lastDeleted) void ChatModel::removeRange(int firstDeleted, int lastDeleted)
{ {
if (firstDeleted >= 0 && firstDeleted <= lastDeleted) { if (firstDeleted >= 0 && firstDeleted <= lastDeleted) {
LOG("Removing range" << firstDeleted << "..." << lastDeleted << "| current messages size" << messages.size()); LOG("Removing range" << firstDeleted << "..." << lastDeleted << "| current messages size" << messages.size());
beginRemoveRows(QModelIndex(), firstDeleted, lastDeleted); beginRemoveRows(QModelIndex(), firstDeleted, lastDeleted);
QList<qlonglong> rescanAlbumIds;
for (int i = firstDeleted; i <= lastDeleted; i++) { for (int i = firstDeleted; i <= lastDeleted; i++) {
MessageData *message = messages.at(i); MessageData *message = messages.at(i);
messageIndexMap.remove(message->messageId); messageIndexMap.remove(message->messageId);
qlonglong albumId = message->messageData.value(MEDIA_ALBUM_ID).toLongLong();
if(albumId != 0 && albumMessageMap.contains(albumId)) {
rescanAlbumIds.append(albumId);
}
delete message; delete message;
} }
messages.erase(messages.begin() + firstDeleted, messages.begin() + (lastDeleted + 1)); 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(); endRemoveRows();
updateAlbumMessages(rescanAlbumIds, true);
} }
} }
@ -757,7 +869,7 @@ void ChatModel::appendMessages(const QList<MessageData*> newMessages)
beginInsertRows(QModelIndex(), oldSize, oldSize + count - 1); beginInsertRows(QModelIndex(), oldSize, oldSize + count - 1);
messages.append(newMessages); messages.append(newMessages);
for (int i = 0; i < count; i++) { 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); messageIndexMap.insert(newMessages.at(i)->messageId, oldSize + i);
} }
endInsertRows(); endInsertRows();
@ -785,6 +897,90 @@ void ChatModel::prependMessages(const QList<MessageData*> newMessages)
endInsertRows(); 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<int> changedRolesFilter;
QVector<int> 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<qlonglong> 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<MessageData *> 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 ChatModel::enhanceMessage(const QVariantMap &message)
{ {
QVariantMap enhancedMessage = message; QVariantMap enhancedMessage = message;

View file

@ -44,6 +44,8 @@ public:
Q_INVOKABLE void triggerLoadMoreFuture(); Q_INVOKABLE void triggerLoadMoreFuture();
Q_INVOKABLE QVariantMap getChatInformation(); Q_INVOKABLE QVariantMap getChatInformation();
Q_INVOKABLE QVariantMap getMessage(int index); 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 int getLastReadMessageIndex();
Q_INVOKABLE void setSearchQuery(const QString newSearchQuery); Q_INVOKABLE void setSearchQuery(const QString newSearchQuery);
@ -85,6 +87,10 @@ private:
void insertMessages(const QList<MessageData*> newMessages); void insertMessages(const QList<MessageData*> newMessages);
void appendMessages(const QList<MessageData*> newMessages); void appendMessages(const QList<MessageData*> newMessages);
void prependMessages(const QList<MessageData*> newMessages); void prependMessages(const QList<MessageData*> newMessages);
void updateAlbumMessages(qlonglong albumId, bool checkDeleted);
void updateAlbumMessages(QList<qlonglong> albumIds, bool checkDeleted);
void setMessagesAlbum(const QList<MessageData*> newMessages);
void setMessagesAlbum(MessageData *message);
QVariantMap enhanceMessage(const QVariantMap &message); QVariantMap enhanceMessage(const QVariantMap &message);
int calculateLastKnownMessageId(); int calculateLastKnownMessageId();
int calculateLastReadSentMessageId(); int calculateLastReadSentMessageId();
@ -95,6 +101,7 @@ private:
TDLibWrapper *tdLibWrapper; TDLibWrapper *tdLibWrapper;
QList<MessageData*> messages; QList<MessageData*> messages;
QHash<qlonglong,int> messageIndexMap; QHash<qlonglong,int> messageIndexMap;
QHash<qlonglong, QVariantList> albumMessageMap;
QVariantMap chatInformation; QVariantMap chatInformation;
qlonglong chatId; qlonglong chatId;
bool inReload; bool inReload;

View file

@ -51,6 +51,7 @@
#include "processlauncher.h" #include "processlauncher.h"
#include "stickermanager.h" #include "stickermanager.h"
#include "textfiltermodel.h" #include "textfiltermodel.h"
#include "boolfiltermodel.h"
#include "tgsplugin.h" #include "tgsplugin.h"
#include "fernschreiberutils.h" #include "fernschreiberutils.h"
#include "knownusersmodel.h" #include "knownusersmodel.h"
@ -130,6 +131,7 @@ int main(int argc, char *argv[])
qmlRegisterType<TDLibFile>(uri, 1, 0, "TDLibFile"); qmlRegisterType<TDLibFile>(uri, 1, 0, "TDLibFile");
qmlRegisterType<NamedAction>(uri, 1, 0, "NamedAction"); qmlRegisterType<NamedAction>(uri, 1, 0, "NamedAction");
qmlRegisterType<TextFilterModel>(uri, 1, 0, "TextFilterModel"); qmlRegisterType<TextFilterModel>(uri, 1, 0, "TextFilterModel");
qmlRegisterType<BoolFilterModel>(uri, 1, 0, "BoolFilterModel");
qmlRegisterType<ChatPermissionFilterModel>(uri, 1, 0, "ChatPermissionFilterModel"); qmlRegisterType<ChatPermissionFilterModel>(uri, 1, 0, "ChatPermissionFilterModel");
qmlRegisterSingletonType<DebugLogJS>(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton); qmlRegisterSingletonType<DebugLogJS>(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton);

View file

@ -499,13 +499,6 @@
<translation>No hay charlas.</translation> <translation>No hay charlas.</translation>
</message> </message>
</context> </context>
<context>
<name>ContactSync</name>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation>No se puede sincronizar los contactos con Telegrama.</translation>
</message>
</context>
<context> <context>
<name>CoverPage</name> <name>CoverPage</name>
<message> <message>

View file

@ -1634,34 +1634,6 @@
<source>When tapping a quoted message, open it in chat instead of showing it in an overlay.</source> <source>When tapping a quoted message, open it in chat instead of showing it in an overlay.</source>
<translation>Citovanú správu otvoriť v čete namiesto v náhľade.</translation> <translation>Citovanú správu otvoriť v čete namiesto v náhľade.</translation>
</message> </message>
<message>
<source>Always append message preview to notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>In addition to showing the number of unread messages, the latest message will also be appended to notifications.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Highlight unread messages</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Highlight Conversations with unread messages</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hide content in notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Go to quoted message</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>When tapping a quoted message, open it in chat instead of showing it in an overlay.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>SettingsPage</name> <name>SettingsPage</name>