Добавлена поддержка галереии в постах. #16
23 changed files with 1651 additions and 103 deletions
|
@ -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 \
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {} }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
207
qml/components/messageContent/MessagePhotoAlbum.qml
Normal file
207
qml/components/messageContent/MessagePhotoAlbum.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
19
qml/components/messageContent/MessageVideoAlbum.qml
Normal file
19
qml/components/messageContent/MessageVideoAlbum.qml
Normal 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 {}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
181
qml/components/messageContent/mediaAlbumPage/VideoComponent.qml
Normal file
181
qml/components/messageContent/mediaAlbumPage/VideoComponent.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
148
qml/components/messageContent/mediaAlbumPage/ZoomArea.qml
Normal file
148
qml/components/messageContent/mediaAlbumPage/ZoomArea.qml
Normal 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
|
||||
}
|
||||
}
|
127
qml/components/messageContent/mediaAlbumPage/ZoomImage.qml
Normal file
127
qml/components/messageContent/mediaAlbumPage/ZoomImage.qml
Normal 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
|
||||
// }
|
||||
}
|
|
@ -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
|
||||
|
|
109
qml/pages/MediaAlbumPage.qml
Normal file
109
qml/pages/MediaAlbumPage.qml
Normal 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
153
src/boolfiltermodel.cpp
Normal 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
63
src/boolfiltermodel.h
Normal 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
|
|
@ -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<int> diff(const MessageData *message) const;
|
||||
QVector<int> setMessageData(const QVariantMap &data);
|
||||
QVector<int> setContent(const QVariantMap &content);
|
||||
QVector<int> setReplyMarkup(const QVariantMap &replyMarkup);
|
||||
QVector<int> setInteractionInfo(const QVariantMap &interactionInfo);
|
||||
QVector<int> setAlbumEntryFilter(bool isAlbumChild);
|
||||
QVector<int> 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<int> 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<int> 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<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)
|
||||
{
|
||||
return flagsToRoles(updateInteractionInfo(info));
|
||||
|
@ -295,6 +352,8 @@ QHash<int,QByteArray> 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<MessageData*> 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<int> 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<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);
|
||||
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<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);
|
||||
const QModelIndex messageIndex(index(pos));
|
||||
emit dataChanged(messageIndex, messageIndex, changedRoles);
|
||||
|
@ -709,18 +808,31 @@ void ChatModel::handleMessagesDeleted(qlonglong chatId, const QList<qlonglong> &
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
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<qlonglong> 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<MessageData*> 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<MessageData*> 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<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 enhancedMessage = message;
|
||||
|
|
|
@ -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<MessageData*> newMessages);
|
||||
void appendMessages(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);
|
||||
int calculateLastKnownMessageId();
|
||||
int calculateLastReadSentMessageId();
|
||||
|
@ -95,6 +101,7 @@ private:
|
|||
TDLibWrapper *tdLibWrapper;
|
||||
QList<MessageData*> messages;
|
||||
QHash<qlonglong,int> messageIndexMap;
|
||||
QHash<qlonglong, QVariantList> albumMessageMap;
|
||||
QVariantMap chatInformation;
|
||||
qlonglong chatId;
|
||||
bool inReload;
|
||||
|
|
|
@ -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<TDLibFile>(uri, 1, 0, "TDLibFile");
|
||||
qmlRegisterType<NamedAction>(uri, 1, 0, "NamedAction");
|
||||
qmlRegisterType<TextFilterModel>(uri, 1, 0, "TextFilterModel");
|
||||
qmlRegisterType<BoolFilterModel>(uri, 1, 0, "BoolFilterModel");
|
||||
qmlRegisterType<ChatPermissionFilterModel>(uri, 1, 0, "ChatPermissionFilterModel");
|
||||
qmlRegisterSingletonType<DebugLogJS>(uri, 1, 0, "DebugLog", DebugLogJS::createSingleton);
|
||||
|
||||
|
|
|
@ -499,13 +499,6 @@
|
|||
<translation>No hay charlas.</translation>
|
||||
</message>
|
||||
</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>
|
||||
<name>CoverPage</name>
|
||||
<message>
|
||||
|
|
|
@ -1634,34 +1634,6 @@
|
|||
<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>
|
||||
</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>
|
||||
<name>SettingsPage</name>
|
||||
|
|
Loading…
Reference in a new issue