diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index f751a28..a99ffc0 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -16,7 +16,7 @@ CONFIG += sailfishapp sailfishapp_i18n PKGCONFIG += nemonotifications-qt5 zlib -QT += core dbus sql +QT += core dbus sql multimedia positioning DEFINES += QT_STATICPLUGIN @@ -58,6 +58,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \ qml/components/ReplyMarkupButtons.qml \ qml/components/StickerPicker.qml \ qml/components/PhotoTextsListItem.qml \ + qml/components/VoiceNoteOverlay.qml \ qml/components/WebPagePreview.qml \ qml/components/chatInformationPage/ChatInformationEditArea.qml \ qml/components/chatInformationPage/ChatInformationPageContent.qml \ diff --git a/images/icon-s-pin.svg b/images/icon-s-pin.svg new file mode 100644 index 0000000..80dfab8 --- /dev/null +++ b/images/icon-s-pin.svg @@ -0,0 +1,26 @@ + +image/svg+xml \ No newline at end of file diff --git a/qml/components/AudioPreview.qml b/qml/components/AudioPreview.qml index e37c8ac..b8a7d17 100644 --- a/qml/components/AudioPreview.qml +++ b/qml/components/AudioPreview.qml @@ -444,7 +444,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: positionText.top minimumValue: 0 - maximumValue: messageAudio.duration ? messageAudio.duration : 0 + maximumValue: messageAudio.duration ? messageAudio.duration : 0.1 stepSize: 1 value: messageAudio.position enabled: messageAudio.seekable diff --git a/qml/components/ChatListViewItem.qml b/qml/components/ChatListViewItem.qml index b01c487..684f02a 100644 --- a/qml/components/ChatListViewItem.qml +++ b/qml/components/ChatListViewItem.qml @@ -9,13 +9,14 @@ PhotoTextsListItem { id: listItem pictureThumbnail { photoData: photo_small || ({}) + highlighted: listItem.highlighted && !listItem.menuOpen } property int ownUserId property bool showDraft: !!draft_message_text && draft_message_date > last_message_date property string previewText: showDraft ? draft_message_text : last_message_text // chat title - primaryText.text: title ? Emoji.emojify(title + ( display.notification_settings.mute_for > 0 ? " 🔇" : "" ), Theme.fontSizeMedium) : qsTr("Unknown") + primaryText.text: title ? Emoji.emojify(title, Theme.fontSizeMedium) : qsTr("Unknown") // last user prologSecondaryText.text: showDraft ? ""+qsTr("Draft")+"" : (is_channel ? "" : ( last_message_sender_id ? ( last_message_sender_id !== ownUserId ? Emoji.emojify(Functions.getUserName(tdLibWrapper.getUserInformation(last_message_sender_id)), primaryText.font.pixelSize) : qsTr("You") ) : "" )) // last message @@ -25,6 +26,8 @@ PhotoTextsListItem { unreadCount: unread_count isSecret: ( chat_type === TelegramAPI.ChatTypeSecret ) isMarkedAsUnread: is_marked_as_unread + isPinned: is_pinned + isMuted: display.notification_settings.mute_for > 0 openMenuOnPressAndHold: true//chat_id != overviewPage.ownUserId @@ -54,19 +57,18 @@ PhotoTextsListItem { } MenuItem { - visible: unread_count === 0 && !is_marked_as_unread + visible: unread_count === 0 onClicked: { - tdLibWrapper.toggleChatIsMarkedAsUnread(chat_id, true); + tdLibWrapper.toggleChatIsMarkedAsUnread(chat_id, !is_marked_as_unread); } - text: qsTr("Mark chat as unread") + text: is_marked_as_unread ? qsTr("Mark chat as read") : qsTr("Mark chat as unread") } MenuItem { - visible: unread_count === 0 && is_marked_as_unread onClicked: { - tdLibWrapper.toggleChatIsMarkedAsUnread(chat_id, false); + tdLibWrapper.toggleChatIsPinned(chat_id, !is_pinned); } - text: qsTr("Mark chat as read") + text: is_pinned ? qsTr("Unpin chat") : qsTr("Pin chat") } MenuItem { @@ -81,7 +83,7 @@ PhotoTextsListItem { newNotificationSettings.use_default_mute_for = false; tdLibWrapper.setChatNotificationSettings(chat_id, newNotificationSettings); } - text: display.notification_settings.mute_for > 0 ? qsTr("Unmute Chat") : qsTr("Mute Chat") + text: display.notification_settings.mute_for > 0 ? qsTr("Unmute chat") : qsTr("Mute chat") } MenuItem { diff --git a/qml/components/DocumentPreview.qml b/qml/components/DocumentPreview.qml index df5a6f1..fa1e7cf 100644 --- a/qml/components/DocumentPreview.qml +++ b/qml/components/DocumentPreview.qml @@ -41,9 +41,9 @@ Item { if (documentData) { if (documentData.document.local.is_downloading_completed) { downloadDocumentButton.visible = false; - openDocumentButton.visible = true; + openDocumentArea.visible = true; } else { - openDocumentButton.visible = false; + openDocumentArea.visible = false; downloadDocumentButton.visible = true; } } @@ -57,7 +57,7 @@ Item { downloadingProgressBar.visible = false; documentData.document = fileInformation; downloadDocumentButton.visible = false; - openDocumentButton.visible = true; + openDocumentArea.visible = true; if (documentPreviewItem.openRequested) { documentPreviewItem.openRequested = false; tdLibWrapper.openFileOnDevice(documentData.document.local.path); @@ -95,17 +95,40 @@ Item { anchors.centerIn: parent } - Button { - id: openDocumentButton - preferredWidth: Theme.buttonWidthMedium - anchors.centerIn: parent - text: qsTr("Open Document") + Column { + id: openDocumentArea visible: false - highlighted: documentPreviewItem.highlighted || down - onClicked: { - documentPreviewItem.openRequested = true; - tdLibWrapper.openFileOnDevice(documentData.document.local.path); + spacing: Theme.paddingMedium + width: parent.width + + onVisibleChanged: { + visible ? (documentPreviewItem.height = openDocumentArea.height) : (documentPreviewItem.height = Theme.itemSizeLarge); + } + + Button { + id: openDocumentButton + preferredWidth: Theme.buttonWidthMedium + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Open Document") + highlighted: documentPreviewItem.highlighted || down + onClicked: { + documentPreviewItem.openRequested = true; + tdLibWrapper.openFileOnDevice(documentData.document.local.path); + } + } + + Button { + id: copyDocumentButton + preferredWidth: Theme.buttonWidthMedium + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Copy Document to Downloads") + highlighted: documentPreviewItem.highlighted || down + onClicked: { + tdLibWrapper.copyFileToDownloads(documentData.document.local.path); + } } } + + } diff --git a/qml/components/MessageListViewItem.qml b/qml/components/MessageListViewItem.qml index 2a3e32e..bc306e7 100644 --- a/qml/components/MessageListViewItem.qml +++ b/qml/components/MessageListViewItem.qml @@ -27,6 +27,7 @@ ListItem { contentHeight: messageBackground.height + Theme.paddingMedium property var chatId property var messageId + property int messageIndex property var myMessage property bool canReplyToMessage readonly property bool isAnonymous: myMessage.sender["@type"] === "messageSenderChat" @@ -159,13 +160,6 @@ ListItem { 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); } - onMessageUpdated: { - if (index === modelIndex) { - Debug.log("[ChatModel] This message was updated, index ", index, ", updating content..."); - messageDateText.text = getMessageStatusText(myMessage, index, chatView.lastReadSentIndex, messageDateText.useElapsed); - messageText.text = Emoji.emojify(Functions.getMessageText(myMessage, false, page.myUserId, false), messageText.font.pixelSize); - } - } } Connections { @@ -190,6 +184,15 @@ ListItem { } } + onMyMessageChanged: { + Debug.log("[ChatModel] This message was updated, index", messageIndex, ", updating content...") + messageDateText.text = getMessageStatusText(myMessage, messageIndex, chatView.lastReadSentIndex, messageDateText.useElapsed) + messageText.text = Emoji.emojify(Functions.getMessageText(myMessage, false, page.myUserId, false), messageText.font.pixelSize) + if (webPagePreviewLoader.item) { + webPagePreviewLoader.item.webPageData = myMessage.content.web_page + } + } + Timer { id: delegateComponentLoadingTimer interval: 500 diff --git a/qml/components/PhotoTextsListItem.qml b/qml/components/PhotoTextsListItem.qml index f5fbb4b..5c69d39 100644 --- a/qml/components/PhotoTextsListItem.qml +++ b/qml/components/PhotoTextsListItem.qml @@ -14,6 +14,8 @@ ListItem { property bool isSecret: false property bool isVerified: false property bool isMarkedAsUnread: false + property bool isPinned: false + property bool isMuted: false property alias pictureThumbnail: pictureThumbnail contentHeight: mainRow.height + separator.height + 2 * Theme.paddingMedium @@ -33,15 +35,14 @@ ListItem { height: contentColumn.height spacing: Theme.paddingMedium - Column { - id: pictureColumn + ShaderEffectSource { + id: pictureItem width: contentColumn.height - Theme.paddingSmall height: contentColumn.height - Theme.paddingSmall anchors.verticalCenter: parent.verticalCenter - - Item { - width: parent.width - height: parent.width + sourceItem: Item { + width: pictureItem.width + height: pictureItem.width ProfileThumbnail { id: pictureThumbnail @@ -50,11 +51,30 @@ ListItem { height: parent.width } + Rectangle { + id: chatPinnedBackground + color: Theme.highlightBackgroundColor + width: Theme.fontSizeLarge + height: Theme.fontSizeLarge + anchors.top: parent.top + radius: parent.width / 2 + visible: chatListViewItem.isPinned + } + + Image { + source: "../../images/icon-s-pin.svg" + height: Theme.fontSizeSmall + width: Theme.fontSizeSmall + sourceSize: Qt.size(Theme.iconSizeSmall, Theme.iconSizeSmall) + anchors.centerIn: chatPinnedBackground + visible: chatListViewItem.isPinned + } + Rectangle { id: chatSecretBackground - color: Theme.overlayBackgroundColor - width: Theme.fontSizeExtraLarge - height: Theme.fontSizeExtraLarge + color: Theme.highlightBackgroundColor + width: Theme.fontSizeLarge + height: Theme.fontSizeLarge anchors.bottom: parent.bottom radius: parent.width / 2 visible: chatListViewItem.isSecret @@ -62,8 +82,8 @@ ListItem { Image { source: "image://theme/icon-s-secure" - height: Theme.fontSizeMedium - width: Theme.fontSizeMedium + height: Theme.fontSizeSmall + width: Theme.fontSizeSmall anchors.centerIn: chatSecretBackground visible: chatListViewItem.isSecret } @@ -93,7 +113,7 @@ ListItem { Column { id: contentColumn - width: mainColumn.width - pictureColumn.width - mainRow.spacing + width: mainColumn.width - pictureItem.width - mainRow.spacing spacing: Theme.paddingSmall Row { @@ -106,15 +126,26 @@ ListItem { font.pixelSize: Theme.fontSizeMedium truncationMode: TruncationMode.Fade anchors.verticalCenter: parent.verticalCenter - width: Math.min(contentColumn.width - (verifiedImage.visible ? (verifiedImage.width + primaryTextRow.spacing) : 0), implicitWidth) + width: Math.min(contentColumn.width - (verifiedImage.visible ? (verifiedImage.width + primaryTextRow.spacing) : 0) - (mutedImage.visible ? (mutedImage.width + primaryTextRow.spacing) : 0), implicitWidth) } Image { id: verifiedImage anchors.verticalCenter: parent.verticalCenter source: chatListViewItem.isVerified ? "../../images/icon-verified.svg" : "" - sourceSize.width: Theme.iconSizeExtraSmall - width: Theme.iconSizeExtraSmall + sourceSize: Qt.size(Theme.iconSizeExtraSmall, Theme.iconSizeExtraSmall) + width: Theme.iconSizeSmall + height: Theme.iconSizeSmall + visible: status === Image.Ready + } + + Image { + id: mutedImage + anchors.verticalCenter: parent.verticalCenter + source: chatListViewItem.isMuted ? "../js/emoji/1f507.svg" : "" + sourceSize: Qt.size(Theme.iconSizeExtraSmall, Theme.iconSizeExtraSmall) + width: Theme.iconSizeSmall + height: Theme.iconSizeSmall visible: status === Image.Ready } } diff --git a/qml/components/ProfileThumbnail.qml b/qml/components/ProfileThumbnail.qml index 04f7cf9..555cc1c 100644 --- a/qml/components/ProfileThumbnail.qml +++ b/qml/components/ProfileThumbnail.qml @@ -22,7 +22,6 @@ import Sailfish.Silica 1.0 import WerkWolf.Fernschreiber 1.0 Item { - id: profileThumbnail property alias photoData: file.fileInformation @@ -30,6 +29,10 @@ Item { property int radius: width / 2 property int imageStatus: -1 property bool optimizeImageSize: true + property bool highlighted + + layer.enabled: highlighted + layer.effect: PressEffect { source: profileThumbnail } function getReplacementString() { if (replacementStringHint.length > 2) { diff --git a/qml/components/VideoPreview.qml b/qml/components/VideoPreview.qml index 8015d71..7854c2c 100644 --- a/qml/components/VideoPreview.qml +++ b/qml/components/VideoPreview.qml @@ -27,7 +27,7 @@ Item { property ListItem messageListItem property MessageOverlayFlickable overlayFlickable - property var rawMessage: messageListItem ? messageListItem.myMessage : overlayFlickable.overlayMessage + property var rawMessage: messageListItem ? messageListItem.myMessage : ( overlayFlickable ? overlayFlickable.overlayMessage : undefined ) property var videoData: ( rawMessage.content['@type'] === "messageVideo" ) ? rawMessage.content.video : ( ( rawMessage.content['@type'] === "messageAnimation" ) ? rawMessage.content.animation : rawMessage.content.video_note ) property string videoUrl; @@ -89,7 +89,7 @@ Item { videoMessageComponent.videoType = videoMessageComponent.isVideoNote ? "video" : videoData['@type']; videoFileId = videoData[videoType].id; - if (rawMessage.content['@type'] === "messageAnimation") { + if (typeof rawMessage !== "undefined" && rawMessage.content['@type'] === "messageAnimation") { playButton.visible = true; fullscreenButton.visible = !videoMessageComponent.fullscreen; handlePlay(); @@ -294,21 +294,6 @@ Item { } } - Connections { - target: videoMessageComponent - onClicked: { - if (messageVideo.playbackState === MediaPlayer.PlayingState) { - enableScreensaver(); - messageVideo.pause(); - timeLeftItem.visible = true; - } else { - disableScreensaver(); - messageVideo.play(); - timeLeftTimer.start(); - } - } - } - Video { id: messageVideo @@ -367,7 +352,7 @@ Item { height: parent.height source: videoUrl layer.enabled: videoMessageComponent.highlighted - layer.effect: PressEffect { source: singleImage } + layer.effect: PressEffect { source: messageVideo } onStopped: { enableScreensaver(); messageVideo.visible = false; @@ -376,6 +361,21 @@ Item { videoComponentLoader.active = false; fullscreenItem.visible = !videoMessageComponent.fullscreen; } + + MouseArea { + anchors.fill: parent + onClicked: { + if (messageVideo.playbackState === MediaPlayer.PlayingState) { + enableScreensaver(); + messageVideo.pause(); + timeLeftItem.visible = true; + } else { + disableScreensaver(); + messageVideo.play(); + timeLeftTimer.start(); + } + } + } } BusyIndicator { @@ -482,7 +482,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: positionText.top minimumValue: 0 - maximumValue: messageVideo.duration ? messageVideo.duration : 0 + maximumValue: messageVideo.duration ? messageVideo.duration : 0.1 highlighted: videoMessageComponent.highlighted || down stepSize: 1 @@ -514,7 +514,6 @@ Item { } - } } diff --git a/qml/components/VoiceNoteOverlay.qml b/qml/components/VoiceNoteOverlay.qml new file mode 100644 index 0000000..cbec5a9 --- /dev/null +++ b/qml/components/VoiceNoteOverlay.qml @@ -0,0 +1,222 @@ +/* + Copyright (C) 2020-21 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 WerkWolf.Fernschreiber 1.0 +import "../components" +import "../js/twemoji.js" as Emoji +import "../js/debug.js" as Debug + +Item { + id: voiceNoteOverlayItem + anchors.fill: parent + + property int recordingState: fernschreiberUtils.getVoiceNoteRecordingState(); + property int recordingDuration: 0; + property bool recordingDone: false; + + function handleRecordingState() { + switch (recordingState) { + case FernschreiberUtilities.Unavailable: + recordingStateLabel.text = qsTr("Unavailable"); + break; + case FernschreiberUtilities.Ready: + recordingStateLabel.text = qsTr("Ready"); + break; + case FernschreiberUtilities.Starting: + recordingStateLabel.text = qsTr("Starting"); + break; + case FernschreiberUtilities.Recording: + recordingStateLabel.text = qsTr("Recording"); + break; + case FernschreiberUtilities.Stopping: + recordingStateLabel.text = qsTr("Stopping"); + break; + } + } + + function getTwoDigitString(numberToBeConverted) { + var numberString = "00"; + if (numberToBeConverted > 0 && numberToBeConverted < 10) { + numberString = "0" + String(numberToBeConverted); + } + if (numberToBeConverted >= 10) { + numberString = String(numberToBeConverted); + } + return numberString; + } + + function handleRecordingDuration() { + var minutes = Math.floor(recordingDuration / 60); + var seconds = recordingDuration % 60; + recordingDurationLabel.text = getTwoDigitString(minutes) + ":" + getTwoDigitString(seconds); + } + + Component.onCompleted: { + handleRecordingState(); + handleRecordingDuration(); + } + + Connections { + target: fernschreiberUtils + onVoiceNoteDurationChanged: { + Debug.log("New duration received: " + duration); + recordingDuration = Math.round(duration / 1000); + handleRecordingDuration(); + } + onVoiceNoteRecordingStateChanged: { + Debug.log("New state received: " + state); + recordingState = state; + handleRecordingState(); + } + } + + Rectangle { + id: stickerPickerOverlayBackground + anchors.fill: parent + + color: Theme.overlayBackgroundColor + opacity: Theme.opacityHigh + } + + Flickable { + id: voiceNoteFlickable + anchors.fill: parent + anchors.margins: Theme.paddingMedium + + Behavior on opacity { NumberAnimation {} } + + contentHeight: voiceNoteColumn.height + clip: true + + Column { + id: voiceNoteColumn + spacing: Theme.paddingMedium + width: voiceNoteFlickable.width + + InfoLabel { + text: qsTr("Record a Voice Note") + } + + Label { + wrapMode: Text.Wrap + width: parent.width - ( 2 * Theme.horizontalPageMargin ) + horizontalAlignment: Text.AlignHCenter + text: qsTr("Press the button to start recording") + font.pixelSize: Theme.fontSizeMedium + anchors { + horizontalCenter: parent.horizontalCenter + } + } + + Item { + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge + anchors { + horizontalCenter: parent.horizontalCenter + } + Rectangle { + color: Theme.primaryColor + opacity: Theme.opacityOverlay + width: Theme.iconSizeExtraLarge + height: Theme.iconSizeExtraLarge + anchors.centerIn: parent + radius: width / 2 + } + + Rectangle { + id: recordButton + color: "red" + width: Theme.iconSizeExtraLarge * 0.6 + height: Theme.iconSizeExtraLarge * 0.6 + anchors.centerIn: parent + radius: width / 2 + MouseArea { + anchors.fill: parent + onClicked: { + recordButton.visible = false; + recordingDone = false; + recordingDuration = 0; + handleRecordingDuration(); + fernschreiberUtils.startRecordingVoiceNote(); + } + } + } + + Rectangle { + id: stopButton + visible: !recordButton.visible + color: Theme.overlayBackgroundColor + width: Theme.iconSizeExtraLarge * 0.4 + height: Theme.iconSizeExtraLarge * 0.4 + anchors.centerIn: parent + MouseArea { + anchors.fill: parent + onClicked: { + recordButton.visible = true; + fernschreiberUtils.stopRecordingVoiceNote(); + recordingDone = true; + } + } + } + } + + Label { + id: recordingStateLabel + wrapMode: Text.Wrap + width: parent.width - ( 2 * Theme.horizontalPageMargin ) + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeMedium + anchors { + horizontalCenter: parent.horizontalCenter + } + } + + Label { + id: recordingDurationLabel + wrapMode: Text.Wrap + width: parent.width - ( 2 * Theme.horizontalPageMargin ) + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeMedium + anchors { + horizontalCenter: parent.horizontalCenter + } + } + + Button { + visible: recordingDone + anchors { + horizontalCenter: parent.horizontalCenter + } + text: qsTr("Use recording") + onClicked: { + attachmentOptionsFlickable.isNeeded = false; + attachmentPreviewRow.isVoiceNote = true; + attachmentPreviewRow.attachmentDescription = qsTr("Voice Note (%1)").arg(recordingDurationLabel.text); + attachmentPreviewRow.visible = true; + controlSendButton(); + voiceNoteOverlayLoader.active = false; + } + } + + } + } + +} + diff --git a/qml/components/WebPagePreview.qml b/qml/components/WebPagePreview.qml index b1c9275..a814b73 100644 --- a/qml/components/WebPagePreview.qml +++ b/qml/components/WebPagePreview.qml @@ -33,7 +33,11 @@ Column { spacing: Theme.paddingSmall - Component.onCompleted: { + Component.onCompleted: updatePhoto() + + onWebPageDataChanged: updatePhoto() + + function updatePhoto() { if (webPageData) { if (webPageData.photo) { // Check first which size fits best... @@ -134,9 +138,10 @@ Column { } BackgroundImage { + id: backgroundImage visible: hasImage && singleImage.status !== Image.Ready layer.enabled: webPagePreviewColumn.highlighted - layer.effect: PressEffect { source: singleImage } + layer.effect: PressEffect { source: backgroundImage } } } diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml index 2d0898d..70fbcce 100644 --- a/qml/pages/AboutPage.qml +++ b/qml/pages/AboutPage.qml @@ -59,7 +59,7 @@ Page { } Label { - text: "Fernschreiber 0.6" + text: "Fernschreiber 0.7" horizontalAlignment: Text.AlignHCenter font.pixelSize: Theme.fontSizeExtraLarge anchors { @@ -178,7 +178,7 @@ Page { } ProfileThumbnail { - photoData: aboutPage.userInformation.profile_photo.small + photoData: ((typeof aboutPage.userInformation.profile_photo !== "undefined") ? aboutPage.userInformation.profile_photo.small : {}) width: Theme.itemSizeExtraLarge height: Theme.itemSizeExtraLarge replacementStringHint: aboutPage.userInformation.first_name + " " + aboutPage.userInformation.last_name diff --git a/qml/pages/ChatPage.qml b/qml/pages/ChatPage.qml index 727b317..4db6a78 100644 --- a/qml/pages/ChatPage.qml +++ b/qml/pages/ChatPage.qml @@ -211,14 +211,21 @@ Page { attachmentPreviewRow.isPicture = false; attachmentPreviewRow.isVideo = false; attachmentPreviewRow.isDocument = false; + attachmentPreviewRow.isVoiceNote = false; + attachmentPreviewRow.isLocation = false; attachmentPreviewRow.fileProperties = {}; + attachmentPreviewRow.locationData = {}; + attachmentPreviewRow.attachmentDescription = ""; + fernschreiberUtils.stopGeoLocationUpdates(); } function controlSendButton() { if (newMessageTextField.text.length !== 0 || attachmentPreviewRow.isPicture || attachmentPreviewRow.isDocument - || attachmentPreviewRow.isVideo) { + || attachmentPreviewRow.isVideo + || attachmentPreviewRow.isVoiceNote + || attachmentPreviewRow.isLocation) { newMessageSendButton.enabled = true; } else { newMessageSendButton.enabled = false; @@ -239,14 +246,25 @@ Page { if (attachmentPreviewRow.isDocument) { tdLibWrapper.sendDocumentMessage(chatInformation.id, attachmentPreviewRow.fileProperties.filePath, newMessageTextField.text, newMessageColumn.replyToMessageId); } + if (attachmentPreviewRow.isVoiceNote) { + tdLibWrapper.sendVoiceNoteMessage(chatInformation.id, fernschreiberUtils.voiceNotePath(), newMessageTextField.text, newMessageColumn.replyToMessageId); + } + if (attachmentPreviewRow.isLocation) { + tdLibWrapper.sendLocationMessage(chatInformation.id, attachmentPreviewRow.locationData.latitude, attachmentPreviewRow.locationData.longitude, attachmentPreviewRow.locationData.horizontalAccuracy, newMessageColumn.replyToMessageId); + } clearAttachmentPreviewRow(); } else { tdLibWrapper.sendTextMessage(chatInformation.id, newMessageTextField.text, newMessageColumn.replyToMessageId); } + + if(appSettings.focusTextAreaAfterSend) { + lostFocusTimer.start(); + } } controlSendButton(); newMessageInReplyToRow.inReplyToMessage = null; newMessageColumn.editMessageId = "0"; + fernschreiberUtils.stopGeoLocationUpdates(); } function getWordBoundaries(text, cursorPosition) { @@ -376,6 +394,7 @@ Page { if (chatPage.canSendMessages) { tdLibWrapper.setChatDraftMessage(chatInformation.id, 0, newMessageColumn.replyToMessageId, newMessageTextField.text); } + fernschreiberUtils.stopGeoLocationUpdates(); tdLibWrapper.closeChat(chatInformation.id); } @@ -455,7 +474,7 @@ Page { Debug.log("[ChatPage] Received pinned message"); pinnedMessageItem.pinnedMessage = message; } - if (messageId === chatInformation.draft_message.reply_to_message_id) { + if (chatInformation.draft_message && messageId === chatInformation.draft_message.reply_to_message_id) { newMessageInReplyToRow.inReplyToMessage = message; } } @@ -623,7 +642,7 @@ Page { contentWidth: width PullDownMenu { - visible: chatInformation.id !== chatPage.myUserId && !stickerPickerLoader.active && !messageOverlayLoader.active + visible: chatInformation.id !== chatPage.myUserId && !stickerPickerLoader.active && !voiceNoteOverlayLoader.active && !messageOverlayLoader.active MenuItem { id: closeSecretChatMenuItem visible: chatPage.isSecretChat && chatPage.secretChatDetails.state["@type"] !== "secretChatStateClosed" @@ -730,7 +749,7 @@ Page { Rectangle { id: chatSecretBackground - color: Theme.overlayBackgroundColor + color: Theme.highlightBackgroundColor width: chatPage.isPortrait ? Theme.fontSizeLarge : Theme.fontSizeMedium height: width anchors.left: parent.left @@ -876,7 +895,7 @@ Page { id: chatView visible: !blurred - property bool blurred: messageOverlayLoader.item + property bool blurred: messageOverlayLoader.item || stickerPickerLoader.item || voiceNoteOverlayLoader.item anchors.fill: parent opacity: chatPage.loading ? 0 : 1 @@ -1010,6 +1029,7 @@ Page { chatId: chatModel.chatId myMessage: model.display messageId: model.message_id + messageIndex: model.index extraContentComponentName: chatView.contentComponentNames[model.content_type] || "" canReplyToMessage: chatPage.canSendMessages onReplyToMessage: { @@ -1110,7 +1130,7 @@ Page { Debug.log("Sticker picked: " + stickerId); tdLibWrapper.sendStickerMessage(chatInformation.id, stickerId); stickerPickerLoader.active = false; - attachmentOptionsRow.isNeeded = false; + attachmentOptionsFlickable.isNeeded = false; } } @@ -1134,6 +1154,20 @@ Page { } } + Loader { + id: voiceNoteOverlayLoader + active: false + asynchronous: true + width: parent.width + height: active ? parent.height : 0 + source: "../components/VoiceNoteOverlay.qml" + onActiveChanged: { + if (!active) { + fernschreiberUtils.stopRecordingVoiceNote(); + } + } + } + } Column { @@ -1141,7 +1175,6 @@ Page { spacing: Theme.paddingSmall topPadding: Theme.paddingSmall anchors.horizontalCenter: parent.horizontalCenter - clip: true visible: height > 0 width: parent.width - ( 2 * Theme.horizontalPageMargin ) height: isNeeded ? implicitHeight : 0 @@ -1173,90 +1206,146 @@ Page { visible: false } - Row { - id: attachmentOptionsRow + Flickable { + id: attachmentOptionsFlickable + property bool isNeeded: false - visible: height > 0 - height: isNeeded ? implicitHeight : 0 - anchors.right: parent.right - width: parent.width - layoutDirection: Qt.RightToLeft - spacing: Theme.paddingMedium - clip: true + width: chatPage.width + x: -Theme.horizontalPageMargin + height: isNeeded ? attachmentOptionsRow.height : 0 Behavior on height { SmoothedAnimation { duration: 200 } } - IconButton { - visible: chatPage.hasSendPrivilege("can_send_media_messages") - icon.source: "image://theme/icon-m-image" - onClicked: { - var picker = pageStack.push("Sailfish.Pickers.ImagePickerPage", { - allowedOrientations: chatPage.allowedOrientations - }) - picker.selectedContentPropertiesChanged.connect(function(){ - attachmentOptionsRow.isNeeded = false; - Debug.log("Selected document: ", picker.selectedContentProperties.filePath ); - attachmentPreviewRow.fileProperties = picker.selectedContentProperties; - attachmentPreviewRow.isPicture = true; + visible: height > 0 + contentHeight: attachmentOptionsRow.height + contentWidth: Math.max(width, attachmentOptionsRow.width) + property bool fadeRight: (attachmentOptionsRow.width-contentX) > width + property bool fadeLeft: !fadeRight && contentX > 0 + layer.enabled: fadeRight || fadeLeft + layer.effect: OpacityRampEffectBase { + direction: attachmentOptionsFlickable.fadeRight ? OpacityRamp.LeftToRight : OpacityRamp.RightToLeft + source: attachmentOptionsFlickable + slope: 1 + 6 * (chatPage.width) / Screen.width + offset: 1 - 1 / slope + } + + + Row { + id: attachmentOptionsRow + + height: attachImageIconButton.height + + anchors.right: parent.right + layoutDirection: Qt.RightToLeft + spacing: Theme.paddingMedium + leftPadding: Theme.horizontalPageMargin + rightPadding: Theme.horizontalPageMargin + + IconButton { + id: attachImageIconButton + visible: chatPage.hasSendPrivilege("can_send_media_messages") + icon.source: "image://theme/icon-m-image" + onClicked: { + var picker = pageStack.push("Sailfish.Pickers.ImagePickerPage", { + allowedOrientations: chatPage.allowedOrientations + }) + picker.selectedContentPropertiesChanged.connect(function(){ + attachmentOptionsFlickable.isNeeded = false; + Debug.log("Selected document: ", picker.selectedContentProperties.filePath ); + attachmentPreviewRow.fileProperties = picker.selectedContentProperties; + attachmentPreviewRow.isPicture = true; + attachmentPreviewRow.visible = true; + controlSendButton(); + }) + } + } + IconButton { + visible: chatPage.hasSendPrivilege("can_send_media_messages") + icon.source: "image://theme/icon-m-video" + onClicked: { + var picker = pageStack.push("Sailfish.Pickers.VideoPickerPage", { + allowedOrientations: chatPage.allowedOrientations + }) + picker.selectedContentPropertiesChanged.connect(function(){ + attachmentOptionsFlickable.isNeeded = false; + Debug.log("Selected video: ", picker.selectedContentProperties.filePath ); + attachmentPreviewRow.fileProperties = picker.selectedContentProperties; + attachmentPreviewRow.isVideo = true; + attachmentPreviewRow.visible = true; + controlSendButton(); + }) + } + } + IconButton { + visible: chatPage.hasSendPrivilege("can_send_media_messages") + icon.source: "image://theme/icon-m-mic" + icon.sourceSize { + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + } + highlighted: down || voiceNoteOverlayLoader.active + onClicked: { + voiceNoteOverlayLoader.active = !voiceNoteOverlayLoader.active; + stickerPickerLoader.active = false; + } + } + IconButton { + visible: chatPage.hasSendPrivilege("can_send_media_messages") + icon.source: "image://theme/icon-m-document" + onClicked: { + var picker = pageStack.push("Sailfish.Pickers.FilePickerPage", { + allowedOrientations: chatPage.allowedOrientations + }) + picker.selectedContentPropertiesChanged.connect(function(){ + attachmentOptionsFlickable.isNeeded = false; + Debug.log("Selected document: ", picker.selectedContentProperties.filePath ); + attachmentPreviewRow.fileProperties = picker.selectedContentProperties; + attachmentPreviewRow.isDocument = true; + attachmentPreviewRow.visible = true; + controlSendButton(); + }) + } + } + IconButton { + visible: chatPage.hasSendPrivilege("can_send_other_messages") + icon.source: "../../images/icon-m-sticker.svg" + icon.sourceSize { + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + } + highlighted: down || stickerPickerLoader.active + onClicked: { + stickerPickerLoader.active = !stickerPickerLoader.active; + voiceNoteOverlayLoader.active = false; + } + } + IconButton { + visible: !(chatPage.isPrivateChat || chatPage.isSecretChat) && chatPage.hasSendPrivilege("can_send_polls") + icon.source: "image://theme/icon-m-question" + onClicked: { + pageStack.push(Qt.resolvedUrl("../pages/PollCreationPage.qml"), { "chatId" : chatInformation.id, groupName: chatInformation.title}); + attachmentOptionsFlickable.isNeeded = false; + } + } + IconButton { + visible: fernschreiberUtils.supportsGeoLocation() && newMessageTextField.text === "" + icon.source: "image://theme/icon-m-location" + icon.sourceSize { + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + } + onClicked: { + fernschreiberUtils.startGeoLocationUpdates(); + attachmentOptionsFlickable.isNeeded = false; + attachmentPreviewRow.isLocation = true; + attachmentPreviewRow.attachmentDescription = qsTr("Location: Obtaining position..."); attachmentPreviewRow.visible = true; controlSendButton(); - }) - } - } - IconButton { - visible: chatPage.hasSendPrivilege("can_send_media_messages") - icon.source: "image://theme/icon-m-video" - onClicked: { - var picker = pageStack.push("Sailfish.Pickers.VideoPickerPage", { - allowedOrientations: chatPage.allowedOrientations - }) - picker.selectedContentPropertiesChanged.connect(function(){ - attachmentOptionsRow.isNeeded = false; - Debug.log("Selected video: ", picker.selectedContentProperties.filePath ); - attachmentPreviewRow.fileProperties = picker.selectedContentProperties; - attachmentPreviewRow.isVideo = true; - attachmentPreviewRow.visible = true; - controlSendButton(); - }) - } - } - IconButton { - visible: chatPage.hasSendPrivilege("can_send_media_messages") - icon.source: "image://theme/icon-m-document" - onClicked: { - var picker = pageStack.push("Sailfish.Pickers.FilePickerPage", { - allowedOrientations: chatPage.allowedOrientations - }) - picker.selectedContentPropertiesChanged.connect(function(){ - attachmentOptionsRow.isNeeded = false; - Debug.log("Selected document: ", picker.selectedContentProperties.filePath ); - attachmentPreviewRow.fileProperties = picker.selectedContentProperties; - attachmentPreviewRow.isDocument = true; - attachmentPreviewRow.visible = true; - controlSendButton(); - }) - } - } - IconButton { - visible: chatPage.hasSendPrivilege("can_send_other_messages") - icon.source: "../../images/icon-m-sticker.svg" - icon.sourceSize { - width: Theme.iconSizeMedium - height: Theme.iconSizeMedium - } - highlighted: down || stickerPickerLoader.active - onClicked: { - stickerPickerLoader.active = !stickerPickerLoader.active; - } - } - IconButton { - visible: !(chatPage.isPrivateChat || chatPage.isSecretChat) && chatPage.hasSendPrivilege("can_send_polls") - icon.source: "image://theme/icon-m-question" - onClicked: { - pageStack.push(Qt.resolvedUrl("../pages/PollCreationPage.qml"), { "chatId" : chatInformation.id, groupName: chatInformation.title}); - attachmentOptionsRow.isNeeded = false; + } } } + } + Row { id: attachmentPreviewRow visible: false @@ -1268,7 +1357,21 @@ Page { property bool isPicture: false; property bool isVideo: false; property bool isDocument: false; + property bool isVoiceNote: false; + property bool isLocation: false; + property var locationData: ({}); property var fileProperties:({}); + property string attachmentDescription: ""; + + Connections { + target: fernschreiberUtils + onNewPositionInformation: { + attachmentPreviewRow.locationData = positionInformation; + if (attachmentPreviewRow.isLocation) { + attachmentPreviewRow.attachmentDescription = qsTr("Location (%1/%2)").arg(attachmentPreviewRow.locationData.latitude).arg(attachmentPreviewRow.locationData.longitude); + } + } + } IconButton { id: removeAttachmentsIconButton @@ -1295,13 +1398,13 @@ Page { Label { id: attachmentPreviewText font.pixelSize: Theme.fontSizeSmall - text: typeof attachmentPreviewRow.fileProperties !== "undefined" ? attachmentPreviewRow.fileProperties.fileName || "" : ""; + text: ( attachmentPreviewRow.isVoiceNote || attachmentPreviewRow.isLocation ) ? attachmentPreviewRow.attachmentDescription : ( typeof attachmentPreviewRow.fileProperties !== "undefined" ? attachmentPreviewRow.fileProperties.fileName || "" : "" ); anchors.verticalCenter: parent.verticalCenter maximumLineCount: 1 truncationMode: TruncationMode.Fade color: Theme.secondaryColor - visible: attachmentPreviewRow.isDocument + visible: attachmentPreviewRow.isDocument || attachmentPreviewRow.isVoiceNote || attachmentPreviewRow.isLocation } } @@ -1503,11 +1606,14 @@ Page { labelVisible: false textLeftMargin: 0 textTopMargin: 0 + enabled: !attachmentPreviewRow.isLocation EnterKey.onClicked: { if (appSettings.sendByEnter) { sendMessage(); newMessageTextField.text = ""; - newMessageTextField.focus = false; + if(!appSettings.focusTextAreaAfterSend) { + newMessageTextField.focus = false; + } } } @@ -1522,16 +1628,17 @@ Page { IconButton { id: attachmentIconButton - icon.source: "image://theme/icon-m-attach?" + (attachmentOptionsRow.isNeeded ? Theme.highlightColor : Theme.primaryColor) + icon.source: "image://theme/icon-m-attach?" + (attachmentOptionsFlickable.isNeeded ? Theme.highlightColor : Theme.primaryColor) anchors.bottom: parent.bottom anchors.bottomMargin: Theme.paddingSmall enabled: !attachmentPreviewRow.visible onClicked: { - if (attachmentOptionsRow.isNeeded) { - attachmentOptionsRow.isNeeded = false; + if (attachmentOptionsFlickable.isNeeded) { + attachmentOptionsFlickable.isNeeded = false; stickerPickerLoader.active = false; + voiceNoteOverlayLoader.active = false; } else { - attachmentOptionsRow.isNeeded = true; + attachmentOptionsFlickable.isNeeded = true; } } } @@ -1546,7 +1653,9 @@ Page { onClicked: { sendMessage(); newMessageTextField.text = ""; - newMessageTextField.focus = false; + if(!appSettings.focusTextAreaAfterSend) { + newMessageTextField.focus = false; + } } } } diff --git a/qml/pages/CoverPage.qml b/qml/pages/CoverPage.qml index 9e5df33..1a29b8d 100644 --- a/qml/pages/CoverPage.qml +++ b/qml/pages/CoverPage.qml @@ -67,7 +67,7 @@ CoverBackground { coverPage.authenticated = (tdLibWrapper.getAuthorizationState() === TelegramAPI.AuthorizationReady); coverPage.connectionState = tdLibWrapper.getConnectionState(); coverPage.unreadMessages = tdLibWrapper.getUnreadMessageInformation().unread_count || 0; - coverPage.unreadChats = tdLibWrapper.getUnreadChatInformation().unread_count; + coverPage.unreadChats = tdLibWrapper.getUnreadChatInformation().unread_count || 0; setUnreadInfoText(); } @@ -91,6 +91,15 @@ CoverBackground { } } + Connections { + target: chatListModel + onUnreadStateChanged: { + coverPage.unreadMessages = unreadMessagesCount; + coverPage.unreadChats = unreadChatsCount; + setUnreadInfoText(); + } + } + BackgroundImage { id: backgroundImage width: parent.height - Theme.paddingLarge diff --git a/qml/pages/OverviewPage.qml b/qml/pages/OverviewPage.qml index 1fb16ea..17c4f32 100644 --- a/qml/pages/OverviewPage.qml +++ b/qml/pages/OverviewPage.qml @@ -67,6 +67,13 @@ Page { overviewPage.chatListCreated = true; chatListView.scrollToTop(); updateSecondaryContentTimer.start(); + var remainingInteractionHints = appSettings.remainingInteractionHints; + Debug.log("Remaining interaction hints: " + remainingInteractionHints); + if (remainingInteractionHints > 0) { + interactionHintTimer.start(); + titleInteractionHint.opacity = 1.0; + appSettings.remainingInteractionHints = remainingInteractionHints - 1; + } } } @@ -81,6 +88,7 @@ Page { id: updateSecondaryContentTimer interval: 600 onTriggered: { + chatListModel.calculateUnreadState(); tdLibWrapper.getRecentStickers(); tdLibWrapper.getInstalledStickerSets(); tdLibWrapper.getContacts(); @@ -178,9 +186,8 @@ Page { function resetFocus() { if (chatSearchField.text === "") { - chatSearchField.visible = false; - pageHeader.visible = true; - searchChatButton.visible = overviewPage.connectionState === TelegramAPI.ConnectionReady; + chatSearchField.opacity = 0.0; + pageHeader.opacity = 1.0; } chatSearchField.focus = false; overviewPage.focus = true; @@ -202,11 +209,15 @@ Page { onChatLastMessageUpdated: { if (!overviewPage.chatListCreated) { chatListCreatedTimer.restart(); + } else { + chatListModel.calculateUnreadState(); } } onChatOrderUpdated: { if (!overviewPage.chatListCreated) { chatListCreatedTimer.restart(); + } else { + chatListModel.calculateUnreadState(); } } onChatsReceived: { @@ -268,9 +279,12 @@ Page { } } - Row { - id: headerRow - width: parent.width - Theme.horizontalPageMargin + PageHeader { + id: pageHeader + title: qsTr("Fernschreiber") + leftMargin: Theme.itemSizeMedium + visible: opacity > 0 + Behavior on opacity { FadeAnimation {} } GlassItem { id: pageStatus @@ -282,63 +296,45 @@ Page { cache: false } - PageHeader { - id: pageHeader - title: qsTr("Fernschreiber") - width: visible ? ( parent.width - pageStatus.width - searchChatButton.width ) : 0 - opacity: visible ? 1 : 0 - Behavior on opacity { FadeAnimation {} } - } - - IconButton { - id: searchChatButton - width: visible ? height : 0 - opacity: visible ? 1 : 0 - Behavior on opacity { NumberAnimation {} } - anchors.verticalCenter: parent.verticalCenter - icon { - source: "image://theme/icon-m-search?" + Theme.highlightColor - asynchronous: true - } - visible: overviewPage.connectionState === TelegramAPI.ConnectionReady + MouseArea { + anchors.fill: parent onClicked: { chatSearchField.focus = true; - chatSearchField.visible = true; - pageHeader.visible = false; - searchChatButton.visible = false; - } - } - - SearchField { - id: chatSearchField - visible: false - opacity: visible ? 1 : 0 - Behavior on opacity { FadeAnimation {} } - width: visible ? ( parent.width - pageStatus.width ) : 0 - height: pageHeader.height - placeholderText: qsTr("Filter your chats...") - canHide: text === "" - - onTextChanged: { - searchChatTimer.restart(); - } - - onHideClicked: { - resetFocus(); - } - - EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: { - resetFocus(); + chatSearchField.opacity = 1.0; + pageHeader.opacity = 0.0; } } } + SearchField { + id: chatSearchField + visible: opacity > 0 + opacity: 0 + Behavior on opacity { FadeAnimation {} } + width: parent.width + height: pageHeader.height + placeholderText: qsTr("Filter your chats...") + canHide: text === "" + + onTextChanged: { + searchChatTimer.restart(); + } + + onHideClicked: { + resetFocus(); + } + + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: { + resetFocus(); + } + } + SilicaListView { id: chatListView anchors { - top: headerRow.bottom + top: pageHeader.bottom bottom: parent.bottom left: parent.left right: parent.right @@ -360,7 +356,8 @@ Page { ViewPlaceholder { enabled: chatListView.count === 0 - text: qsTr("You don't have any chats yet.") + text: chatSearchField.text === "" ? qsTr("You don't have any chats yet.") : qsTr("No matching chats found.") + hintText: qsTr("You can search public chats or create a new chat via the pull-down menu.") } VerticalScrollDecorator {} @@ -403,4 +400,24 @@ Page { } } } + + Timer { + id: interactionHintTimer + running: false + interval: 4000 + onTriggered: { + titleInteractionHint.opacity = 0.0; + } + } + + InteractionHintLabel { + id: titleInteractionHint + text: qsTr("Tap on the title bar to filter your chats") + visible: opacity > 0 + invert: true + anchors.fill: parent + Behavior on opacity { FadeAnimation {} } + opacity: 0 + } + } diff --git a/qml/pages/SearchChatsPage.qml b/qml/pages/SearchChatsPage.qml index 82fe385..3e0571b 100644 --- a/qml/pages/SearchChatsPage.qml +++ b/qml/pages/SearchChatsPage.qml @@ -175,13 +175,13 @@ Page { onBasicGroupFullInfoUpdated: { if (foundChatListDelegate.isBasicGroup && groupId.toString() === foundChatListDelegate.foundChatInformation.type.basic_group_id.toString()) { - foundChatListItem.secondaryText.text = qsTr("%1 members").arg(Number(groupFullInfo.members.length).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 members", "", groupFullInfo.members.length).arg(Number(groupFullInfo.members.length).toLocaleString(Qt.locale(), "f", 0)); foundChatListItem.tertiaryText.text = Emoji.emojify(groupFullInfo.description, foundChatListItem.tertiaryText.font.pixelSize, "../js/emoji/"); } } onBasicGroupFullInfoReceived: { if (foundChatListDelegate.isBasicGroup && groupId.toString() === foundChatListDelegate.foundChatInformation.type.basic_group_id.toString()) { - foundChatListItem.secondaryText.text = qsTr("%1 members").arg(Number(groupFullInfo.members.length).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 members", "", groupFullInfo.members.length).arg(Number(groupFullInfo.members.length).toLocaleString(Qt.locale(), "f", 0)); foundChatListItem.tertiaryText.text = Emoji.emojify(groupFullInfo.description, foundChatListItem.tertiaryText.font.pixelSize, "../js/emoji/"); } } @@ -189,9 +189,9 @@ Page { onSupergroupFullInfoUpdated: { if (foundChatListDelegate.isSupergroup && groupId.toString() === foundChatListDelegate.foundChatInformation.type.supergroup_id.toString()) { if (foundChatListDelegate.relatedInformation.is_channel) { - foundChatListItem.secondaryText.text = qsTr("%1 subscribers").arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 subscribers", "", groupFullInfo.member_count).arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); } else { - foundChatListItem.secondaryText.text = qsTr("%1 members").arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 members", "", groupFullInfo.member_count).arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); } foundChatListItem.tertiaryText.text = Emoji.emojify(groupFullInfo.description, foundChatListItem.tertiaryText.font.pixelSize, "../js/emoji/"); } @@ -199,9 +199,9 @@ Page { onSupergroupFullInfoReceived: { if (foundChatListDelegate.isSupergroup && groupId.toString() === foundChatListDelegate.foundChatInformation.type.supergroup_id.toString()) { if (foundChatListDelegate.relatedInformation.is_channel) { - foundChatListItem.secondaryText.text = qsTr("%1 subscribers").arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 subscribers", "", groupFullInfo.member_count).arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); } else { - foundChatListItem.secondaryText.text = qsTr("%1 members").arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); + foundChatListItem.secondaryText.text = qsTr("%1 members", "", groupFullInfo.member_count).arg(Number(groupFullInfo.member_count).toLocaleString(Qt.locale(), "f", 0)); } foundChatListItem.tertiaryText.text = Emoji.emojify(groupFullInfo.description, foundChatListItem.tertiaryText.font.pixelSize, "../js/emoji/"); } diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index e9abe19..b91691e 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -52,6 +52,16 @@ Page { } } + TextSwitch { + checked: appSettings.focusTextAreaAfterSend + text: qsTr("Focus text input area after send") + description: qsTr("Focus the text input area after sending a message") + automaticCheck: false + onClicked: { + appSettings.focusTextAreaAfterSend = !checked + } + } + TextSwitch { checked: appSettings.useOpenWith text: qsTr("Open-with menu integration") @@ -162,6 +172,16 @@ Page { } } + TextSwitch { + checked: appSettings.onlineOnlyMode + text: qsTr("Enable online-only mode") + description: qsTr("Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect.") + automaticCheck: false + onClicked: { + appSettings.onlineOnlyMode = !checked + } + } + Item { width: 1 height: Theme.paddingLarge // Some space at the bottom diff --git a/rpm/harbour-fernschreiber.changes b/rpm/harbour-fernschreiber.changes index 4af99be..7df474a 100644 --- a/rpm/harbour-fernschreiber.changes +++ b/rpm/harbour-fernschreiber.changes @@ -12,6 +12,38 @@ # * date Author's Name version-release # - Summary of changes +* Wed Jan 6 2021 Sebastian J. Wolf 0.6.1 +- Show indicator for pinned chats +- Use highlightBackgroundColor and align size for all chat attributes (unread messages, secret chat, pinned chat) +- Fix: Chat list order of sequence was wrong in some circumstances +- Fix: Make video (fullscreen) page work again +- Fix: Add build requirements for Qt Multimedia and Qt Positioning +- Updated translations for Finnish and Swedish - thanks to jorm1s and eson57 + +* Tue Jan 5 2021 Sebastian J. Wolf 0.6 +- Filter chat list (tap on title bar to open search field) - thanks to the entire dev team for the great discussion and contributions to the layout :) +- Search for public chats (see pulley menu on overview page) +- Search for chat messages (see pulley menu on chat page) +- Support sending voice notes +- Support sending locations +- Add message draft support - thanks to jgibbon +- Basic bot messages support (reply markup) - thanks to jgibbon +- Add 'Mark chat as read/unread' option +- Add download option to audio messages (voice notes, music...) +- Introduce proper text if other people added/removed a person from a chat +- Tweaks to poll layout - thanks to monich +- New option to keep message input focused after message was sent - thanks to jgibbon +- Send message button now removed if Send-message-by-enter option is enabled (and no attachment is set) - thanks to santhoshmanikandan +- Performance and code optimizations (architecture, startup, JS components) - thanks to monich and jgibbon +- Improve readability in light ambiences +- Upgrade to TDLib 1.7 +- Fix: Only show reply to option for messages that can be replied to - thanks to monich +- Fix: Emoji positioning in multi-line texts - thanks to monich +- Fix: Left-over @-mention notifications when all messages in chat are read +- Fix: Occasional crashes on opening poll context menu - thanks to monich +- Fix: Don't display in-reply-to section if message wasn't found +- Several translations updated - thanks to all contributors + * Fri Dec 4 2020 Sebastian J. Wolf 0.5.1 - Add verification badge to verified chats - thanks to monich - Improve preview of wide images - thanks to monich diff --git a/rpm/harbour-fernschreiber.spec b/rpm/harbour-fernschreiber.spec index 9d2da62..d1f9180 100644 --- a/rpm/harbour-fernschreiber.spec +++ b/rpm/harbour-fernschreiber.spec @@ -11,7 +11,7 @@ Name: harbour-fernschreiber # << macros Summary: Fernschreiber is a Telegram client for Sailfish OS -Version: 0.6 +Version: 0.7 Release: 1 Group: Qt/Qt License: LICENSE @@ -25,6 +25,8 @@ BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5Quick) BuildRequires: pkgconfig(Qt5DBus) BuildRequires: pkgconfig(Qt5Sql) +BuildRequires: pkgconfig(Qt5Multimedia) +BuildRequires: pkgconfig(Qt5Positioning) BuildRequires: pkgconfig(nemonotifications-qt5) BuildRequires: pkgconfig(ngf-qt5) BuildRequires: desktop-file-utils diff --git a/rpm/harbour-fernschreiber.yaml b/rpm/harbour-fernschreiber.yaml index a9e8967..935a55c 100644 --- a/rpm/harbour-fernschreiber.yaml +++ b/rpm/harbour-fernschreiber.yaml @@ -1,6 +1,6 @@ Name: harbour-fernschreiber Summary: Fernschreiber is a Telegram client for Sailfish OS -Version: 0.6 +Version: 0.7 Release: 1 # The contents of the Group field should be one of the groups listed here: # https://github.com/mer-tools/spectacle/blob/master/data/GROUPS @@ -25,6 +25,8 @@ PkgConfigBR: - Qt5Quick - Qt5DBus - Qt5Sql + - Qt5Multimedia + - Qt5Positioning - nemonotifications-qt5 - ngf-qt5 diff --git a/src/appsettings.cpp b/src/appsettings.cpp index 497578f..d9239a9 100644 --- a/src/appsettings.cpp +++ b/src/appsettings.cpp @@ -22,12 +22,15 @@ namespace { const QString KEY_SEND_BY_ENTER("sendByEnter"); + const QString KEY_FOCUS_TEXTAREA_AFTER_SEND("focusTextAreaAfterSend"); const QString KEY_USE_OPEN_WITH("useOpenWith"); const QString KEY_SHOW_STICKERS_AS_IMAGES("showStickersAsImages"); const QString KEY_ANIMATE_STICKERS("animateStickers"); const QString KEY_NOTIFICATION_TURNS_DISPLAY_ON("notificationTurnsDisplayOn"); const QString KEY_NOTIFICATION_FEEDBACK("notificationFeedback"); const QString KEY_STORAGE_OPTIMIZER("storageOptimizer"); + const QString KEY_REMAINING_INTERACTION_HINTS("remainingInteractionHints"); + const QString KEY_ONLINE_ONLY_MODE("onlineOnlyMode"); } AppSettings::AppSettings(QObject *parent) : QObject(parent), settings("harbour-fernschreiber", "settings") @@ -48,6 +51,20 @@ void AppSettings::setSendByEnter(bool sendByEnter) } } +bool AppSettings::getFocusTextAreaAfterSend() const +{ + return settings.value(KEY_FOCUS_TEXTAREA_AFTER_SEND, false).toBool(); +} + +void AppSettings::setFocusTextAreaAfterSend(bool focusTextAreaAfterSend) +{ + if (getFocusTextAreaAfterSend() != focusTextAreaAfterSend) { + LOG(KEY_FOCUS_TEXTAREA_AFTER_SEND << focusTextAreaAfterSend); + settings.setValue(KEY_FOCUS_TEXTAREA_AFTER_SEND, focusTextAreaAfterSend); + emit focusTextAreaAfterSendChanged(); + } +} + bool AppSettings::getUseOpenWith() const { return settings.value(KEY_USE_OPEN_WITH, true).toBool(); @@ -131,3 +148,31 @@ void AppSettings::setStorageOptimizer(bool enable) emit storageOptimizerChanged(); } } + +int AppSettings::remainingInteractionHints() const +{ + return settings.value(KEY_REMAINING_INTERACTION_HINTS, 3).toInt(); +} + +void AppSettings::setRemainingInteractionHints(int remainingHints) +{ + if (remainingInteractionHints() != remainingHints) { + LOG(KEY_REMAINING_INTERACTION_HINTS << remainingHints); + settings.setValue(KEY_REMAINING_INTERACTION_HINTS, remainingHints); + emit remainingInteractionHintsChanged(); + } +} + +bool AppSettings::onlineOnlyMode() const +{ + return settings.value(KEY_ONLINE_ONLY_MODE, false).toBool(); +} + +void AppSettings::setOnlineOnlyMode(bool enable) +{ + if (onlineOnlyMode() != enable) { + LOG(KEY_ONLINE_ONLY_MODE << enable); + settings.setValue(KEY_ONLINE_ONLY_MODE, enable); + emit onlineOnlyModeChanged(); + } +} diff --git a/src/appsettings.h b/src/appsettings.h index bb63cdf..373d3df 100644 --- a/src/appsettings.h +++ b/src/appsettings.h @@ -24,12 +24,15 @@ class AppSettings : public QObject { Q_OBJECT Q_PROPERTY(bool sendByEnter READ getSendByEnter WRITE setSendByEnter NOTIFY sendByEnterChanged) + Q_PROPERTY(bool focusTextAreaAfterSend READ getFocusTextAreaAfterSend WRITE setFocusTextAreaAfterSend NOTIFY focusTextAreaAfterSendChanged) Q_PROPERTY(bool useOpenWith READ getUseOpenWith WRITE setUseOpenWith NOTIFY useOpenWithChanged) Q_PROPERTY(bool showStickersAsImages READ showStickersAsImages WRITE setShowStickersAsImages NOTIFY showStickersAsImagesChanged) Q_PROPERTY(bool animateStickers READ animateStickers WRITE setAnimateStickers NOTIFY animateStickersChanged) Q_PROPERTY(bool notificationTurnsDisplayOn READ notificationTurnsDisplayOn WRITE setNotificationTurnsDisplayOn NOTIFY notificationTurnsDisplayOnChanged) Q_PROPERTY(NotificationFeedback notificationFeedback READ notificationFeedback WRITE setNotificationFeedback NOTIFY notificationFeedbackChanged) Q_PROPERTY(bool storageOptimizer READ storageOptimizer WRITE setStorageOptimizer NOTIFY storageOptimizerChanged) + Q_PROPERTY(int remainingInteractionHints READ remainingInteractionHints WRITE setRemainingInteractionHints NOTIFY remainingInteractionHintsChanged) + Q_PROPERTY(bool onlineOnlyMode READ onlineOnlyMode WRITE setOnlineOnlyMode NOTIFY onlineOnlyModeChanged) public: enum NotificationFeedback { @@ -45,6 +48,9 @@ public: bool getSendByEnter() const; void setSendByEnter(bool sendByEnter); + bool getFocusTextAreaAfterSend() const; + void setFocusTextAreaAfterSend(bool focusTextAreaAfterSend); + bool getUseOpenWith() const; void setUseOpenWith(bool useOpenWith); @@ -63,14 +69,23 @@ public: bool storageOptimizer() const; void setStorageOptimizer(bool enable); + int remainingInteractionHints() const; + void setRemainingInteractionHints(int remainingHints); + + bool onlineOnlyMode() const; + void setOnlineOnlyMode(bool enable); + signals: void sendByEnterChanged(); + void focusTextAreaAfterSendChanged(); void useOpenWithChanged(); void showStickersAsImagesChanged(); void animateStickersChanged(); void notificationTurnsDisplayOnChanged(); void notificationFeedbackChanged(); void storageOptimizerChanged(); + void remainingInteractionHintsChanged(); + void onlineOnlyModeChanged(); private: QSettings settings; diff --git a/src/chatlistmodel.cpp b/src/chatlistmodel.cpp index 7d2ed45..1a796be 100644 --- a/src/chatlistmodel.cpp +++ b/src/chatlistmodel.cpp @@ -19,6 +19,7 @@ #include "chatlistmodel.h" #include "fernschreiberutils.h" +#include #define DEBUG_MODULE ChatListModel #include "debuglog.h" @@ -48,6 +49,7 @@ namespace { const QString IS_CHANNEL("is_channel"); const QString IS_VERIFIED("is_verified"); const QString IS_MARKED_AS_UNREAD("is_marked_as_unread"); + const QString IS_PINNED("is_pinned"); const QString PINNED_MESSAGE_ID("pinned_message_id"); const QString _TYPE("@type"); const QString SECRET_CHAT_ID("secret_chat_id"); @@ -77,6 +79,7 @@ public: bool isChannel() const; bool isHidden() const; bool isMarkedAsUnread() const; + bool isPinned() const; bool updateUnreadCount(int unreadCount); bool updateLastReadInboxMessageId(qlonglong messageId); QVector updateLastMessage(const QVariantMap &message); @@ -272,6 +275,11 @@ bool ChatListModel::ChatData::isMarkedAsUnread() const return chatData.value(IS_MARKED_AS_UNREAD).toBool(); } +bool ChatListModel::ChatData::isPinned() const +{ + return chatData.value(IS_PINNED).toBool(); +} + bool ChatListModel::ChatData::updateUnreadCount(int count) { const int prevUnreadCount(unreadCount()); @@ -346,9 +354,10 @@ QVector ChatListModel::ChatData::updateSecretChat(const QVariantMap &secret return changedRoles; } -ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper) : showHiddenChats(false) +ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSettings) : showHiddenChats(false) { this->tdLibWrapper = tdLibWrapper; + this->appSettings = appSettings; connect(tdLibWrapper, SIGNAL(newChatDiscovered(QString, QVariantMap)), this, SLOT(handleChatDiscovered(QString, QVariantMap))); connect(tdLibWrapper, SIGNAL(chatLastMessageUpdated(QString, QString, QVariantMap)), this, SLOT(handleChatLastMessageUpdated(QString, QString, QVariantMap))); connect(tdLibWrapper, SIGNAL(chatOrderUpdated(QString, QString)), this, SLOT(handleChatOrderUpdated(QString, QString))); @@ -364,6 +373,7 @@ ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper) : showHiddenChats(false connect(tdLibWrapper, SIGNAL(secretChatReceived(qlonglong, QVariantMap)), this, SLOT(handleSecretChatUpdated(qlonglong, QVariantMap))); connect(tdLibWrapper, SIGNAL(chatTitleUpdated(QString, QString)), this, SLOT(handleChatTitleUpdated(QString, QString))); connect(tdLibWrapper, SIGNAL(chatIsMarkedAsUnreadUpdated(qlonglong, bool)), this, SLOT(handleChatIsMarkedAsUnreadUpdated(qlonglong, bool))); + connect(tdLibWrapper, SIGNAL(chatPinnedUpdated(qlonglong, bool)), this, SLOT(handleChatPinnedUpdated(qlonglong, bool))); connect(tdLibWrapper, SIGNAL(chatDraftMessageUpdated(qlonglong, QVariantMap, QString)), this, SLOT(handleChatDraftMessageUpdated(qlonglong, QVariantMap, QString))); // Don't start the timer until we have at least one chat @@ -405,6 +415,7 @@ QHash ChatListModel::roleNames() const roles.insert(ChatListModel::RoleIsVerified, "is_verified"); roles.insert(ChatListModel::RoleIsChannel, "is_channel"); roles.insert(ChatListModel::RoleIsMarkedAsUnread, "is_marked_as_unread"); + roles.insert(ChatListModel::RoleIsPinned, "is_pinned"); roles.insert(ChatListModel::RoleFilter, "filter"); roles.insert(ChatListModel::RoleDraftMessageDate, "draft_message_date"); roles.insert(ChatListModel::RoleDraftMessageText, "draft_message_text"); @@ -438,6 +449,7 @@ QVariant ChatListModel::data(const QModelIndex &index, int role) const case ChatListModel::RoleIsVerified: return data->verified; case ChatListModel::RoleIsChannel: return data->isChannel(); case ChatListModel::RoleIsMarkedAsUnread: return data->isMarkedAsUnread(); + case ChatListModel::RoleIsPinned: return data->isPinned(); case ChatListModel::RoleFilter: return QString(data->title() + " " + data->senderMessageText()).trimmed(); case ChatListModel::RoleDraftMessageText: return data->draftMessageText(); case ChatListModel::RoleDraftMessageDate: return data->draftMessageDate(); @@ -515,6 +527,26 @@ int ChatListModel::updateChatOrder(int chatIndex) return newIndex; } +void ChatListModel::calculateUnreadState() +{ + if (this->appSettings->onlineOnlyMode()) { + LOG("Online-only mode: Calculating unread state on my own..."); + int unreadMessages = 0; + int unreadChats = 0; + QListIterator chatIterator(this->chatList); + while (chatIterator.hasNext()) { + ChatData *currentChat = chatIterator.next(); + int unreadCount = currentChat->unreadCount(); + if (unreadCount > 0) { + unreadChats++; + unreadMessages += unreadCount; + } + } + LOG("Online-only mode: New unread state:" << unreadMessages << unreadChats); + emit unreadStateChanged(unreadMessages, unreadChats); + } +} + void ChatListModel::addVisibleChat(ChatData *chat) { const int n = chatList.size(); @@ -715,6 +747,7 @@ void ChatListModel::handleChatReadInboxUpdated(const QString &id, const QString } const QModelIndex modelIndex(index(chatIndex)); emit dataChanged(modelIndex, modelIndex, changedRoles); + this->calculateUnreadState(); } else { ChatData *chat = hiddenChats.value(chatId); if (chat) { @@ -856,6 +889,26 @@ void ChatListModel::handleChatTitleUpdated(const QString &chatId, const QString } } +void ChatListModel::handleChatPinnedUpdated(qlonglong chatId, bool chatIsPinned) +{ + if (chatIndexMap.contains(chatId)) { + LOG("Updating chat is pinned for" << chatId << chatIsPinned); + const int chatIndex = chatIndexMap.value(chatId); + ChatData *chat = chatList.at(chatIndex); + chat->chatData.insert(IS_PINNED, chatIsPinned); + QVector changedRoles; + changedRoles.append(ChatListModel::RoleIsPinned); + const QModelIndex modelIndex(index(chatIndex)); + emit dataChanged(modelIndex, modelIndex, changedRoles); + } else { + ChatData *chat = hiddenChats.value(chatId); + if (chat) { + LOG("Updating chat is pinned for hidden chat" << chatId); + chat->chatData.insert(IS_PINNED, chatIsPinned); + } + } +} + void ChatListModel::handleChatIsMarkedAsUnreadUpdated(qlonglong chatId, bool chatIsMarkedAsUnread) { if (chatIndexMap.contains(chatId)) { diff --git a/src/chatlistmodel.h b/src/chatlistmodel.h index 964f9f7..8c17d75 100644 --- a/src/chatlistmodel.h +++ b/src/chatlistmodel.h @@ -22,6 +22,7 @@ #include #include "tdlibwrapper.h" +#include "appsettings.h" class ChatListModel : public QAbstractListModel { @@ -47,12 +48,13 @@ public: RoleIsVerified, RoleIsChannel, RoleIsMarkedAsUnread, + RoleIsPinned, RoleFilter, RoleDraftMessageText, RoleDraftMessageDate }; - ChatListModel(TDLibWrapper *tdLibWrapper); + ChatListModel(TDLibWrapper *tdLibWrapper, AppSettings *appSettings); ~ChatListModel() override; virtual QHash roleNames() const override; @@ -64,6 +66,7 @@ public: Q_INVOKABLE QVariantMap getById(qlonglong chatId); Q_INVOKABLE void reset(); + Q_INVOKABLE void calculateUnreadState(); bool showAllChats() const; void setShowAllChats(bool showAll); @@ -81,6 +84,7 @@ private slots: void handleGroupUpdated(qlonglong groupId); void handleSecretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat); void handleChatTitleUpdated(const QString &chatId, const QString &title); + void handleChatPinnedUpdated(qlonglong chatId, bool chatIsPinned); void handleChatIsMarkedAsUnreadUpdated(qlonglong chatId, bool chatIsMarkedAsUnread); void handleChatDraftMessageUpdated(qlonglong chatId, const QVariantMap &draftMessage, const QString &order); void handleRelativeTimeRefreshTimer(); @@ -89,6 +93,7 @@ signals: void showAllChatsChanged(); void chatChanged(const qlonglong &changedChatId); void chatJoined(const qlonglong &chatId, const QString &chatTitle); + void unreadStateChanged(int unreadMessagesCount, int unreadChatsCount); private: class ChatData; @@ -99,6 +104,7 @@ private: private: TDLibWrapper *tdLibWrapper; + AppSettings *appSettings; QTimer *relativeTimeRefreshTimer; QList chatList; QHash chatIndexMap; diff --git a/src/chatmodel.cpp b/src/chatmodel.cpp index a89a74a..7dbc0a4 100644 --- a/src/chatmodel.cpp +++ b/src/chatmodel.cpp @@ -54,8 +54,10 @@ public: MessageData(const QVariantMap &data, qlonglong msgid); static bool lessThan(const MessageData *message1, const MessageData *message2); - void setContent(const QVariantMap &content); - void setReplyMarkup(const QVariantMap &replyMarkup); + QVector diff(const MessageData *message) const; + QVector setMessageData(const QVariantMap &data); + QVector setContent(const QVariantMap &content); + QVector setReplyMarkup(const QVariantMap &replyMarkup); int senderUserId() const; qlonglong senderChatId() const; bool senderIsChat() const; @@ -63,7 +65,7 @@ public: public: QVariantMap messageData; const qlonglong messageId; - const QString messageContentType; + QString messageContentType; }; ChatModel::MessageData::MessageData(const QVariantMap &data, qlonglong msgid) : @@ -88,13 +90,48 @@ bool ChatModel::MessageData::senderIsChat() const return messageData.value(SENDER).toMap().value(_TYPE).toString() == "messageSenderChat"; } -void ChatModel::MessageData::setContent(const QVariantMap &content) +QVector ChatModel::MessageData::diff(const MessageData *message) const { - messageData.insert(CONTENT, content); + QVector roles; + if (message != this) { + roles.append(RoleDisplay); + if (message->messageId != messageId) { + roles.append(RoleMessageId); + } + if (message->messageContentType != messageContentType) { + roles.append(RoleMessageContentType); + } + } + return roles; } -void ChatModel::MessageData::setReplyMarkup(const QVariantMap &replyMarkup) + +QVector ChatModel::MessageData::setMessageData(const QVariantMap &data) +{ + messageData = data; + QVector changedRoles; + changedRoles.append(RoleDisplay); + return changedRoles; +} + +QVector ChatModel::MessageData::setContent(const QVariantMap &content) +{ + const QString oldContentType(messageContentType); + messageData.insert(CONTENT, content); + messageContentType = content.value(_TYPE).toString(); + QVector changedRoles; + if (oldContentType != messageContentType) { + changedRoles.append(RoleMessageContentType); + } + changedRoles.append(RoleDisplay); + return changedRoles; +} + +QVector ChatModel::MessageData::setReplyMarkup(const QVariantMap &replyMarkup) { messageData.insert(REPLY_MARKUP, replyMarkup); + QVector changedRoles; + changedRoles.append(RoleDisplay); + return changedRoles; } bool ChatModel::MessageData::lessThan(const MessageData *message1, const MessageData *message2) @@ -105,7 +142,8 @@ bool ChatModel::MessageData::lessThan(const MessageData *message1, const Message ChatModel::ChatModel(TDLibWrapper *tdLibWrapper) : chatId(0), inReload(false), - inIncrementalUpdate(false) + inIncrementalUpdate(false), + searchModeActive(false) { this->tdLibWrapper = tdLibWrapper; connect(this->tdLibWrapper, SIGNAL(messagesReceived(QVariantList, int)), this, SLOT(handleMessagesReceived(QVariantList, int))); @@ -370,11 +408,10 @@ void ChatModel::handleMessageReceived(qlonglong chatId, qlonglong messageId, con if (chatId == this->chatId && messageIndexMap.contains(messageId)) { LOG("Received a message that we already know, let's update it!"); const int position = messageIndexMap.value(messageId); - MessageData *messageData = messages.at(position); - messageData->messageData = message; + const QVector changedRoles(messages.at(position)->setMessageData(message)); LOG("Message was updated at index" << position); const QModelIndex messageIndex(index(position)); - emit dataChanged(messageIndex, messageIndex); + emit dataChanged(messageIndex, messageIndex, changedRoles); } } @@ -405,11 +442,14 @@ void ChatModel::handleMessageSendSucceeded(qlonglong messageId, qlonglong oldMes if (this->messageIndexMap.contains(oldMessageId)) { LOG("Message was successfully sent" << oldMessageId); const int pos = messageIndexMap.take(oldMessageId); - delete messages.at(pos); - messages.replace(pos, new MessageData(message, messageId)); + MessageData* oldMessage = messages.at(pos); + MessageData* newMessage = new MessageData(message, messageId); + messages.replace(pos, newMessage); + const QVector changedRoles(newMessage->diff(oldMessage)); + delete oldMessage; LOG("Message was replaced at index" << pos); const QModelIndex messageIndex(index(pos)); - emit dataChanged(messageIndex, messageIndex); + emit dataChanged(messageIndex, messageIndex, changedRoles); emit lastReadSentMessageUpdated(calculateLastReadSentMessageId()); } } @@ -448,10 +488,10 @@ 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) { - messages.at(pos)->setContent(newContent); - LOG("Message was replaced at index" << pos); + const QVector changedRoles(messages.at(pos)->setContent(newContent)); + LOG("Message was updated at index" << pos); const QModelIndex messageIndex(index(pos)); - emit dataChanged(messageIndex, messageIndex); + emit dataChanged(messageIndex, messageIndex, changedRoles); emit messageUpdated(pos); } } @@ -464,10 +504,10 @@ 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) { - messages.at(pos)->setReplyMarkup(replyMarkup); + const QVector changedRoles(messages.at(pos)->setReplyMarkup(replyMarkup)); LOG("Message was edited at index" << pos); const QModelIndex messageIndex(index(pos)); - emit dataChanged(messageIndex, messageIndex); + emit dataChanged(messageIndex, messageIndex, changedRoles); emit messageUpdated(pos); } } diff --git a/src/fernschreiberutils.cpp b/src/fernschreiberutils.cpp index b75734a..3237982 100644 --- a/src/fernschreiberutils.cpp +++ b/src/fernschreiberutils.cpp @@ -1,11 +1,74 @@ -#include "fernschreiberutils.h" +/* + Copyright (C) 2020-21 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 . +*/ + +#include "fernschreiberutils.h" #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_MODULE FernschreiberUtils +#include "debuglog.h" FernschreiberUtils::FernschreiberUtils(QObject *parent) : QObject(parent) { + LOG("Initializing audio recorder..."); + QString temporaryDirectoryPath = this->getTemporaryDirectoryPath(); + QDir temporaryDirectory(temporaryDirectoryPath); + if (!temporaryDirectory.exists()) { + temporaryDirectory.mkpath(temporaryDirectoryPath); + } + + QAudioEncoderSettings encoderSettings; + encoderSettings.setCodec("audio/vorbis"); + encoderSettings.setChannelCount(1); + encoderSettings.setQuality(QMultimedia::LowQuality); + this->audioRecorder.setEncodingSettings(encoderSettings); + this->audioRecorder.setContainerFormat("ogg"); + + QMediaRecorder::Status audioRecorderStatus = this->audioRecorder.status(); + this->handleAudioRecorderStatusChanged(audioRecorderStatus); + + connect(&audioRecorder, SIGNAL(durationChanged(qlonglong)), this, SIGNAL(voiceNoteDurationChanged(qlonglong))); + connect(&audioRecorder, SIGNAL(statusChanged(QMediaRecorder::Status)), this, SLOT(handleAudioRecorderStatusChanged(QMediaRecorder::Status))); + + this->geoPositionInfoSource = QGeoPositionInfoSource::createDefaultSource(this); + if (this->geoPositionInfoSource) { + LOG("Geolocation successfully initialized..."); + this->geoPositionInfoSource->setUpdateInterval(5000); + connect(geoPositionInfoSource, SIGNAL(positionUpdated(QGeoPositionInfo)), this, SLOT(handleGeoPositionUpdated(QGeoPositionInfo))); + } else { + LOG("Unable to initialize geolocation!"); + } +} + +FernschreiberUtils::~FernschreiberUtils() +{ + this->cleanUp(); } QString FernschreiberUtils::getMessageShortText(TDLibWrapper *tdLibWrapper, const QVariantMap &messageContent, const bool isChannel, const qlonglong currentUserId, const QVariantMap &messageSender) @@ -128,3 +191,117 @@ QString FernschreiberUtils::getUserName(const QVariantMap &userInformation) const QString lastName = userInformation.value("last_name").toString(); return QString(firstName + " " + lastName).trimmed(); } + +void FernschreiberUtils::startRecordingVoiceNote() +{ + LOG("Start recording voice note..."); + QDateTime thisIsNow = QDateTime::currentDateTime(); + this->audioRecorder.setOutputLocation(QUrl::fromLocalFile(this->getTemporaryDirectoryPath() + "/voicenote-" + thisIsNow.toString("yyyy-MM-dd-HH-mm-ss") + ".ogg")); + this->audioRecorder.setVolume(1); + this->audioRecorder.record(); +} + +void FernschreiberUtils::stopRecordingVoiceNote() +{ + LOG("Stop recording voice note..."); + this->audioRecorder.stop(); +} + +QString FernschreiberUtils::voiceNotePath() +{ + return this->audioRecorder.outputLocation().toLocalFile(); +} + +FernschreiberUtils::VoiceNoteRecordingState FernschreiberUtils::getVoiceNoteRecordingState() +{ + return this->voiceNoteRecordingState; +} + +void FernschreiberUtils::startGeoLocationUpdates() +{ + if (this->geoPositionInfoSource) { + this->geoPositionInfoSource->startUpdates(); + } +} + +void FernschreiberUtils::stopGeoLocationUpdates() +{ + if (this->geoPositionInfoSource) { + this->geoPositionInfoSource->stopUpdates(); + } +} + +bool FernschreiberUtils::supportsGeoLocation() +{ + return this->geoPositionInfoSource; +} + +void FernschreiberUtils::handleAudioRecorderStatusChanged(QMediaRecorder::Status status) +{ + LOG("Audio recorder status changed:" << status); + switch (status) { + case QMediaRecorder::UnavailableStatus: + case QMediaRecorder::UnloadedStatus: + case QMediaRecorder::LoadingStatus: + this->voiceNoteRecordingState = VoiceNoteRecordingState::Unavailable; + break; + case QMediaRecorder::LoadedStatus: + case QMediaRecorder::PausedStatus: + this->voiceNoteRecordingState = VoiceNoteRecordingState::Ready; + break; + case QMediaRecorder::StartingStatus: + this->voiceNoteRecordingState = VoiceNoteRecordingState::Starting; + break; + case QMediaRecorder::FinalizingStatus: + this->voiceNoteRecordingState = VoiceNoteRecordingState::Stopping; + break; + case QMediaRecorder::RecordingStatus: + this->voiceNoteRecordingState = VoiceNoteRecordingState::Recording; + break; + } + emit voiceNoteRecordingStateChanged(this->voiceNoteRecordingState); +} + +void FernschreiberUtils::handleGeoPositionUpdated(const QGeoPositionInfo &info) +{ + LOG("Geo position was updated"); + QVariantMap positionInformation; + if (info.hasAttribute(QGeoPositionInfo::HorizontalAccuracy)) { + positionInformation.insert("horizontalAccuracy", info.attribute(QGeoPositionInfo::HorizontalAccuracy)); + } else { + positionInformation.insert("horizontalAccuracy", 0); + } + if (info.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) { + positionInformation.insert("verticalAccuracy", info.attribute(QGeoPositionInfo::VerticalAccuracy)); + } else { + positionInformation.insert("verticalAccuracy", 0); + } + QGeoCoordinate geoCoordinate = info.coordinate(); + positionInformation.insert("latitude", geoCoordinate.latitude()); + positionInformation.insert("longitude", geoCoordinate.longitude()); + + + emit newPositionInformation(positionInformation); +} + +void FernschreiberUtils::cleanUp() +{ + if (this->geoPositionInfoSource) { + this->geoPositionInfoSource->stopUpdates(); + } + QString temporaryDirectoryPath = this->getTemporaryDirectoryPath(); + QDirIterator temporaryDirectoryIterator(temporaryDirectoryPath, QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (temporaryDirectoryIterator.hasNext()) { + QString nextFilePath = temporaryDirectoryIterator.next(); + if (QFile::remove(nextFilePath)) { + LOG("Temporary file removed " << nextFilePath); + } else { + LOG("Error removing temporary file " << nextFilePath); + } + } +} + +QString FernschreiberUtils::getTemporaryDirectoryPath() +{ + return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + "/harbour-fernschreiber"; +} diff --git a/src/fernschreiberutils.h b/src/fernschreiberutils.h index 35fc8a0..ce3bc3b 100644 --- a/src/fernschreiberutils.h +++ b/src/fernschreiberutils.h @@ -1,7 +1,29 @@ +/* + Copyright (C) 2020-21 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 . +*/ + #ifndef FERNSCHREIBERUTILS_H #define FERNSCHREIBERUTILS_H #include +#include +#include +#include #include "tdlibwrapper.h" class FernschreiberUtils : public QObject @@ -9,13 +31,46 @@ class FernschreiberUtils : public QObject Q_OBJECT public: explicit FernschreiberUtils(QObject *parent = nullptr); + ~FernschreiberUtils(); + + enum VoiceNoteRecordingState { + Unavailable, + Ready, + Starting, + Recording, + Stopping + }; + Q_ENUM(VoiceNoteRecordingState) static QString getMessageShortText(TDLibWrapper *tdLibWrapper, const QVariantMap &messageContent, const bool isChannel, const qlonglong currentUserId, const QVariantMap &messageSender); static QString getUserName(const QVariantMap &userInformation); -signals: + Q_INVOKABLE void startRecordingVoiceNote(); + Q_INVOKABLE void stopRecordingVoiceNote(); + Q_INVOKABLE QString voiceNotePath(); + Q_INVOKABLE VoiceNoteRecordingState getVoiceNoteRecordingState(); + Q_INVOKABLE void startGeoLocationUpdates(); + Q_INVOKABLE void stopGeoLocationUpdates(); + Q_INVOKABLE bool supportsGeoLocation(); + +signals: + void voiceNoteDurationChanged(qlonglong duration); + void voiceNoteRecordingStateChanged(VoiceNoteRecordingState state); + void newPositionInformation(const QVariantMap &positionInformation); + +private slots: + void handleAudioRecorderStatusChanged(QMediaRecorder::Status status); + void handleGeoPositionUpdated(const QGeoPositionInfo &info); + +private: + QAudioRecorder audioRecorder; + VoiceNoteRecordingState voiceNoteRecordingState; + + QGeoPositionInfoSource *geoPositionInfoSource; + + void cleanUp(); + QString getTemporaryDirectoryPath(); -public slots: }; #endif // FERNSCHREIBERUTILS_H diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp index 6efdf5d..51febb6 100644 --- a/src/harbour-fernschreiber.cpp +++ b/src/harbour-fernschreiber.cpp @@ -82,11 +82,12 @@ int main(int argc, char *argv[]) FernschreiberUtils *fernschreiberUtils = new FernschreiberUtils(view.data()); context->setContextProperty("fernschreiberUtils", fernschreiberUtils); + qmlRegisterUncreatableType(uri, 1, 0, "FernschreiberUtilities", QString()); DBusAdaptor *dBusAdaptor = tdLibWrapper->getDBusAdaptor(); context->setContextProperty("dBusAdaptor", dBusAdaptor); - ChatListModel chatListModel(tdLibWrapper); + ChatListModel chatListModel(tdLibWrapper, appSettings); context->setContextProperty("chatListModel", &chatListModel); QSortFilterProxyModel chatListProxyModel(view.data()); chatListProxyModel.setSourceModel(&chatListModel); diff --git a/src/tdlibfile.cpp b/src/tdlibfile.cpp index 232fa9a..dece939 100644 --- a/src/tdlibfile.cpp +++ b/src/tdlibfile.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2020 Slava Monich at al. + Copyright (C) 2020-2021 Slava Monich at al. This file is part of Fernschreiber. @@ -107,6 +107,7 @@ void TDLibFile::init() queuedSignals = 0; firstQueuedSignal = SignalCount; autoLoad = false; + downloadHoldOffTimer = 0; id = 0; expected_size = 0; size = 0; @@ -174,7 +175,7 @@ void TDLibFile::updateTDLibWrapper(TDLibWrapper* tdlib) if (tdlib) { connect(tdlib, SIGNAL(fileUpdated(int,QVariantMap)), SLOT(handleFileUpdated(int,QVariantMap))); if (autoLoad) { - load(); + downloadFile(); } } queueSignal(SignalTdLibChanged); @@ -187,7 +188,7 @@ void TDLibFile::setAutoLoad(bool enableAutoLoad) autoLoad = enableAutoLoad; queueSignal(SignalAutoLoadChanged); if (autoLoad) { - load(); + downloadFile(); } emitQueuedSignals(); } @@ -195,7 +196,19 @@ void TDLibFile::setAutoLoad(bool enableAutoLoad) bool TDLibFile::load() { - if (id && tdLibWrapper && !is_downloading_active && !is_downloading_completed && can_be_downloaded) { + // Manual load ignores hold-off timer + if (downloadHoldOffTimer) { + killTimer(downloadHoldOffTimer); + downloadHoldOffTimer = 0; + } + return downloadFile(); +} + +bool TDLibFile::downloadFile() +{ + if (id && tdLibWrapper && !downloadHoldOffTimer && + !is_downloading_active && !is_downloading_completed && can_be_downloaded) { + downloadHoldOffTimer = startTimer(DownloadHoldOffMs); tdLibWrapper->downloadFile(id); return true; } @@ -205,12 +218,26 @@ bool TDLibFile::load() void TDLibFile::setFileInfo(const QVariantMap &fileInfo) { updateFileInfo(fileInfo); + if (is_downloading_completed && downloadHoldOffTimer) { + // Don't need this timer anymore + killTimer(downloadHoldOffTimer); + downloadHoldOffTimer = 0; + } if (autoLoad) { - load(); + downloadFile(); } emitQueuedSignals(); } +void TDLibFile::timerEvent(QTimerEvent *) +{ + killTimer(downloadHoldOffTimer); + downloadHoldOffTimer = 0; + if (autoLoad) { + downloadFile(); + } +} + void TDLibFile::handleFileUpdated(int fileId, const QVariantMap &fileInfo) { if (id == fileId) { @@ -256,6 +283,7 @@ void TDLibFile::updateFileInfo(const QVariantMap &file) bool b, fileChanged = false; int i = file.value(ID).toInt(); if (id != i) { + LOG("File id has changed" << id << "=>" << i); id = i; fileChanged = true; queueSignal(SignalIdChanged); diff --git a/src/tdlibfile.h b/src/tdlibfile.h index 9a16af4..0616271 100644 --- a/src/tdlibfile.h +++ b/src/tdlibfile.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2020 Slava Monich at al. + Copyright (C) 2020-2021 Slava Monich at al. This file is part of Fernschreiber. @@ -45,6 +45,8 @@ class TDLibFile : public QObject Q_PROPERTY(bool isUploadingActive READ isUploadingActive NOTIFY uploadingActiveChanged) Q_PROPERTY(bool isUploadingCompleted READ isUploadingCompleted NOTIFY uploadingCompletedChanged) + enum { DownloadHoldOffMs = 1000 }; + public: TDLibFile(); TDLibFile(TDLibWrapper *tdlib); @@ -102,10 +104,14 @@ signals: private slots: void handleFileUpdated(int fileId, const QVariantMap &fileInfo); +protected: + void timerEvent(QTimerEvent *event) Q_DECL_OVERRIDE; + private: void init(); void updateTDLibWrapper(TDLibWrapper* tdlib); void updateFileInfo(const QVariantMap &fileInfo); + bool downloadFile(); void queueSignal(uint signal); void emitQueuedSignals(); @@ -114,6 +120,7 @@ private: uint queuedSignals; uint firstQueuedSignal; bool autoLoad; + int downloadHoldOffTimer; // file QVariantMap infoMap; int id; diff --git a/src/tdlibreceiver.cpp b/src/tdlibreceiver.cpp index a367937..045e27e 100644 --- a/src/tdlibreceiver.cpp +++ b/src/tdlibreceiver.cpp @@ -38,6 +38,7 @@ namespace { const QString POSITIONS("positions"); const QString PHOTO("photo"); const QString ORDER("order"); + const QString IS_PINNED("is_pinned"); const QString BASIC_GROUP("basic_group"); const QString SUPERGROUP("supergroup"); const QString LAST_MESSAGE("last_message"); @@ -287,9 +288,20 @@ void TDLibReceiver::processUpdateChatOrder(const QVariantMap &receivedInformatio void TDLibReceiver::processUpdateChatPosition(const QVariantMap &receivedInformation) { const QString chat_id(receivedInformation.value(CHAT_ID).toString()); - const QString order(receivedInformation.value(POSITION).toMap().value(ORDER).toString()); - LOG("Chat position updated for ID" << chat_id << "new order" << order); - emit chatOrderUpdated(chat_id, order); + QVariantMap positionMap = receivedInformation.value(POSITION).toMap(); + + QString updateForChatList = positionMap.value(LIST).toMap().value(TYPE).toString(); + const QString order(positionMap.value(ORDER).toString()); + bool is_pinned = positionMap.value(IS_PINNED).toBool(); + + // We are only processing main chat list updates at the moment... + if (updateForChatList == "chatListMain") { + LOG("Chat position updated for ID" << chat_id << "new order" << order << "is pinned" << is_pinned); + emit chatOrderUpdated(chat_id, order); + emit chatPinnedUpdated(chat_id.toLongLong(), is_pinned); + } else { + LOG("Received chat position update for uninteresting list" << updateForChatList << "ID" << chat_id << "new order" << order << "is pinned" << is_pinned); + } } void TDLibReceiver::processUpdateChatReadInbox(const QVariantMap &receivedInformation) diff --git a/src/tdlibreceiver.h b/src/tdlibreceiver.h index 311610b..7eb8b99 100644 --- a/src/tdlibreceiver.h +++ b/src/tdlibreceiver.h @@ -49,6 +49,7 @@ signals: void unreadChatCountUpdated(const QVariantMap &chatCountInformation); void chatLastMessageUpdated(const QString &chatId, const QString &order, const QVariantMap &lastMessage); void chatOrderUpdated(const QString &chatId, const QString &order); + void chatPinnedUpdated(qlonglong chatId, bool isPinned); void chatReadInboxUpdated(const QString &chatId, const QString &lastReadInboxMessageId, int unreadCount); void chatReadOutboxUpdated(const QString &chatId, const QString &lastReadOutboxMessageId); void basicGroupUpdated(qlonglong groupId, const QVariantMap &groupInformation); diff --git a/src/tdlibwrapper.cpp b/src/tdlibwrapper.cpp index f3c41e5..9ad3a99 100644 --- a/src/tdlibwrapper.cpp +++ b/src/tdlibwrapper.cpp @@ -47,8 +47,10 @@ namespace { const QString USERNAME("username"); const QString THREAD_ID("thread_id"); const QString VALUE("value"); + const QString CHAT_LIST_TYPE("chat_list_type"); const QString _TYPE("@type"); const QString _EXTRA("@extra"); + const QString CHAT_LIST_MAIN("chatListMain"); } TDLibWrapper::TDLibWrapper(AppSettings *appSettings, MceInterface *mceInterface, QObject *parent) : QObject(parent), joinChatRequested(false) @@ -145,6 +147,7 @@ void TDLibWrapper::initializeTDLibReciever() { connect(this->tdLibReceiver, SIGNAL(chatPermissionsUpdated(QString, QVariantMap)), this, SIGNAL(chatPermissionsUpdated(QString, QVariantMap))); connect(this->tdLibReceiver, SIGNAL(chatPhotoUpdated(qlonglong, QVariantMap)), this, SIGNAL(chatPhotoUpdated(qlonglong, QVariantMap))); connect(this->tdLibReceiver, SIGNAL(chatTitleUpdated(QString, QString)), this, SIGNAL(chatTitleUpdated(QString, QString))); + connect(this->tdLibReceiver, SIGNAL(chatPinnedUpdated(qlonglong, bool)), this, SIGNAL(chatPinnedUpdated(qlonglong, bool))); connect(this->tdLibReceiver, SIGNAL(chatPinnedMessageUpdated(qlonglong, qlonglong)), this, SIGNAL(chatPinnedMessageUpdated(qlonglong, qlonglong))); connect(this->tdLibReceiver, SIGNAL(messageIsPinnedUpdated(qlonglong, qlonglong, bool)), this, SLOT(handleMessageIsPinnedUpdated(qlonglong, qlonglong, bool))); connect(this->tdLibReceiver, SIGNAL(usersReceived(QString, QVariantList, int)), this, SIGNAL(usersReceived(QString, QVariantList, int))); @@ -491,6 +494,56 @@ void TDLibWrapper::sendDocumentMessage(const QString &chatId, const QString &fil this->sendRequest(requestObject); } +void TDLibWrapper::sendVoiceNoteMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId) +{ + LOG("Sending voice note message" << chatId << filePath << message << replyToMessageId); + QVariantMap requestObject; + requestObject.insert(_TYPE, "sendMessage"); + requestObject.insert(CHAT_ID, chatId); + if (replyToMessageId != "0") { + requestObject.insert("reply_to_message_id", replyToMessageId); + } + QVariantMap inputMessageContent; + inputMessageContent.insert(_TYPE, "inputMessageVoiceNote"); + QVariantMap formattedText; + formattedText.insert("text", message); + formattedText.insert(_TYPE, "formattedText"); + inputMessageContent.insert("caption", formattedText); + QVariantMap documentInputFile; + documentInputFile.insert(_TYPE, "inputFileLocal"); + documentInputFile.insert("path", filePath); + inputMessageContent.insert("voice_note", documentInputFile); + + requestObject.insert("input_message_content", inputMessageContent); + this->sendRequest(requestObject); +} + +void TDLibWrapper::sendLocationMessage(const QString &chatId, double latitude, double longitude, double horizontalAccuracy, const QString &replyToMessageId) +{ + LOG("Sending location message" << chatId << latitude << longitude << horizontalAccuracy << replyToMessageId); + QVariantMap requestObject; + requestObject.insert(_TYPE, "sendMessage"); + requestObject.insert(CHAT_ID, chatId); + if (replyToMessageId != "0") { + requestObject.insert("reply_to_message_id", replyToMessageId); + } + QVariantMap inputMessageContent; + inputMessageContent.insert(_TYPE, "inputMessageLocation"); + QVariantMap location; + location.insert("latitude", latitude); + location.insert("longitude", longitude); + location.insert("horizontal_accuracy", horizontalAccuracy); + location.insert(_TYPE, "location"); + inputMessageContent.insert("location", location); + + inputMessageContent.insert("live_period", 0); + inputMessageContent.insert("heading", 0); + inputMessageContent.insert("proximity_alert_radius", 0); + + requestObject.insert("input_message_content", inputMessageContent); + this->sendRequest(requestObject); +} + void TDLibWrapper::sendStickerMessage(const QString &chatId, const QString &fileId, const QString &replyToMessageId) { LOG("Sending sticker message" << chatId << fileId << replyToMessageId); @@ -1002,6 +1055,20 @@ void TDLibWrapper::toggleChatIsMarkedAsUnread(qlonglong chatId, bool isMarkedAsU this->sendRequest(requestObject); } +void TDLibWrapper::toggleChatIsPinned(qlonglong chatId, bool isPinned) +{ + LOG("Toggle chat is pinned" << chatId << isPinned); + QVariantMap requestObject; + requestObject.insert(_TYPE, "toggleChatIsPinned"); + QVariantMap chatListMap; + chatListMap.insert(_TYPE, CHAT_LIST_MAIN); + requestObject.insert("chat_list", chatListMap); + requestObject.insert(CHAT_ID, chatId); + requestObject.insert("is_pinned", isPinned); + requestObject.insert("is_marked_as_unread", isPinned); + this->sendRequest(requestObject); +} + void TDLibWrapper::setChatDraftMessage(qlonglong chatId, qlonglong threadId, qlonglong replyToMessageId, const QString &draft) { LOG("Set Draft Message" << chatId); @@ -1334,7 +1401,7 @@ void TDLibWrapper::handleChatReceived(const QVariantMap &chatInformation) void TDLibWrapper::handleUnreadMessageCountUpdated(const QVariantMap &messageCountInformation) { - if (messageCountInformation.value("chat_list_type").toString() == "chatListMain") { + if (messageCountInformation.value(CHAT_LIST_TYPE).toString() == CHAT_LIST_MAIN) { this->unreadMessageInformation = messageCountInformation; emit unreadMessageCountUpdated(messageCountInformation); } @@ -1342,7 +1409,7 @@ void TDLibWrapper::handleUnreadMessageCountUpdated(const QVariantMap &messageCou void TDLibWrapper::handleUnreadChatCountUpdated(const QVariantMap &chatCountInformation) { - if (chatCountInformation.value("chat_list_type").toString() == "chatListMain") { + if (chatCountInformation.value(CHAT_LIST_TYPE).toString() == CHAT_LIST_MAIN) { this->unreadChatInformation = chatCountInformation; emit unreadChatCountUpdated(chatCountInformation); } @@ -1449,15 +1516,16 @@ void TDLibWrapper::setInitialParameters() initialParameters.insert("api_id", TDLIB_API_ID); initialParameters.insert("api_hash", TDLIB_API_HASH); initialParameters.insert("database_directory", QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tdlib"); - initialParameters.insert("use_file_database", true); - initialParameters.insert("use_chat_info_database", true); - initialParameters.insert("use_message_database", true); + bool onlineOnlyMode = this->appSettings->onlineOnlyMode(); + initialParameters.insert("use_file_database", !onlineOnlyMode); + initialParameters.insert("use_chat_info_database", !onlineOnlyMode); + initialParameters.insert("use_message_database", !onlineOnlyMode); initialParameters.insert("use_secret_chats", true); initialParameters.insert("system_language_code", QLocale::system().name()); QSettings hardwareSettings("/etc/hw-release", QSettings::NativeFormat); initialParameters.insert("device_model", hardwareSettings.value("NAME", "Unknown Mobile Device").toString()); initialParameters.insert("system_version", QSysInfo::prettyProductName()); - initialParameters.insert("application_version", "0.6"); + initialParameters.insert("application_version", "0.7"); initialParameters.insert("enable_storage_optimizer", appSettings->storageOptimizer()); // initialParameters.insert("use_test_dc", true); requestObject.insert("parameters", initialParameters); diff --git a/src/tdlibwrapper.h b/src/tdlibwrapper.h index 72c66a8..4a02a02 100644 --- a/src/tdlibwrapper.h +++ b/src/tdlibwrapper.h @@ -142,6 +142,8 @@ public: Q_INVOKABLE void sendPhotoMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendVideoMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendDocumentMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); + Q_INVOKABLE void sendVoiceNoteMessage(const QString &chatId, const QString &filePath, const QString &message, const QString &replyToMessageId = "0"); + Q_INVOKABLE void sendLocationMessage(const QString &chatId, double latitude, double longitude, double horizontalAccuracy, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendStickerMessage(const QString &chatId, const QString &fileId, const QString &replyToMessageId = "0"); Q_INVOKABLE void sendPollMessage(const QString &chatId, const QString &question, const QVariantList &options, bool anonymous, int correctOption, bool multiple, const QString &replyToMessageId = "0"); Q_INVOKABLE void forwardMessages(const QString &chatId, const QString &fromChatId, const QVariantList &messageIds, bool sendCopy, bool removeCaption); @@ -186,6 +188,7 @@ public: Q_INVOKABLE void searchPublicChats(const QString &query); Q_INVOKABLE void readAllChatMentions(qlonglong chatId); Q_INVOKABLE void toggleChatIsMarkedAsUnread(qlonglong chatId, bool isMarkedAsUnread); + Q_INVOKABLE void toggleChatIsPinned(qlonglong chatId, bool isPinned); Q_INVOKABLE void setChatDraftMessage(qlonglong chatId, qlonglong threadId, qlonglong replyToMessageId, const QString &draft); // Others (candidates for extraction ;)) @@ -211,6 +214,7 @@ signals: void unreadChatCountUpdated(const QVariantMap &chatCountInformation); void chatLastMessageUpdated(const QString &chatId, const QString &order, const QVariantMap &lastMessage); void chatOrderUpdated(const QString &chatId, const QString &order); + void chatPinnedUpdated(qlonglong chatId, bool isPinned); void chatReadInboxUpdated(const QString &chatId, const QString &lastReadInboxMessageId, int unreadCount); void chatReadOutboxUpdated(const QString &chatId, const QString &lastReadOutboxMessageId); void userUpdated(const QString &userId, const QVariantMap &userInformation); diff --git a/translations/harbour-fernschreiber-de.ts b/translations/harbour-fernschreiber-de.ts index aa5579c..95d0042 100644 --- a/translations/harbour-fernschreiber-de.ts +++ b/translations/harbour-fernschreiber-de.ts @@ -131,14 +131,6 @@ Leaving chat Verlasse Chat - - Unmute Chat - Stummschaltung des Chats aufheben - - - Mute Chat - Chat stummschalten - Unknown Unbekannt @@ -191,6 +183,14 @@ New Secret Chat Neuer geheimer Chat + + Unmute Chat + Stummschaltung des Chats aufheben + + + Mute Chat + Chat stummschalten + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You Sie - - Unmute Chat - Stummschaltung des Chats aufheben - - - Mute Chat - Chat stummschalten - User Info Benutzerinfos @@ -284,6 +276,22 @@ Draft Entwurf + + Unpin chat + Chat losheften + + + Pin chat + Chat anheften + + + Unmute chat + Stummschaltung des Chats aufheben + + + Mute chat + Chat stummschalten + ChatPage @@ -419,6 +427,14 @@ Search in chat... Im Chat suchen... + + Location: Obtaining position... + Standort: Erlange Position... + + + Location (%1/%2) + Standort (%1/%2) + ChatSelectionPage @@ -484,6 +500,10 @@ Open Document Dokument öffnen + + Copy Document to Downloads + Dokument zu Downloads kopieren + EditGroupChatPermissionsColumn @@ -1085,8 +1105,16 @@ Download fehlgeschlagen. - Connecting to network... - Verbinde zum Netzwerk... + Tap on the title bar to filter your chats + Tippen Sie auf die Titelleiste, um Ihre Chats zu filtern + + + No matching chats found. + Keine passenden Chats gefunden. + + + You can search public chats or create a new chat via the pull-down menu. + Sie können über das Pull-Down-Menü öffentliche Chats finden oder einen Neuen erstellen. Logging out @@ -1308,13 +1336,19 @@ Channel Kanal - + %1 members - %1 Mitglied + + %1 Mitglied + %1 Mitglieder + - + %1 subscribers - %1 Abonnent + + %1 Abonnent + %1 Abonnenten + Search Chats @@ -1403,6 +1437,22 @@ Enable storage optimizer Speicheroptimierer einschalten + + Focus text input area after send + Texteingabe nach Senden fokussieren + + + Focus the text input area after sending a message + Fokussiert die Texteingabe nach Senden einer Nachricht + + + Enable online-only mode + Nur-Online-Modus einschalten + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + Schaltet das Offline-Caching aus. Bestimmte Features können in diesem Modus eingeschränkt sein oder fehlen. Änderungen erfordern einen Neustart von Fernschreiber, um in Kraft zu treten. + StickerPicker @@ -1430,6 +1480,45 @@ Download fehlgeschlagen. + + VoiceNoteOverlay + + Record a Voice Note + Eine Sprachnachricht aufzeichnen + + + Press the button to start recording + Drücken Sie den Knopf, um die Aufzeichnung zu starten + + + Unavailable + Nicht verfügbar + + + Ready + Bereit + + + Starting + Startet + + + Recording + Zeichnet auf + + + Stopping + Stoppt + + + Use recording + Aufzeichnung verwenden + + + Voice Note (%1) + Sprachnachricht (%1) + + WebPagePreview diff --git a/translations/harbour-fernschreiber-en.ts b/translations/harbour-fernschreiber-en.ts index 66f406b..a6dbd3a 100644 --- a/translations/harbour-fernschreiber-en.ts +++ b/translations/harbour-fernschreiber-en.ts @@ -131,14 +131,6 @@ Leaving chat Leaving chat - - Unmute Chat - Unmute Chat - - - Mute Chat - Mute Chat - Unknown Unknown @@ -191,6 +183,14 @@ New Secret Chat New Secret Chat + + Unmute Chat + Unmute Chat + + + Mute Chat + Mute Chat + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You You - - Unmute Chat - Unmute Chat - - - Mute Chat - Mute Chat - User Info User Info @@ -284,6 +276,22 @@ Draft Draft + + Unpin chat + Unpin chat + + + Pin chat + Pin chat + + + Unmute chat + Unmute chat + + + Mute chat + Mute chat + ChatPage @@ -419,6 +427,14 @@ Search in chat... Search in chat... + + Location: Obtaining position... + Location: Obtaining position... + + + Location (%1/%2) + Location (%1/%2) + ChatSelectionPage @@ -484,6 +500,10 @@ Open Document Open Document + + Copy Document to Downloads + Copy Document to Downloads + EditGroupChatPermissionsColumn @@ -1085,12 +1105,20 @@ Download failed. - Connecting to network... - Connecting to network... + Tap on the title bar to filter your chats + Tap on the title bar to filter your chats + + + No matching chats found. + No matching chats found. + + + You can search public chats or create a new chat via the pull-down menu. + You can search public chats or create a new chat via the pull-down menu. Logging out - + Logging out @@ -1308,13 +1336,19 @@ Channel Channel - + %1 members - %1 member + + %1 member + %1 members + - + %1 subscribers - %1 subscriber + + %1 subscriber + %1 subscribers + Search Chats @@ -1403,6 +1437,22 @@ Enable storage optimizer Enable storage optimizer + + Focus text input area after send + Focus text input area after send + + + Focus the text input area after sending a message + Focus the text input area after sending a message + + + Enable online-only mode + Enable online-only mode + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + StickerPicker @@ -1430,6 +1480,45 @@ Download failed. + + VoiceNoteOverlay + + Record a Voice Note + Record a Voice Note + + + Press the button to start recording + Press the button to start recording + + + Unavailable + Unavailable + + + Starting + Starting + + + Recording + Recording + + + Stopping + Stopping + + + Use recording + Use recording + + + Voice Note (%1) + Voice Note (%1) + + + Ready + Ready + + WebPagePreview @@ -1815,21 +1904,21 @@ has added %1 to the chat - has added %1 to the chat + has added %1 to the chat has removed %1 from the chat - has removed %1 from the chat + has removed %1 from the chat have added %1 to the chat myself - have added %1 to the chat + have added %1 to the chat have removed %1 from the chat myself - have removed %1 from the chat + have removed %1 from the chat diff --git a/translations/harbour-fernschreiber-es.ts b/translations/harbour-fernschreiber-es.ts index c460743..9ee8018 100644 --- a/translations/harbour-fernschreiber-es.ts +++ b/translations/harbour-fernschreiber-es.ts @@ -129,14 +129,6 @@ Leaving chat Saliendo de la charla - - Unmute Chat - Notificar - - - Mute Chat - No notificar - Unknown Desconocido @@ -188,6 +180,14 @@ New Secret Chat Charla secreta + + Unmute Chat + Notificar + + + Mute Chat + No notificar + ChatInformationTabItemMembersGroups @@ -245,14 +245,6 @@ You Usted - - Unmute Chat - Notificar - - - Mute Chat - No notificar - User Info Usuario @@ -271,16 +263,32 @@ Mark chat as unread - + Marcar como no leído Draft - + Borrador Mark chat as read + Marcar como leído + + + Unpin chat + + Pin chat + + + + Unmute chat + Notificar + + + Mute chat + No notificar + ChatPage @@ -403,11 +411,19 @@ Search in Chat - + Buscar en charla Search in chat... - + Buscar + + + Location: Obtaining position... + Ubicación: Recibiendo posición ... + + + Location (%1/%2) + Ubicación (%1/%2) @@ -474,6 +490,10 @@ Open Document Abrir Documento + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -785,21 +805,21 @@ has added %1 to the chat - + ha añadido %1 a charla has removed %1 from the chat - + ha quitado %1 de charla have added %1 to the chat myself - + ha añadido %1 a la charla have removed %1 from the chat myself - + ha quitado %1 de charla @@ -836,7 +856,7 @@ Please enter your phone number to continue. - Marcar número de teléfono para continuar. + Marcar el número de teléfono para continuar. Continue @@ -884,7 +904,7 @@ Use the international format, e.g. %1 - Usar el formato internacional %1 + Usa el formato internacional %1 About Fernschreiber @@ -910,7 +930,7 @@ Copy Message to Clipboard - Copiar mensaje + Copiar Message deleted @@ -930,7 +950,7 @@ Select Message - Seleccionar mensaje + Seleccionar Pin Message @@ -938,11 +958,11 @@ Message unpinned - Desanclar mensaje + Mensaje desanclado Unpin Message - + Desanclar mensaje @@ -1059,19 +1079,31 @@ Filter your chats... - + Filtrar las charlas... Search Chats - + Buscar charlas Download of %1 successful. - Bajada de %1 exitosa. + Bajada de %1 exitosa. Download failed. - Error al bajar + Error al bajar + + + Tap on the title bar to filter your chats + Tocar la barra de título para filtrar las charlas + + + No matching chats found. + No hay coincidencias. + + + You can search public chats or create a new chat via the pull-down menu. + Puede buscar charlas públicas o crear un nueva charla a través de la polea de opciones. Connecting to network... @@ -1094,7 +1126,7 @@ Message unpinned - Desanclar mensaje + Mensaje desanclado @@ -1271,43 +1303,47 @@ SearchChatsPage No chats found. - + No se han encontrado charlas. Searching chats... - + Buscando charlas... Private Chat - Privado + Privado Group - + Grupo Channel - + Canal - + %1 members - %1 miembros + + %1 miembros + - + %1 subscribers - %1 suscriptores + + %1 suscriptores + Search Chats - + Buscar charla Search a chat... - + A b c Enter your query to start searching (at least 5 characters needed) - + Para iniciar la búsqueda se necesitan al menos 5 caracteres @@ -1342,15 +1378,15 @@ Notification feedback - Notificaciones + Notificar en All events - Todos los eventos + Eventos Only new events - Sólo nuevos eventos + Nuevos eventos None @@ -1384,6 +1420,22 @@ Enable storage optimizer Optimizador de almacenamiento + + Focus text input area after send + Enfocar área de entrada de texto + + + Focus the text input area after sending a message + Enfoca el área de entrada de texto después de enviar un mensaje + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1411,6 +1463,45 @@ Error al bajar + + VoiceNoteOverlay + + Record a Voice Note + Nota de voz + + + Press the button to start recording + Presionar el botón para iniciar a grabar + + + Unavailable + No diponible + + + Starting + Iniciando + + + Recording + Grabando + + + Stopping + Deteniendo + + + Use recording + Usar grabación + + + Voice Note (%1) + Nota de voz (%1) + + + Ready + Listo + + WebPagePreview @@ -1796,21 +1887,21 @@ has added %1 to the chat - + ha añadido %1 a la charla has removed %1 from the chat - + ha quitado %1 de la charla have added %1 to the chat myself - + ha añadido %1 a la charla have removed %1 from the chat myself - + ha añadido %1 de la charla diff --git a/translations/harbour-fernschreiber-fi.ts b/translations/harbour-fernschreiber-fi.ts index da59c9a..ca7eafc 100644 --- a/translations/harbour-fernschreiber-fi.ts +++ b/translations/harbour-fernschreiber-fi.ts @@ -131,14 +131,6 @@ Leaving chat Poistutaan keskustelusta - - Unmute Chat - Poista keskustelun vaimennus - - - Mute Chat - Vaimenna keskustelu - Unknown Tuntematon @@ -191,6 +183,14 @@ New Secret Chat Uusi salattu keskustelu + + Unmute Chat + Poista keskustelun vaimennus + + + Mute Chat + Vaimenna keskustelu + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You Sinä - - Unmute Chat - Poista keskustelun vaimennus - - - Mute Chat - Vaimenna keskustelu - User Info Käyttäjän tiedot @@ -274,16 +266,32 @@ Mark chat as unread - + Merkitse keskustelu lukemattomaksi Draft - + Luonnos Mark chat as read + Merkitse keskustelu luetuksi + + + Unpin chat + + Pin chat + + + + Unmute chat + Poista keskustelun vaimennus + + + Mute chat + Vaimenna keskustelu + ChatPage @@ -413,11 +421,19 @@ Search in Chat - + Etsi keskustelusta Search in chat... - + Etsi keskustelusta... + + + Location: Obtaining position... + Sijainti: Paikannetaan... + + + Location (%1/%2) + Sijainti (%1/%2) @@ -485,6 +501,10 @@ Open Document Avaa dokumentti + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -796,21 +816,21 @@ has added %1 to the chat - + lisäsi käyttäjän %1 keskusteluun has removed %1 from the chat - + posit käyttäjän %1 keskustelusta have added %1 to the chat myself - + lisäsit käyttäjän %1 keskusteluun have removed %1 from the chat myself - + poistit käyttäjän %1 keskustelusta @@ -949,11 +969,11 @@ Message unpinned - Viestin kiinnitys poistettu + Viestin kiinnitys poistettu Unpin Message - + Poista viestin kiinnitys @@ -1071,19 +1091,31 @@ Filter your chats... - + Suodata keskustelujasi... Search Chats - + Etsi keskusteluista Download of %1 successful. - + Tiedoston %1 lataus onnistui. Download failed. - Lataus epäonnistui. + Lataus epäonnistui. + + + Tap on the title bar to filter your chats + Kosketa otsikkopalkkia suodattaaksesi keskusteluja + + + No matching chats found. + Hakua vastaavia keskusteluja ei löytynyt, + + + You can search public chats or create a new chat via the pull-down menu. + Voit etsiä julkisia keskusteluja tai luoda uuden keskustelun alasvetovalikosta. Connecting to network... @@ -1291,43 +1323,49 @@ SearchChatsPage No chats found. - + Keskusteluja ei löytynyt Searching chats... - + Etsitään keskusteluja... Private Chat - Yksityinen keskustelu + Yksityinen keskustelu Group - + Ryhmä Channel - + Kanava - + %1 members - %1 jäsen + + %1 jäsen + %1 jäsentä + - + %1 subscribers - %1 tilaaja + + %1 tilaaja + %1 tilaajaa + Search Chats - + Etsi keskusteluja Search a chat... - + Etsi keskustelua... Enter your query to start searching (at least 5 characters needed) - + Syötä hakusanasi etsiäksesi (vähintään 5 merkkiä tarvitaan) @@ -1404,6 +1442,22 @@ Enable storage optimizer Käytä tallennustilan optimointia + + Focus text input area after send + Kohdista tekstinsyöttökenttä lähetyksen jälkeen + + + Focus the text input area after sending a message + Kohdista tekstinsyöttökenttä viestin lähetyksen jälkeen + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1431,6 +1485,45 @@ Lataus epäonnistui. + + VoiceNoteOverlay + + Record a Voice Note + Nauhoita ääniviesti + + + Press the button to start recording + Paina nappia aloittaaksesi nauhoituksen + + + Unavailable + Ei saatavilla + + + Starting + Aloitetaan + + + Recording + Nauhoitetaan + + + Stopping + Lopetetaan + + + Use recording + Käytä nauhoitusta + + + Voice Note (%1) + Ääniviesti (%1) + + + Ready + Valmis + + WebPagePreview @@ -1816,21 +1909,21 @@ has added %1 to the chat - + lisäsi käyttäjä %1 keskusteluun has removed %1 from the chat - + posit käyttäjän %1 keskustelusta have added %1 to the chat myself - + lisäsit käyttäjän %1 keskusteluun have removed %1 from the chat myself - + poistit käyttäjän %1 keskustelusta diff --git a/translations/harbour-fernschreiber-hu.ts b/translations/harbour-fernschreiber-hu.ts index 9491de2..30a75b0 100644 --- a/translations/harbour-fernschreiber-hu.ts +++ b/translations/harbour-fernschreiber-hu.ts @@ -129,14 +129,6 @@ Leaving chat - - Unmute Chat - Csevegés némítás feloldása - - - Mute Chat - Csevegés némítása - Unknown Ismeretlen @@ -188,6 +180,14 @@ New Secret Chat + + Unmute Chat + Csevegés némítás feloldása + + + Mute Chat + Csevegés némítása + ChatInformationTabItemMembersGroups @@ -245,14 +245,6 @@ You Te - - Unmute Chat - Csevegés némítás feloldása - - - Mute Chat - Csevegés némítása - User Info @@ -281,6 +273,22 @@ Mark chat as read + + Unpin chat + + + + Pin chat + + + + Unmute chat + Csevegés némítás feloldása + + + Mute chat + Csevegés némítása + ChatPage @@ -409,6 +417,14 @@ Search in chat... + + Location: Obtaining position... + + + + Location (%1/%2) + + ChatSelectionPage @@ -474,6 +490,10 @@ Open Document Dokumentum megyitása + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -1074,8 +1094,16 @@ A letöltés nem sikerült. - Connecting to network... - Csatlakozás a hálózathoz... + Tap on the title bar to filter your chats + + + + No matching chats found. + + + + You can search public chats or create a new chat via the pull-down menu. + Logging out @@ -1289,13 +1317,17 @@ Channel - + %1 members - %1 tag + + %1 tag + - + %1 subscribers - %1 feliratkozott + + %1 feliratkozott + Search Chats @@ -1384,6 +1416,22 @@ Enable storage optimizer + + Focus text input area after send + + + + Focus the text input area after sending a message + + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1411,6 +1459,45 @@ A letöltés nem sikerült. + + VoiceNoteOverlay + + Record a Voice Note + + + + Press the button to start recording + + + + Unavailable + + + + Starting + + + + Recording + + + + Stopping + + + + Use recording + + + + Voice Note (%1) + + + + Ready + + + WebPagePreview diff --git a/translations/harbour-fernschreiber-it.ts b/translations/harbour-fernschreiber-it.ts index 50911b1..03a0be9 100644 --- a/translations/harbour-fernschreiber-it.ts +++ b/translations/harbour-fernschreiber-it.ts @@ -131,14 +131,6 @@ Leaving chat Lascia chat - - Unmute Chat - Riattiva suoni chat - - - Mute Chat - Silenzia chat - Unknown Sconosciuto @@ -191,6 +183,14 @@ New Secret Chat Nuova chat segreta + + Unmute Chat + Riattiva suoni chat + + + Mute Chat + Silenzia chat + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You Tu - - Unmute Chat - Riattiva suoni chat - - - Mute Chat - Silenzia chat - User Info Info utente @@ -274,15 +266,31 @@ Mark chat as unread - + Segna chat come non letta Draft - + Bozza Mark chat as read - + Segna chat come letta + + + Unpin chat + Togli chat in evidenza + + + Pin chat + Metti chat in evidenza + + + Unmute chat + Riattiva suoni chat + + + Mute chat + Silenzia chat @@ -413,11 +421,19 @@ Search in Chat - + Cerca nella chat Search in chat... - + Cerca nella chat... + + + Location: Obtaining position... + Posizione: ottengo posizione... + + + Location (%1/%2) + Posizione(%1/%2) @@ -484,6 +500,10 @@ Open Document Apri documento + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -795,21 +815,21 @@ has added %1 to the chat - + ha aggiunto %1 alla chat has removed %1 from the chat - + ha rimosso %1 dalla chat have added %1 to the chat myself - + hai aggiunto %1 alla chat have removed %1 from the chat myself - + hai rimosso %1 dalla chat @@ -948,11 +968,11 @@ Message unpinned - Messaggio non più in evidenza + Messaggio non più in evidenza Unpin Message - + Togli messaggio in evidenza @@ -1070,19 +1090,31 @@ Filter your chats... - + Filtra le chat... Search Chats - + Ricerca chat Download of %1 successful. - Download di %1 completato. + Download di %1 completato. Download failed. - Download non riuscito. + Download non riuscito. + + + Tap on the title bar to filter your chats + Clicca sulla barra del titolo per filtrare le tue chat + + + No matching chats found. + Nessuna chat corrispondente. + + + You can search public chats or create a new chat via the pull-down menu. + Puoi creare una nuova chat o cercare chat pubbliche dal menu a trascinamento. Connecting to network... @@ -1290,43 +1322,49 @@ SearchChatsPage No chats found. - + Nessuna chat trovata. Searching chats... - + Ricerca chat... Private Chat - Chat privata + Chat privata Group - + Gruppo Channel - + Canale - + %1 members - %1 membro + + %1 membro + %1 membri + - + %1 subscribers - %1 abbonato + + %1 abbonato + %1 abbonati + Search Chats - + Cerca chat Search a chat... - + Cerca chat... Enter your query to start searching (at least 5 characters needed) - + Scrivi il testo che vuoi cercare (almeno 5 caratteri) @@ -1403,6 +1441,22 @@ Enable storage optimizer Abilita ottimizzazione memoria + + Focus text input area after send + Tastiera in primo piano dopo invio + + + Focus the text input area after sending a message + Mantieni la tastiera in primo piano dopo aver inviato un messaggio + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1430,6 +1484,45 @@ Download non riuscito. + + VoiceNoteOverlay + + Record a Voice Note + Registra una nota vocale + + + Press the button to start recording + Premi il pulsante per iniziare la registrazione + + + Unavailable + Non disponibile + + + Starting + Inizia + + + Recording + In registrazione + + + Stopping + Stop + + + Use recording + Usa registrazione + + + Voice Note (%1) + Nota vocale (%1) + + + Ready + Pronto + + WebPagePreview @@ -1815,21 +1908,21 @@ has added %1 to the chat - + ha aggiunto %1 alla chat has removed %1 from the chat - + ha rimosso %1 dalla chat have added %1 to the chat myself - + hai aggiunto %1 alla chat have removed %1 from the chat myself - + hai rimosso %1 dalla chat diff --git a/translations/harbour-fernschreiber-pl.ts b/translations/harbour-fernschreiber-pl.ts index f3c30d7..953b8e8 100644 --- a/translations/harbour-fernschreiber-pl.ts +++ b/translations/harbour-fernschreiber-pl.ts @@ -105,14 +105,6 @@ ChatInformationPageContent - - Unmute Chat - Wyłącz wyciszenie czatu - - - Mute Chat - Wycisz czat - Unknown Nieznany @@ -194,6 +186,14 @@ New Secret Chat Nowy tajny czat + + Unmute Chat + Wyłącz wyciszenie czatu + + + Mute Chat + Wycisz czat + ChatInformationTabItemMembersGroups @@ -251,14 +251,6 @@ You Ty - - Unmute Chat - Wyłącz wyciszenie czatu - - - Mute Chat - Wycisz czat - User Info Informacje o użytkowniku @@ -277,16 +269,32 @@ Mark chat as unread - + Oznacz czat jako nieprzeczytany Draft - + Kopia robocza Mark chat as read + Oznacz czat jako przeczytany + + + Unpin chat + + Pin chat + + + + Unmute chat + Wyłącz wyciszenie czatu + + + Mute chat + Wycisz czat + ChatPage @@ -423,11 +431,19 @@ Search in Chat - + Wyszukaj w czacie Search in chat... - + Wyszukaj w czacie + + + Location: Obtaining position... + Lokalizacja: Uzyskanie pozycji ... + + + Location (%1/%2) + Lokalizacja (%1/%2) @@ -494,6 +510,10 @@ Open Document Otwórz dokument + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -805,21 +825,21 @@ has added %1 to the chat - + dodał %1 do czatu has removed %1 from the chat - + usunął %1 z czatu have added %1 to the chat myself - + dodsałem %1 do czatu have removed %1 from the chat myself - + usunąłem %1 z czatu @@ -958,11 +978,11 @@ Message unpinned - Wiadomość opięta + Wiadomość odpięta Unpin Message - + Odepnij wiadomość @@ -1081,19 +1101,31 @@ Filter your chats... - + Filtruj swoje czaty... Search Chats - + Wyszukaj czaty Download of %1 successful. - + Pobieranie %1 zakończone sukcesem Download failed. - + Nieudane pobranie + + + Tap on the title bar to filter your chats + Dotknij paska tytułowego, aby filtrować swoje czaty + + + No matching chats found. + Brak pasujących czatów + + + You can search public chats or create a new chat via the pull-down menu. + Możesz przeszukiwać czaty publiczne lub utworzyć nowy czat za pomocą menu rozwijanego z góry. Connecting to network... @@ -1309,43 +1341,51 @@ SearchChatsPage No chats found. - + Brak pasujących czatów Searching chats... - + Wyszukiwanie czatów... Private Chat - Prywatny czat + Prywatny czat Group - + Grupa Channel - + Kanał - + %1 members - %1 członek + + %1 członek + %1 członków + %1 członków + - + %1 subscribers - %1 subskrybent + + %1 subskrybent + %1 subskrybentów + %1 subskrybentów + Search Chats - + Wyszukaj czaty Search a chat... - + Wyszukaj czat... Enter your query to start searching (at least 5 characters needed) - + Wprowadź zapytanie aby zacząć wyszukiwanie (minimum 5 znaków) @@ -1422,6 +1462,22 @@ Enable storage optimizer Włącz optymalizację pamięci + + Focus text input area after send + Po wysłaniu zaznacz pole wprowadzania tekstu + + + Focus the text input area after sending a message + Po wysłaniu wiadomości zaznacz pole wprowadzania tekstu + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1449,6 +1505,45 @@ Nieudane pobieranie + + VoiceNoteOverlay + + Record a Voice Note + Nagraj notatkę głosową + + + Press the button to start recording + Naciśnij przycisk, aby zacząć nagrywać + + + Unavailable + Niedostepne + + + Starting + Uruchamianie + + + Recording + Nagrywanie + + + Stopping + Zatrzymywanie + + + Use recording + Użyj nagrywania + + + Voice Note (%1) + Notatka głosowa (%1) + + + Ready + Gotowy + + WebPagePreview @@ -1834,21 +1929,21 @@ has added %1 to the chat - + dodał %1 do czatu has removed %1 from the chat - + usunął %1 z czatu have added %1 to the chat myself - + dodałem %1 do czatu have removed %1 from the chat myself - + usunąłem %1 z czatu diff --git a/translations/harbour-fernschreiber-ru.ts b/translations/harbour-fernschreiber-ru.ts index 8d1ec48..e8385c5 100644 --- a/translations/harbour-fernschreiber-ru.ts +++ b/translations/harbour-fernschreiber-ru.ts @@ -123,59 +123,51 @@ Leave Chat - Выйти из чата + Выйти из чата Join Chat - Зайти в чат + Зайти в чат Leaving chat - Выход из чата - - - Unmute Chat - Включить уведомления - - - Mute Chat - Выключить уведомления + Выход из чата Unknown - Неизвестный + Неизвестный Chat Title group title header - Заголовок чата + Заголовок чата Enter 1-128 characters - Введите 1-128 символов + Введите 1-128 символов There is no information text available, yet. - Информация отсутствует + Информация отсутствует Info group or user infotext header - Информация + Информация Phone Number user phone number header - Номер телефона + Номер телефона Invite Link header - Ссылка для приглашения + Ссылка для приглашения The Invite Link has been copied to the clipboard. - Ссылка для приглашения скопирована в буффер обмена + Ссылка для приглашения скопирована в буффер обмена %1, %2 @@ -194,6 +186,14 @@ New Secret Chat Новый секретный чат + + Unmute Chat + Включить уведомления + + + Mute Chat + Выключить уведомления + ChatInformationTabItemMembersGroups @@ -228,17 +228,17 @@ Groups Button: groups in common (short) - Группы + Группы Members Button: Group Members - Участники группы + Участники группы Settings Button: Chat Settings - Настройки + Настройки @@ -251,14 +251,6 @@ You Вы - - Unmute Chat - Включить уведомления - - - Mute Chat - Выключить уведомления - User Info Информация о пользователе @@ -277,16 +269,32 @@ Mark chat as unread - + Отметить чат как непрочитанный Draft - + Черновик Mark chat as read + Отметить чат как прочитанный + + + Unpin chat + + Pin chat + + + + Unmute chat + Включить уведомления + + + Mute chat + Выключить уведомления + ChatPage @@ -423,11 +431,19 @@ Search in Chat - + Найти в Чате Search in chat... - + Поиск... + + + Location: Obtaining position... + Определение координат... + + + Location (%1/%2) + Местоположение (%1/%2) @@ -438,18 +454,18 @@ You don't have any chats yet. - Тут пока ничего нет + Тут пока ничего нет CoverPage unread message - непрочитанное сообщение + сообщение unread messages - непрочитанных сообщений + сообщений in @@ -469,7 +485,7 @@ Connected - Подключен + В сети Updating content... @@ -477,11 +493,11 @@ chat - чат + чате chats - чаты + чатах @@ -494,6 +510,10 @@ Open Document Открыть документ + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -805,21 +825,21 @@ has added %1 to the chat - + %1 добавлен в чат has removed %1 from the chat - + %1 удалён из чата have added %1 to the chat myself - + %1 добавлены в чат have removed %1 from the chat myself - + %1 удалены из чата @@ -958,11 +978,11 @@ Message unpinned - Сообщение откреплено + Сообщение откреплено Unpin Message - + Открепить сообщение @@ -991,7 +1011,7 @@ You don't have any contacts. - У вас нет никаких контактов. + У вас нет никаких контактов. Private Chat @@ -1081,19 +1101,31 @@ Filter your chats... - + Выбрать чат... Search Chats - + Найти Чаты Download of %1 successful. - Успешно скачано %1. + Успешно скачано %1. Download failed. - Ошибка скачивания. + Ошибка скачивания. + + + Tap on the title bar to filter your chats + Коснитесь строки заголовка, чтобы отфильтровать ваши чаты + + + No matching chats found. + Совпадающих чатов не найдено. + + + You can search public chats or create a new chat via the pull-down menu. + Вы можете искать публичные чаты или создать новый чат с помощью выпадающего меню Connecting to network... @@ -1309,43 +1341,51 @@ SearchChatsPage No chats found. - + Чаты не найдены Searching chats... - + Идёт поиск чатов... Private Chat - Приватный Чат + Приватный Чат Group - + Группа Channel - + Канал - + %1 members - %1 участников + + %1 участников + + + - + %1 subscribers - %1 подписчиков + + %1 подписчиков + + + Search Chats - + Найти Чаты Search a chat... - + Поиск... Enter your query to start searching (at least 5 characters needed) - + Введите не менее 5 символов, чтобы начать поиск @@ -1422,6 +1462,22 @@ Enable storage optimizer Включить оптимизацию хранилища + + Focus text input area after send + Приоритет фокусировки при разговоре в чате + + + Focus the text input area after sending a message + Сфокусироваться на поле ввода текста после отправки сообщения + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1449,6 +1505,45 @@ Ошибка скачивания. + + VoiceNoteOverlay + + Record a Voice Note + Голосовое сообщение + + + Press the button to start recording + Коснитесь кнопки для записи + + + Unavailable + Недоступно + + + Starting + Пуск + + + Recording + Запись + + + Stopping + Стоп + + + Use recording + Использовать запись + + + Voice Note (%1) + Аудиозаметка (%1) + + + Ready + Готов + + WebPagePreview @@ -1834,21 +1929,21 @@ has added %1 to the chat - + %1 добавлен в чат has removed %1 from the chat - + %1 удалён из чата have added %1 to the chat myself - + %1 добавлены в чат have removed %1 from the chat myself - + %1 удалены из чата diff --git a/translations/harbour-fernschreiber-sv.ts b/translations/harbour-fernschreiber-sv.ts index 16cc679..80230eb 100644 --- a/translations/harbour-fernschreiber-sv.ts +++ b/translations/harbour-fernschreiber-sv.ts @@ -131,14 +131,6 @@ Leaving chat Lämnar chatten - - Unmute Chat - Slå på chatten - - - Mute Chat - Stäng av chatten - Unknown Okänd @@ -191,6 +183,14 @@ New Secret Chat Ny hemlig chatt + + Unmute Chat + Slå på chatten + + + Mute Chat + Stäng av chatten + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You Du - - Unmute Chat - Slå på chatten - - - Mute Chat - Stäng av chatten - User Info Användarinfo @@ -278,12 +270,28 @@ Draft - + Utkast Mark chat as read Markera chatten som läst + + Unpin chat + + + + Pin chat + + + + Unmute chat + Slå på chatten + + + Mute chat + Stäng av chatten + ChatPage @@ -419,6 +427,14 @@ Search in chat... Sök i chatten... + + Location: Obtaining position... + Plats: Hämtar position... + + + Location (%1/%2) + Plats (%1/%2) + ChatSelectionPage @@ -484,6 +500,10 @@ Open Document Öppna dokument + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -1085,8 +1105,16 @@ Nerladdning misslyckades. - Connecting to network... - Ansluter till nätverket... + Tap on the title bar to filter your chats + Tryck på titelfältet för att filtrera dina chattar + + + No matching chats found. + Ingen passande chatt hittades. + + + You can search public chats or create a new chat via the pull-down menu. + Du kan söka efter allmänna chattar eller skapa en ny chatt via toppmenyn. Logging out @@ -1308,13 +1336,19 @@ Channel Kanal - + %1 members - %1 medlemmar + + %1 medlem + %1 medlemmar + - + %1 subscribers - %1 prenumeranter + + %1 prenumerant + %1 prenumeranter + Search Chats @@ -1403,6 +1437,22 @@ Enable storage optimizer Aktivera lagringsoptimering + + Focus text input area after send + Fokusera textinmatningsfältet efter sändning + + + Focus the text input area after sending a message + Fokusera textinmatningsfältet efter att ett meddelande skickats + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1430,6 +1480,45 @@ Nerladdning misslyckades. + + VoiceNoteOverlay + + Record a Voice Note + Spela in ett röstmeddelande + + + Press the button to start recording + Tryck på knappen för att starta inspelning + + + Unavailable + Ej tillgänglig + + + Starting + Startar + + + Recording + Spelar in + + + Stopping + Stoppar + + + Use recording + Använd inspelning + + + Voice Note (%1) + Röstmeddelande (%1) + + + Ready + Klar + + WebPagePreview diff --git a/translations/harbour-fernschreiber-zh_CN.ts b/translations/harbour-fernschreiber-zh_CN.ts index 9b057bf..6dc6501 100644 --- a/translations/harbour-fernschreiber-zh_CN.ts +++ b/translations/harbour-fernschreiber-zh_CN.ts @@ -129,14 +129,6 @@ Leaving chat 正在离开对话 - - Unmute Chat - 取消对话静音 - - - Mute Chat - 静音对话 - Unknown 未知 @@ -188,6 +180,14 @@ New Secret Chat 新加密对话 + + Unmute Chat + 取消对话静音 + + + Mute Chat + 静音对话 + ChatInformationTabItemMembersGroups @@ -245,14 +245,6 @@ You - - Unmute Chat - 取消对话静音 - - - Mute Chat - 静音对话 - User Info 用户信息 @@ -271,16 +263,32 @@ Mark chat as unread - + 标记此对话为未读 Draft - + 草稿 Mark chat as read + 标记为已读 + + + Unpin chat + + Pin chat + + + + Unmute chat + 取消对话静音 + + + Mute chat + 静音对话 + ChatPage @@ -403,11 +411,19 @@ Search in Chat - + 对话内搜索 Search in chat... - + 正在搜索对话内容… + + + Location: Obtaining position... + 位置:正在获取位置… + + + Location (%1/%2) + 位置 (%1/%2) @@ -425,7 +441,8 @@ CoverPage unread message - 未读消息 + 未读 +消息 unread messages @@ -474,6 +491,10 @@ Open Document 打开文档 + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -574,7 +595,7 @@ sent a voice note - 发送语言消息 + 发送语音消息 sent a document @@ -785,21 +806,21 @@ has added %1 to the chat - + 已加入 %1 到此对话 has removed %1 from the chat - + 已从此对话移除 %1 have added %1 to the chat myself - + 已加入 %1 到此对话 have removed %1 from the chat myself - + 已从此对话移除 %1 @@ -1059,11 +1080,11 @@ Filter your chats... - + 筛选你的对话… Search Chats - + 搜索对话 Download of %1 successful. @@ -1074,8 +1095,16 @@ 下载失败。 - Connecting to network... - 正在连接到网络… + Tap on the title bar to filter your chats + 点击顶部状态栏即可筛选你的对话 + + + No matching chats found. + 没有找到匹配的对话。 + + + You can search public chats or create a new chat via the pull-down menu. + 你可以搜索公共对话或通过下拉菜单创建新对话。 Logging out @@ -1271,43 +1300,47 @@ SearchChatsPage No chats found. - + 没有找到对话。 Searching chats... - + 正在搜索对话… Private Chat - 个人对话 + 个人对话 Group - + 群组 Channel - + 频道 - + %1 members - %1 位成员 + + %1 位成员 + - + %1 subscribers - %1 位订阅者 + + %1 位订阅者 + Search Chats - + 搜索对话 Search a chat... - + 搜索对话… Enter your query to start searching (at least 5 characters needed) - + 输入你要搜索的内容(至少需要输入5个字符) @@ -1384,6 +1417,22 @@ Enable storage optimizer 启用储存加速器 + + Focus text input area after send + 发送后聚焦文本输入区域 + + + Focus the text input area after sending a message + 发送消息后聚焦文本输入区域 + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1411,6 +1460,45 @@ 下载失败 + + VoiceNoteOverlay + + Record a Voice Note + 录制语音消息 + + + Press the button to start recording + 按下按钮即可开始录音 + + + Unavailable + 不可用 + + + Starting + 正在启动 + + + Recording + 正在录音 + + + Stopping + 正在停止 + + + Use recording + 使用录音 + + + Voice Note (%1) + 语音消息 (%1) + + + Ready + 就绪 + + WebPagePreview @@ -1788,7 +1876,7 @@ Closed! - 已关闭! + 已关闭! Pending acknowledgement @@ -1796,21 +1884,21 @@ has added %1 to the chat - + 已加入 %1 到此对话 has removed %1 from the chat - + 已从此对话移除 %1 have added %1 to the chat myself - + 已加入 %1 到此对话 have removed %1 from the chat myself - + 已从此对话移除 %1 diff --git a/translations/harbour-fernschreiber.ts b/translations/harbour-fernschreiber.ts index 99c085d..0ae37b2 100644 --- a/translations/harbour-fernschreiber.ts +++ b/translations/harbour-fernschreiber.ts @@ -131,14 +131,6 @@ Leaving chat Leaving chat - - Unmute Chat - Unmute Chat - - - Mute Chat - Mute Chat - Unknown Unknown @@ -191,6 +183,14 @@ New Secret Chat + + Unmute Chat + Unmute Chat + + + Mute Chat + Mute Chat + ChatInformationTabItemMembersGroups @@ -248,14 +248,6 @@ You You - - Unmute Chat - Unmute Chat - - - Mute Chat - Mute Chat - User Info User Info @@ -284,6 +276,22 @@ Draft + + Unpin chat + + + + Pin chat + + + + Unmute chat + Unmute chat + + + Mute chat + Mute chat + ChatPage @@ -419,6 +427,14 @@ Search in chat... + + Location: Obtaining position... + + + + Location (%1/%2) + + ChatSelectionPage @@ -484,6 +500,10 @@ Open Document Open Document + + Copy Document to Downloads + + EditGroupChatPermissionsColumn @@ -1085,8 +1105,16 @@ Download failed. - Connecting to network... - Connecting to network... + Tap on the title bar to filter your chats + + + + No matching chats found. + + + + You can search public chats or create a new chat via the pull-down menu. + Logging out @@ -1308,13 +1336,19 @@ Channel - + %1 members - %1 member + + %1 member + + - + %1 subscribers - %1 subscriber + + %1 subscriber + + Search Chats @@ -1403,6 +1437,22 @@ Enable storage optimizer + + Focus text input area after send + + + + Focus the text input area after sending a message + + + + Enable online-only mode + + + + Disables offline caching. Certain features may be limited or missing in this mode. Changes require a restart of Fernschreiber to take effect. + + StickerPicker @@ -1430,6 +1480,45 @@ Download failed. + + VoiceNoteOverlay + + Record a Voice Note + + + + Press the button to start recording + + + + Unavailable + + + + Starting + + + + Recording + + + + Stopping + + + + Use recording + + + + Voice Note (%1) + + + + Ready + + + WebPagePreview