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