From cf00abc03e01849b2d322ab7ae39cc7c85cf740f Mon Sep 17 00:00:00 2001 From: Alexander von Gluck IV Date: Wed, 3 Feb 2021 14:39:49 -0600 Subject: [PATCH 01/50] cmake: Don't build with Werror on Haiku --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c9e29998..a3a62651 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -645,7 +645,7 @@ if(QML_DEBUGGING) endif() -if(NOT MSVC) +if(NOT MSVC AND NOT HAIKU) if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR CI_BUILD) target_compile_options(nheko PRIVATE "-Werror") endif() From e8ff6c9486d325a8b09a0b0bd191a7e31321e709 Mon Sep 17 00:00:00 2001 From: Alexander von Gluck IV Date: Wed, 3 Feb 2021 14:40:14 -0600 Subject: [PATCH 02/50] notifications/mananger: Follow Linux code paths on Haiku as well --- src/notifications/Manager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index 4c9852cc..2b869efc 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -4,7 +4,7 @@ #include #include -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) #include #include #endif @@ -41,7 +41,7 @@ signals: public slots: void removeNotification(const QString &roomId, const QString &eventId); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) public: void closeNotifications(QString roomId); @@ -61,7 +61,7 @@ private slots: void notificationReplied(uint id, QString reply); }; -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU) QDBusArgument & operator<<(QDBusArgument &arg, const QImage &image); const QDBusArgument & From 7874d61c337d7d24aec918ccdd6876ceaa20f989 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 4 Feb 2021 01:02:38 +0100 Subject: [PATCH 03/50] Fix scheme handler not passing arguments --- resources/nheko.desktop | 2 +- src/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/nheko.desktop b/resources/nheko.desktop index 4404e460..00d7fda5 100644 --- a/resources/nheko.desktop +++ b/resources/nheko.desktop @@ -2,7 +2,7 @@ Name=nheko Version=1.0 Comment=Desktop client for Matrix -Exec=nheko +Exec=nheko %u Icon=nheko Type=Application Categories=Network;InstantMessaging;Qt; diff --git a/src/main.cpp b/src/main.cpp index a890a6fd..07962b9b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -128,7 +128,7 @@ main(int argc, char *argv[]) // parsed before the SingleApplication userdata is set. QString userdata{""}; QString matrixUri; - for (int i = 0; i < argc; ++i) { + for (int i = 1; i < argc; ++i) { QString arg{argv[i]}; if (arg.startsWith("--profile=")) { arg.remove("--profile="); From eae09f8f14c046e12ff6d4123d9a09ed00a82dc0 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:41:00 -0500 Subject: [PATCH 04/50] Fix bug on logout of non-default profile --- src/ChatPage.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index db80ecd5..ac327b82 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -448,15 +448,13 @@ void ChatPage::deleteConfigs() { QSettings settings; + + QString profilePrefix = (UserSettings::instance()->profile() == "default" ? "" : QString("profile/%1").arg(UserSettings::instance()->profile())); + settings.beginGroup(profilePrefix); settings.beginGroup("auth"); settings.remove(""); - settings.endGroup(); - settings.beginGroup("client"); - settings.remove(""); - settings.endGroup(); - settings.beginGroup("notifications"); - settings.remove(""); - settings.endGroup(); + settings.endGroup(); // auth + settings.endGroup(); // profilePrefix http::client()->shutdown(); cache::deleteData(); From 46e15218d4bdc83d27b688b5856fcd18d37ddb1c Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:41:32 -0500 Subject: [PATCH 05/50] Use UserSettings where possible --- src/Utils.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index 5af5748e..1b2808b3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -20,6 +20,7 @@ #include "Cache.h" #include "Config.h" #include "MatrixClient.h" +#include "UserSettingsPage.h" using TimelineEvent = mtx::events::collections::TimelineEvents; @@ -65,14 +66,11 @@ utils::replaceEmoji(const QString &body) QVector utf32_string = body.toUcs4(); - QSettings settings; - QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); - bool insideFontBlock = false; for (auto &code : utf32_string) { if (utils::codepointIsEmoji(code)) { if (!insideFontBlock) { - fmtBody += QString(""); + fmtBody += QString("font() + "\">"); insideFontBlock = true; } @@ -505,13 +503,7 @@ utils::getQuoteBody(const RelatedInfo &related) QString utils::linkColor() { - QSettings settings; - // Default to system theme if QT_QPA_PLATFORMTHEME var is set. - QString defaultTheme = - QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() - ? "light" - : "system"; - const auto theme = settings.value("user/theme", defaultTheme).toString(); + const auto theme = UserSettings::instance()->theme(); if (theme == "light") { return "#0077b5"; From 777b9bf20de698bf9dabd2782c212a3b819c8e51 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:43:33 -0500 Subject: [PATCH 06/50] Set profile to "" if it's the default for compatibility --- src/UserSettingsPage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 96c07d7c..d31c8ef9 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -115,8 +115,8 @@ UserSettings::load(std::optional profile) cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); useStunServer_ = settings.value("user/use_stun_server", false).toBool(); - if (profile) - profile_ = *profile; + if (profile) // set to "" if it's the default to maintain compatibility + profile_ = (*profile == "default") ? "" : *profile; else profile_ = settings.value("user/currentProfile", "").toString(); From 343c9c811630b1321f3f15458c5c802d0c5094eb Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 19:01:48 -0500 Subject: [PATCH 07/50] Don't attempt to begin group "" --- src/ChatPage.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index ac327b82..d8907740 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -449,12 +449,14 @@ ChatPage::deleteConfigs() { QSettings settings; - QString profilePrefix = (UserSettings::instance()->profile() == "default" ? "" : QString("profile/%1").arg(UserSettings::instance()->profile())); - settings.beginGroup(profilePrefix); + if (UserSettings::instance()->profile() != "") + { + settings.beginGroup("profile"); + settings.beginGroup(UserSettings::instance()->profile()); + } settings.beginGroup("auth"); settings.remove(""); settings.endGroup(); // auth - settings.endGroup(); // profilePrefix http::client()->shutdown(); cache::deleteData(); From f02342fe227c6ab05e94d6ac72d05dd0b6f127a2 Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Fri, 5 Feb 2021 21:52:49 +0530 Subject: [PATCH 08/50] close emoji autocompleter if space typed after : issue #433 and adds default option for emoji font family settings --- resources/qml/MessageInput.qml | 4 ++++ src/UserSettingsPage.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 5d335872..5a9cfd33 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -163,6 +163,10 @@ Rectangle { TimelineManager.timeline.input.paste(false); event.accepted = true; } else if (event.key == Qt.Key_Space) { + // close popup if user enters space after colon + if(cursorPosition == completerTriggeredAt + 1) + popup.close(); + if (popup.opened && popup.count <= 0) popup.close(); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 96c07d7c..5bff1323 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -725,6 +725,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge // TODO: Is there a way to limit to just emojis, rather than // all emoji fonts? auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + emojiFontSelectionCombo_->addItem(QString("default")); for (const auto &family : emojiFamilies) { emojiFontSelectionCombo_->addItem(family); } From 4aefac08a4b57e6fb344cd8cffb0b6e32698e0ba Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Fri, 5 Feb 2021 22:42:08 +0530 Subject: [PATCH 09/50] focus message input on adding emoji/reacting to a message --- resources/qml/MessageInput.qml | 6 ++++++ resources/qml/emoji/EmojiButton.qml | 1 + src/timeline/TimelineViewManager.cpp | 6 ++++++ src/timeline/TimelineViewManager.h | 2 ++ 4 files changed, 15 insertions(+) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 5a9cfd33..9a83b52b 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -270,6 +270,11 @@ Rectangle { target: TimelineManager.timeline } + Connections { + target: TimelineManager + onFocusInput: messageInput.forceActiveFocus() + } + MouseArea { // workaround for wrong cursor shape on some platforms anchors.fill: parent @@ -297,6 +302,7 @@ Rectangle { ToolTip.text: qsTr("Emoji") onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { messageInput.insert(messageInput.cursorPosition, emoji); + TimelineManager.focusMessageInput() }) } diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml index 928d6226..622f8aa2 100644 --- a/resources/qml/emoji/EmojiButton.qml +++ b/resources/qml/emoji/EmojiButton.qml @@ -14,5 +14,6 @@ ImageButton { image: ":/icons/icons/ui/smile.png" onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { TimelineManager.queueReactionMessage(event_id, emoji); + TimelineManager.focusMessageInput() }) } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 93451976..9ca08e06 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -545,3 +545,9 @@ TimelineViewManager::queueCallMessage(const QString &roomid, { models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } + +void +TimelineViewManager::focusMessageInput() +{ + emit focusInput(); +} \ No newline at end of file diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 74128865..7c994a14 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -66,6 +66,7 @@ public: Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); Q_INVOKABLE void openMemberListDialog() const; Q_INVOKABLE void openLeaveRoomDialog() const; @@ -87,6 +88,7 @@ signals: void showRoomList(); void narrowViewChanged(); void focusChanged(); + void focusInput(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); From 87122ffe77eadc43964874d7c904443eea30965a Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 5 Feb 2021 22:30:06 -0500 Subject: [PATCH 10/50] Translated using Weblate (Hungarian) Currently translated at 91.8% (406 of 442 strings) Co-authored-by: maxigaz Translate-URL: https://weblate.nheko.im/projects/nheko/nheko-master/hu/ Translation: Nheko/nheko --- resources/langs/nheko_hu.ts | 96 +++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index acf5865f..c00dc096 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -1881,22 +1881,22 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Self signing key - + Önaláírókulcs The key to verify your own devices. If it is cached, verifying one of your devices will mark it verified for all your other devices and for users, that have verified you. - + A kulcs, amellyel hitelesítheted a saját eszközeidet. Ha gyorsítótárazva van, egy eszköz hitelesítése azt hitelesítettnek fogja megjelölni az összes többi eszközeidnek és azoknak a felhasználóidnak, akik már hitelesítettek téged. Backup key - + Biztonsági mentési kulcs The key to decrypt online key backups. If it is cached, you can enable online key backup to store encryption keys securely encrypted on the server. - + A kulcs, amellyel hozzáférhetsz a kulcsok online biztonsági mentéseihez. Ha gyorsítótárazva van, lehetőséged van biztonsági másolatként, titkosítva a szerveren tárolni a titkosítási kulcsokat. @@ -1911,7 +1911,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Open Sessions File - + Munkameneti fájl megnyitása @@ -1921,34 +1921,34 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Error - + Hiba File Password - + Fájljelszó Enter the passphrase to decrypt the file: - + Írd be a jelmondatot a fájl titkosításának feloldásához: The password cannot be empty - + A jelszó nem lehet üres Enter passphrase to encrypt your session keys: - + Írd be a jelmondatot a munkameneti kulcsok titkosításához: File to save the exported session keys - + Exportált munkameneti kulcsok mentése fájlba @@ -1956,17 +1956,17 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Waiting for other party… - + Várakozás a másik félre… Waiting for other side to accept the verification request. - + Várakozás a másik oldalra, hogy elfogadja a hitelesítési kérelmet. Waiting for other side to continue the verification process. - + Várakozás a másik oldalra a hitelesítés folytatásához. @@ -1984,12 +1984,12 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Welcome to nheko! The desktop client for the Matrix protocol. - + Üdvözöl a Nheko, egy asztali kliens a Matrix protokollhoz! Enjoy your stay! - + Érezd jól magad nálunk! @@ -2007,7 +2007,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Yesterday - + Tegnap @@ -2015,7 +2015,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Create room - + Szoba létrehozása @@ -2035,22 +2035,22 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Alias - + Álnév Room Visibility - + Szoba láthatósága Room Preset - + Szoba jellege Direct Chat - + Közvetlen csevegés @@ -2068,7 +2068,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Confirm - + Megerősítés @@ -2086,7 +2086,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő User ID to invite - + Meghívandó felhasználó azonosítója @@ -2094,7 +2094,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Join - + Csatlakozás @@ -2104,7 +2104,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Room ID or alias - + Szoba azonosítója vagy álneve @@ -2117,7 +2117,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Are you sure you want to leave? - + Biztosan távozni akarsz? @@ -2130,7 +2130,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Logout. Are you sure? - + Biztosan kijelentkezel? @@ -2138,7 +2138,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Upload - + Feltöltés @@ -2150,7 +2150,9 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Media type: %1 Media size: %2 - + Média típusa: %1 +Média mérete: %2 + @@ -2163,12 +2165,12 @@ Media size: %2 Confirm - + Megerősítés Solve the reCAPTCHA and press the confirm button - + Oldd meg a reCAPTCHA feladványát, és nyomd meg a „Megerősítés” gombot @@ -2189,12 +2191,12 @@ Media size: %2 Today %1 - + Ma %1 Yesterday %1 - + Tegnap %1 @@ -2207,69 +2209,69 @@ Media size: %2 Info - + Infó Internal ID - + Belső azonosító Room Version - + Szoba verziója Notifications - + Értesítések Muted - + Némítva Mentions only - + Csak megemlítéskor All messages - + Mindegyik üzenetnél Room access - + Szoba hozzáférhetősége Anyone and guests - + Bárkinek és vendégeknek Anyone - + Bárkinek, aki ismeri a szoba linkjét (vendégek nem) Invited users - + Meghívott felhasználók Encryption - + Titkosítás Whether or not the client should respond automatically with the session keys upon request. Use with caution, this is a temporary measure to test the E2E implementation until device verification is completed. - + Válaszoljon-e kérés esetén a kliens munkamenetkulcsokkal automatikusan. Óvatosan használandó, mivel ez csak egy átmeneti megoldás a végponttól végpontig (E2E) titkosítás teszteléséhez, amíg be nincs fejezve az eszközhitelesítés. From 8d195a4d11d6dc0584a626e72581782b494bfc8e Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sat, 6 Feb 2021 10:24:41 +0530 Subject: [PATCH 11/50] translation fix for default text in emoji combo --- src/UserSettingsPage.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 5bff1323..f9885abd 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -93,7 +93,7 @@ UserSettings::load(std::optional profile) sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); readReceipts_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); - font_ = settings.value("user/font_family", "default").toString(); + font_ = settings.value("user/font_family", tr("Default")).toString(); avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); @@ -101,7 +101,7 @@ UserSettings::load(std::optional profile) shareKeysWithTrustedUsers_ = settings.value("user/share_keys_with_trusted_users", true).toBool(); mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); + emojiFont_ = settings.value("user/emoji_font_family", tr("Default")).toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); auto tempPresence = settings.value("user/presence", "").toString().toStdString(); auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); @@ -725,7 +725,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge // TODO: Is there a way to limit to just emojis, rather than // all emoji fonts? auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); - emojiFontSelectionCombo_->addItem(QString("default")); + emojiFontSelectionCombo_->addItem(tr("Default")); for (const auto &family : emojiFamilies) { emojiFontSelectionCombo_->addItem(family); } From 375e20462ba3641db3b71c12a66533c63ac2d597 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 7 Feb 2021 02:01:25 +0100 Subject: [PATCH 12/50] Native rendering breaks kerning --- src/timeline/TimelineViewManager.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 93451976..dab735db 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -170,10 +170,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->setResizeMode(QQuickWidget::SizeRootObjectToView); container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) - view->quickWindow()->setTextRenderType(QQuickWindow::NativeTextRendering); -#endif - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { nhlog::ui()->debug("Status changed to {}", status); }); From c2a56fc23301967927ac019db0ac8c2ad99618ac Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sun, 7 Feb 2021 22:15:06 +0530 Subject: [PATCH 13/50] emoji default translation fix --- src/UserSettingsPage.cpp | 21 +++++++++++++++++---- src/UserSettingsPage.h | 8 +++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index f9885abd..bf1bdb86 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -93,7 +93,7 @@ UserSettings::load(std::optional profile) sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); readReceipts_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); - font_ = settings.value("user/font_family", tr("Default")).toString(); + font_ = settings.value("user/font_family", "").toString(); avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); @@ -101,7 +101,7 @@ UserSettings::load(std::optional profile) shareKeysWithTrustedUsers_ = settings.value("user/share_keys_with_trusted_users", true).toBool(); mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", tr("Default")).toString(); + emojiFont_ = settings.value("user/emoji_font_family", "Default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); auto tempPresence = settings.value("user/presence", "").toString().toStdString(); auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); @@ -341,7 +341,13 @@ UserSettings::setEmojiFontFamily(QString family) { if (family == emojiFont_) return; - emojiFont_ = family; + + if (family == tr("Default")) { + emojiFont_ = "Default"; + } else { + emojiFont_ = family; + } + emit emojiFontChanged(family); save(); } @@ -730,7 +736,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge emojiFontSelectionCombo_->addItem(family); } - fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(settings_->font())); + QString currentFont = settings_->font(); + if (currentFont == "Default") { + fontSelectionCombo_->setCurrentIndex( + fontSelectionCombo_->findText(tr(currentFont.toStdString().c_str()))); + } else { + fontSelectionCombo_->setCurrentIndex( + fontSelectionCombo_->findText(currentFont)); + } emojiFontSelectionCombo_->setCurrentIndex( emojiFontSelectionCombo_->findText(settings_->emojiFont())); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index b65e1efc..ab82c282 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -177,7 +177,13 @@ public: int timelineMaxWidth() const { return timelineMaxWidth_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } - QString emojiFont() const { return emojiFont_; } + QString emojiFont() const { + if (emojiFont_ == "Default") { + return tr("Default"); + } + + return emojiFont_; + } Presence presence() const { return presence_; } QString ringtone() const { return ringtone_; } QString microphone() const { return microphone_; } From f1bc3ba587baacce84acbdb04343d5c4b74d18a2 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 11:47:47 -0500 Subject: [PATCH 14/50] Move call device handling out of WebRTCSession --- CMakeLists.txt | 2 + src/CallDevices.cpp | 433 +++++++++++++++++++++++++++++++++++++++ src/CallDevices.h | 45 ++++ src/CallManager.cpp | 24 +-- src/CallManager.h | 3 +- src/UserSettingsPage.cpp | 17 +- src/WebRTCSession.cpp | 370 +-------------------------------- src/WebRTCSession.h | 11 +- 8 files changed, 509 insertions(+), 396 deletions(-) create mode 100644 src/CallDevices.cpp create mode 100644 src/CallDevices.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c9e29998..34330147 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -298,6 +298,7 @@ set(SRC_FILES src/AvatarProvider.cpp src/BlurhashProvider.cpp src/Cache.cpp + src/CallDevices.cpp src/CallManager.cpp src/ChatPage.cpp src/ColorImageProvider.cpp @@ -512,6 +513,7 @@ qt5_wrap_cpp(MOC_HEADERS src/AvatarProvider.h src/BlurhashProvider.h src/Cache_p.h + src/CallDevices.h src/CallManager.h src/ChatPage.h src/CommunitiesList.h diff --git a/src/CallDevices.cpp b/src/CallDevices.cpp new file mode 100644 index 00000000..32ae6e69 --- /dev/null +++ b/src/CallDevices.cpp @@ -0,0 +1,433 @@ +#include +#include +#include + +#include "CallDevices.h" +#include "ChatPage.h" +#include "Logging.h" +#include "UserSettingsPage.h" + +#ifdef GSTREAMER_AVAILABLE +extern "C" +{ +#include "gst/gst.h" +} +#endif + +CallDevices::CallDevices() + : QObject() +{} + +#ifdef GSTREAMER_AVAILABLE +namespace { + +struct AudioSource +{ + std::string name; + GstDevice *device; +}; + +struct VideoSource +{ + struct Caps + { + std::string resolution; + std::vector frameRates; + }; + std::string name; + GstDevice *device; + std::vector caps; +}; + +std::vector audioSources_; +std::vector videoSources_; + +using FrameRate = std::pair; +std::optional +getFrameRate(const GValue *value) +{ + if (GST_VALUE_HOLDS_FRACTION(value)) { + gint num = gst_value_get_fraction_numerator(value); + gint den = gst_value_get_fraction_denominator(value); + return FrameRate{num, den}; + } + return std::nullopt; +} + +void +addFrameRate(std::vector &rates, const FrameRate &rate) +{ + constexpr double minimumFrameRate = 15.0; + if (static_cast(rate.first) / rate.second >= minimumFrameRate) + rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); +} + +void +setDefaultDevice(bool isVideo) +{ + auto settings = ChatPage::instance()->userSettings(); + if (isVideo && settings->camera().isEmpty()) { + const VideoSource &camera = videoSources_.front(); + settings->setCamera(QString::fromStdString(camera.name)); + settings->setCameraResolution( + QString::fromStdString(camera.caps.front().resolution)); + settings->setCameraFrameRate( + QString::fromStdString(camera.caps.front().frameRates.front())); + } else if (!isVideo && settings->microphone().isEmpty()) { + settings->setMicrophone(QString::fromStdString(audioSources_.front().name)); + } +} + +void +addDevice(GstDevice *device) +{ + if (!device) + return; + + gchar *name = gst_device_get_display_name(device); + gchar *type = gst_device_get_device_class(device); + bool isVideo = !std::strncmp(type, "Video", 5); + g_free(type); + nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name); + if (!isVideo) { + audioSources_.push_back({name, device}); + g_free(name); + setDefaultDevice(false); + return; + } + + GstCaps *gstcaps = gst_device_get_caps(device); + if (!gstcaps) { + nhlog::ui()->debug("WebRTC: unable to get caps for {}", name); + g_free(name); + return; + } + + VideoSource source{name, device, {}}; + g_free(name); + guint nCaps = gst_caps_get_size(gstcaps); + for (guint i = 0; i < nCaps; ++i) { + GstStructure *structure = gst_caps_get_structure(gstcaps, i); + const gchar *name = gst_structure_get_name(structure); + if (!std::strcmp(name, "video/x-raw")) { + gint widthpx, heightpx; + if (gst_structure_get(structure, + "width", + G_TYPE_INT, + &widthpx, + "height", + G_TYPE_INT, + &heightpx, + nullptr)) { + VideoSource::Caps caps; + caps.resolution = + std::to_string(widthpx) + "x" + std::to_string(heightpx); + const GValue *value = + gst_structure_get_value(structure, "framerate"); + if (auto fr = getFrameRate(value); fr) + addFrameRate(caps.frameRates, *fr); + else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) { + addFrameRate( + caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_min(value))); + addFrameRate( + caps.frameRates, + *getFrameRate(gst_value_get_fraction_range_max(value))); + } else if (GST_VALUE_HOLDS_LIST(value)) { + guint nRates = gst_value_list_get_size(value); + for (guint j = 0; j < nRates; ++j) { + const GValue *rate = + gst_value_list_get_value(value, j); + if (auto fr = getFrameRate(rate); fr) + addFrameRate(caps.frameRates, *fr); + } + } + if (!caps.frameRates.empty()) + source.caps.push_back(std::move(caps)); + } + } + } + gst_caps_unref(gstcaps); + videoSources_.push_back(std::move(source)); + setDefaultDevice(true); +} + +#if GST_CHECK_VERSION(1, 18, 0) +template +bool +removeDevice(T &sources, GstDevice *device, bool changed) +{ + if (auto it = std::find_if(sources.begin(), + sources.end(), + [device](const auto &s) { return s.device == device; }); + it != sources.end()) { + nhlog::ui()->debug(std::string("WebRTC: device ") + + (changed ? "changed: " : "removed: ") + "{}", + it->name); + gst_object_unref(device); + sources.erase(it); + return true; + } + return false; +} + +void +removeDevice(GstDevice *device, bool changed) +{ + if (device) { + if (removeDevice(audioSources_, device, changed) || + removeDevice(videoSources_, device, changed)) + return; + } +} + +gboolean +newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) +{ + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_DEVICE_ADDED: { + GstDevice *device; + gst_message_parse_device_added(msg, &device); + addDevice(device); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_REMOVED: { + GstDevice *device; + gst_message_parse_device_removed(msg, &device); + removeDevice(device, false); + emit CallDevices::instance().devicesChanged(); + break; + } + case GST_MESSAGE_DEVICE_CHANGED: { + GstDevice *device; + GstDevice *oldDevice; + gst_message_parse_device_changed(msg, &device, &oldDevice); + removeDevice(oldDevice, true); + addDevice(device); + break; + } + default: + break; + } + return TRUE; +} +#endif + +template +std::vector +deviceNames(T &sources, const std::string &defaultDevice) +{ + std::vector ret; + ret.reserve(sources.size()); + for (const auto &s : sources) + ret.push_back(s.name); + + // move default device to top of the list + if (auto it = std::find(ret.begin(), ret.end(), defaultDevice); it != ret.end()) + std::swap(ret.front(), *it); + + return ret; +} + +std::optional +getVideoSource(const std::string &cameraName) +{ + if (auto it = std::find_if(videoSources_.cbegin(), + videoSources_.cend(), + [&cameraName](const auto &s) { return s.name == cameraName; }); + it != videoSources_.cend()) { + return *it; + } + return std::nullopt; +} + +std::pair +tokenise(std::string_view str, char delim) +{ + std::pair ret; + ret.first = std::atoi(str.data()); + auto pos = str.find_first_of(delim); + ret.second = std::atoi(str.data() + pos + 1); + return ret; +} + +} + +void +CallDevices::init() +{ +#if GST_CHECK_VERSION(1, 18, 0) + static GstDeviceMonitor *monitor = nullptr; + if (!monitor) { + monitor = gst_device_monitor_new(); + GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); + gst_device_monitor_add_filter(monitor, "Audio/Source", caps); + gst_caps_unref(caps); + caps = gst_caps_new_empty_simple("video/x-raw"); + gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_caps_unref(caps); + + GstBus *bus = gst_device_monitor_get_bus(monitor); + gst_bus_add_watch(bus, newBusMessage, nullptr); + gst_object_unref(bus); + if (!gst_device_monitor_start(monitor)) { + nhlog::ui()->error("WebRTC: failed to start device monitor"); + return; + } + } +#endif +} + +void +CallDevices::refresh() +{ +#if !GST_CHECK_VERSION(1, 18, 0) + + static GstDeviceMonitor *monitor = nullptr; + if (!monitor) { + monitor = gst_device_monitor_new(); + GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); + gst_device_monitor_add_filter(monitor, "Audio/Source", caps); + gst_caps_unref(caps); + caps = gst_caps_new_empty_simple("video/x-raw"); + gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_caps_unref(caps); + } + + auto clearDevices = [](auto &sources) { + std::for_each( + sources.begin(), sources.end(), [](auto &s) { gst_object_unref(s.device); }); + sources.clear(); + }; + clearDevices(audioSources_); + clearDevices(videoSources_); + + GList *devices = gst_device_monitor_get_devices(monitor); + if (devices) { + for (GList *l = devices; l != nullptr; l = l->next) + addDevice(GST_DEVICE_CAST(l->data)); + g_list_free(devices); + } + emit devicesChanged(); +#endif +} + +bool +CallDevices::haveMic() const +{ + return !audioSources_.empty(); +} + +bool +CallDevices::haveCamera() const +{ + return !videoSources_.empty(); +} + +std::vector +CallDevices::names(bool isVideo, const std::string &defaultDevice) const +{ + return isVideo ? deviceNames(videoSources_, defaultDevice) + : deviceNames(audioSources_, defaultDevice); +} + +std::vector +CallDevices::resolutions(const std::string &cameraName) const +{ + std::vector ret; + if (auto s = getVideoSource(cameraName); s) { + ret.reserve(s->caps.size()); + for (const auto &c : s->caps) + ret.push_back(c.resolution); + } + return ret; +} + +std::vector +CallDevices::frameRates(const std::string &cameraName, const std::string &resolution) const +{ + if (auto s = getVideoSource(cameraName); s) { + if (auto it = + std::find_if(s->caps.cbegin(), + s->caps.cend(), + [&](const auto &c) { return c.resolution == resolution; }); + it != s->caps.cend()) + return it->frameRates; + } + return {}; +} + +GstDevice * +CallDevices::audioDevice() const +{ + std::string name = ChatPage::instance()->userSettings()->microphone().toStdString(); + if (auto it = std::find_if(audioSources_.cbegin(), + audioSources_.cend(), + [&name](const auto &s) { return s.name == name; }); + it != audioSources_.cend()) { + nhlog::ui()->debug("WebRTC: microphone: {}", name); + return it->device; + } else { + nhlog::ui()->error("WebRTC: unknown microphone: {}", name); + return nullptr; + } +} + +GstDevice * +CallDevices::videoDevice(std::pair &resolution, std::pair &frameRate) const +{ + auto settings = ChatPage::instance()->userSettings(); + std::string name = settings->camera().toStdString(); + if (auto s = getVideoSource(name); s) { + nhlog::ui()->debug("WebRTC: camera: {}", name); + resolution = tokenise(settings->cameraResolution().toStdString(), 'x'); + frameRate = tokenise(settings->cameraFrameRate().toStdString(), '/'); + nhlog::ui()->debug( + "WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); + nhlog::ui()->debug( + "WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); + return s->device; + } else { + nhlog::ui()->error("WebRTC: unknown camera: {}", name); + return nullptr; + } +} + +#else + +void +CallDevices::refresh() +{} + +bool +CallDevices::haveMic() const +{ + return false; +} + +bool +CallDevices::haveCamera() const +{ + return false; +} + +std::vector +CallDevices::names(bool, const std::string &) const +{ + return {}; +} + +std::vector +CallDevices::resolutions(const std::string &) const +{ + return {}; +} + +std::vector +CallDevices::frameRates(const std::string &, const std::string &) const +{ + return {}; +} + +#endif diff --git a/src/CallDevices.h b/src/CallDevices.h new file mode 100644 index 00000000..2b4129f1 --- /dev/null +++ b/src/CallDevices.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include + +typedef struct _GstDevice GstDevice; + +class CallDevices : public QObject +{ + Q_OBJECT + +public: + static CallDevices &instance() + { + static CallDevices instance; + return instance; + } + + void refresh(); + bool haveMic() const; + bool haveCamera() const; + std::vector names(bool isVideo, const std::string &defaultDevice) const; + std::vector resolutions(const std::string &cameraName) const; + std::vector frameRates(const std::string &cameraName, + const std::string &resolution) const; + +signals: + void devicesChanged(); + +private: + CallDevices(); + + friend class WebRTCSession; + void init(); + GstDevice *audioDevice() const; + GstDevice *videoDevice(std::pair &resolution, + std::pair &frameRate) const; + +public: + CallDevices(CallDevices const &) = delete; + void operator=(CallDevices const &) = delete; +}; diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 0841a079..7acd9592 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -7,6 +7,7 @@ #include #include "Cache.h" +#include "CallDevices.h" #include "CallManager.h" #include "ChatPage.h" #include "Logging.h" @@ -114,21 +115,10 @@ CallManager::CallManager(QObject *parent) emit newCallState(); }); - connect(&session_, &WebRTCSession::devicesChanged, this, [this]() { - if (ChatPage::instance()->userSettings()->microphone().isEmpty()) { - auto mics = session_.getDeviceNames(false, std::string()); - if (!mics.empty()) - ChatPage::instance()->userSettings()->setMicrophone( - QString::fromStdString(mics.front())); - } - if (ChatPage::instance()->userSettings()->camera().isEmpty()) { - auto cameras = session_.getDeviceNames(true, std::string()); - if (!cameras.empty()) - ChatPage::instance()->userSettings()->setCamera( - QString::fromStdString(cameras.front())); - } - emit devicesChanged(); - }); + connect(&CallDevices::instance(), + &CallDevices::devicesChanged, + this, + &CallManager::devicesChanged); connect(&player_, &QMediaPlayer::mediaStatusChanged, @@ -292,7 +282,7 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent) haveCallInvite_ = true; isVideo_ = isVideo; inviteSDP_ = callInviteEvent.content.sdp; - session_.refreshDevices(); + CallDevices::instance().refresh(); emit newInviteState(); } @@ -409,7 +399,7 @@ CallManager::devices(bool isVideo) const const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera() : ChatPage::instance()->userSettings()->microphone(); std::vector devices = - session_.getDeviceNames(isVideo, defaultDevice.toStdString()); + CallDevices::instance().names(isVideo, defaultDevice.toStdString()); ret.reserve(devices.size()); std::transform(devices.cbegin(), devices.cend(), diff --git a/src/CallManager.h b/src/CallManager.h index 7d388efd..97cffbc8 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -8,6 +8,7 @@ #include #include +#include "CallDevices.h" #include "WebRTCSession.h" #include "mtx/events/collections.hpp" #include "mtx/events/voip.hpp" @@ -53,7 +54,7 @@ public: public slots: void sendInvite(const QString &roomid, bool isVideo); void syncEvent(const mtx::events::collections::TimelineEvents &event); - void refreshDevices() { session_.refreshDevices(); } + void refreshDevices() { CallDevices::instance().refresh(); } void toggleMicMute(); void toggleCameraView() { session_.toggleCameraView(); } void acceptInvite(); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index d31c8ef9..d4e56b4d 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -39,12 +39,12 @@ #include #include "Cache.h" +#include "CallDevices.h" #include "Config.h" #include "MatrixClient.h" #include "Olm.h" #include "UserSettingsPage.h" #include "Utils.h" -#include "WebRTCSession.h" #include "ui/FlatButton.h" #include "ui/ToggleButton.h" @@ -1060,7 +1060,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge [this](const QString &camera) { settings_->setCamera(camera); std::vector resolutions = - WebRTCSession::instance().getResolutions(camera.toStdString()); + CallDevices::instance().resolutions(camera.toStdString()); cameraResolutionCombo_->clear(); for (const auto &resolution : resolutions) cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); @@ -1070,9 +1070,8 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge static_cast(&QComboBox::currentTextChanged), [this](const QString &resolution) { settings_->setCameraResolution(resolution); - std::vector frameRates = - WebRTCSession::instance().getFrameRates(settings_->camera().toStdString(), - resolution.toStdString()); + std::vector frameRates = CallDevices::instance().frameRates( + settings_->camera().toStdString(), resolution.toStdString()); cameraFrameRateCombo_->clear(); for (const auto &frameRate : frameRates) cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); @@ -1231,9 +1230,8 @@ UserSettingsPage::showEvent(QShowEvent *) timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); - WebRTCSession::instance().refreshDevices(); - auto mics = - WebRTCSession::instance().getDeviceNames(false, settings_->microphone().toStdString()); + CallDevices::instance().refresh(); + auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); microphoneCombo_->clear(); for (const auto &m : mics) microphoneCombo_->addItem(QString::fromStdString(m)); @@ -1241,8 +1239,7 @@ UserSettingsPage::showEvent(QShowEvent *) auto cameraResolution = settings_->cameraResolution(); auto cameraFrameRate = settings_->cameraFrameRate(); - auto cameras = - WebRTCSession::instance().getDeviceNames(true, settings_->camera().toStdString()); + auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); cameraCombo_->clear(); for (const auto &c : cameras) cameraCombo_->addItem(QString::fromStdString(c)); diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index d306007d..b6d98058 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -35,6 +35,7 @@ using webrtc::State; WebRTCSession::WebRTCSession() : QObject() + , devices_(CallDevices::instance()) { qRegisterMetaType(); qmlRegisterUncreatableMetaObject( @@ -68,9 +69,7 @@ WebRTCSession::init(std::string *errorMessage) gchar *version = gst_version_string(); nhlog::ui()->info("WebRTC: initialised {}", version); g_free(version); -#if GST_CHECK_VERSION(1, 18, 0) - startDeviceMonitor(); -#endif + devices_.init(); return true; #else (void)errorMessage; @@ -81,195 +80,17 @@ WebRTCSession::init(std::string *errorMessage) #ifdef GSTREAMER_AVAILABLE namespace { -struct AudioSource -{ - std::string name; - GstDevice *device; -}; - -struct VideoSource -{ - struct Caps - { - std::string resolution; - std::vector frameRates; - }; - std::string name; - GstDevice *device; - std::vector caps; -}; - std::string localsdp_; std::vector localcandidates_; bool haveAudioStream_; bool haveVideoStream_; -std::vector audioSources_; -std::vector videoSources_; GstPad *insetSinkPad_ = nullptr; -using FrameRate = std::pair; -std::optional -getFrameRate(const GValue *value) -{ - if (GST_VALUE_HOLDS_FRACTION(value)) { - gint num = gst_value_get_fraction_numerator(value); - gint den = gst_value_get_fraction_denominator(value); - return FrameRate{num, den}; - } - return std::nullopt; -} - -void -addFrameRate(std::vector &rates, const FrameRate &rate) -{ - constexpr double minimumFrameRate = 15.0; - if (static_cast(rate.first) / rate.second >= minimumFrameRate) - rates.push_back(std::to_string(rate.first) + "/" + std::to_string(rate.second)); -} - -std::pair -tokenise(std::string_view str, char delim) -{ - std::pair ret; - ret.first = std::atoi(str.data()); - auto pos = str.find_first_of(delim); - ret.second = std::atoi(str.data() + pos + 1); - return ret; -} - -void -addDevice(GstDevice *device) -{ - if (!device) - return; - - gchar *name = gst_device_get_display_name(device); - gchar *type = gst_device_get_device_class(device); - bool isVideo = !std::strncmp(type, "Video", 5); - g_free(type); - nhlog::ui()->debug("WebRTC: {} device added: {}", isVideo ? "video" : "audio", name); - if (!isVideo) { - audioSources_.push_back({name, device}); - g_free(name); - return; - } - - GstCaps *gstcaps = gst_device_get_caps(device); - if (!gstcaps) { - nhlog::ui()->debug("WebRTC: unable to get caps for {}", name); - g_free(name); - return; - } - - VideoSource source{name, device, {}}; - g_free(name); - guint nCaps = gst_caps_get_size(gstcaps); - for (guint i = 0; i < nCaps; ++i) { - GstStructure *structure = gst_caps_get_structure(gstcaps, i); - const gchar *name = gst_structure_get_name(structure); - if (!std::strcmp(name, "video/x-raw")) { - gint widthpx, heightpx; - if (gst_structure_get(structure, - "width", - G_TYPE_INT, - &widthpx, - "height", - G_TYPE_INT, - &heightpx, - nullptr)) { - VideoSource::Caps caps; - caps.resolution = - std::to_string(widthpx) + "x" + std::to_string(heightpx); - const GValue *value = - gst_structure_get_value(structure, "framerate"); - if (auto fr = getFrameRate(value); fr) - addFrameRate(caps.frameRates, *fr); - else if (GST_VALUE_HOLDS_FRACTION_RANGE(value)) { - const GValue *minRate = - gst_value_get_fraction_range_min(value); - if (auto fr = getFrameRate(minRate); fr) - addFrameRate(caps.frameRates, *fr); - const GValue *maxRate = - gst_value_get_fraction_range_max(value); - if (auto fr = getFrameRate(maxRate); fr) - addFrameRate(caps.frameRates, *fr); - } else if (GST_VALUE_HOLDS_LIST(value)) { - guint nRates = gst_value_list_get_size(value); - for (guint j = 0; j < nRates; ++j) { - const GValue *rate = - gst_value_list_get_value(value, j); - if (auto fr = getFrameRate(rate); fr) - addFrameRate(caps.frameRates, *fr); - } - } - if (!caps.frameRates.empty()) - source.caps.push_back(std::move(caps)); - } - } - } - gst_caps_unref(gstcaps); - videoSources_.push_back(std::move(source)); -} - -#if GST_CHECK_VERSION(1, 18, 0) -template -bool -removeDevice(T &sources, GstDevice *device, bool changed) -{ - if (auto it = std::find_if(sources.begin(), - sources.end(), - [device](const auto &s) { return s.device == device; }); - it != sources.end()) { - nhlog::ui()->debug(std::string("WebRTC: device ") + - (changed ? "changed: " : "removed: ") + "{}", - it->name); - gst_object_unref(device); - sources.erase(it); - return true; - } - return false; -} - -void -removeDevice(GstDevice *device, bool changed) -{ - if (device) { - if (removeDevice(audioSources_, device, changed) || - removeDevice(videoSources_, device, changed)) - return; - } -} -#endif - gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data) { WebRTCSession *session = static_cast(user_data); switch (GST_MESSAGE_TYPE(msg)) { -#if GST_CHECK_VERSION(1, 18, 0) - case GST_MESSAGE_DEVICE_ADDED: { - GstDevice *device; - gst_message_parse_device_added(msg, &device); - addDevice(device); - emit WebRTCSession::instance().devicesChanged(); - break; - } - case GST_MESSAGE_DEVICE_REMOVED: { - GstDevice *device; - gst_message_parse_device_removed(msg, &device); - removeDevice(device, false); - emit WebRTCSession::instance().devicesChanged(); - break; - } - case GST_MESSAGE_DEVICE_CHANGED: { - GstDevice *device; - GstDevice *oldDevice; - gst_message_parse_device_changed(msg, &device, &oldDevice); - removeDevice(oldDevice, true); - addDevice(device); - break; - } -#endif case GST_MESSAGE_EOS: nhlog::ui()->error("WebRTC: end of stream"); session->end(); @@ -724,27 +545,6 @@ getMediaAttributes(const GstSDPMessage *sdp, return false; } -template -std::vector -deviceNames(T &sources, const std::string &defaultDevice) -{ - std::vector ret; - ret.reserve(sources.size()); - std::transform(sources.cbegin(), - sources.cend(), - std::back_inserter(ret), - [](const auto &s) { return s.name; }); - - // move default device to top of the list - if (auto it = std::find_if(ret.begin(), - ret.end(), - [&defaultDevice](const auto &s) { return s == defaultDevice; }); - it != ret.end()) - std::swap(ret.front(), *it); - - return ret; -} - } bool @@ -995,19 +795,11 @@ WebRTCSession::startPipeline(int opusPayloadType, int vp8PayloadType) bool WebRTCSession::createPipeline(int opusPayloadType, int vp8PayloadType) { - std::string microphoneSetting = - ChatPage::instance()->userSettings()->microphone().toStdString(); - auto it = - std::find_if(audioSources_.cbegin(), - audioSources_.cend(), - [µphoneSetting](const auto &s) { return s.name == microphoneSetting; }); - if (it == audioSources_.cend()) { - nhlog::ui()->error("WebRTC: unknown microphone: {}", microphoneSetting); + GstDevice *device = devices_.audioDevice(); + if (!device) return false; - } - nhlog::ui()->debug("WebRTC: microphone: {}", microphoneSetting); - GstElement *source = gst_device_create_element(it->device, nullptr); + GstElement *source = gst_device_create_element(device, nullptr); GstElement *volume = gst_element_factory_make("volume", "srclevel"); GstElement *convert = gst_element_factory_make("audioconvert", nullptr); GstElement *resample = gst_element_factory_make("audioresample", nullptr); @@ -1070,30 +862,16 @@ bool WebRTCSession::addVideoPipeline(int vp8PayloadType) { // allow incoming video calls despite localUser having no webcam - if (videoSources_.empty()) + if (!devices_.haveCamera()) return !isOffering_; - QSharedPointer settings = ChatPage::instance()->userSettings(); - std::string cameraSetting = settings->camera().toStdString(); - auto it = std::find_if(videoSources_.cbegin(), - videoSources_.cend(), - [&cameraSetting](const auto &s) { return s.name == cameraSetting; }); - if (it == videoSources_.cend()) { - nhlog::ui()->error("WebRTC: unknown camera: {}", cameraSetting); + std::pair resolution; + std::pair frameRate; + GstDevice *device = devices_.videoDevice(resolution, frameRate); + if (!device) return false; - } - std::string resSetting = settings->cameraResolution().toStdString(); - const std::string &res = resSetting.empty() ? it->caps.front().resolution : resSetting; - std::string frSetting = settings->cameraFrameRate().toStdString(); - const std::string &fr = frSetting.empty() ? it->caps.front().frameRates.front() : frSetting; - auto resolution = tokenise(res, 'x'); - auto frameRate = tokenise(fr, '/'); - nhlog::ui()->debug("WebRTC: camera: {}", cameraSetting); - nhlog::ui()->debug("WebRTC: camera resolution: {}x{}", resolution.first, resolution.second); - nhlog::ui()->debug("WebRTC: camera frame rate: {}/{}", frameRate.first, frameRate.second); - - GstElement *source = gst_device_create_element(it->device, nullptr); + GstElement *source = gst_device_create_element(device, nullptr); GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr); GstElement *capsfilter = gst_element_factory_make("capsfilter", "camerafilter"); GstCaps *caps = gst_caps_new_simple("video/x-raw", @@ -1239,111 +1017,6 @@ WebRTCSession::end() emit stateChanged(State::DISCONNECTED); } -#if GST_CHECK_VERSION(1, 18, 0) -void -WebRTCSession::startDeviceMonitor() -{ - if (!initialised_) - return; - - static GstDeviceMonitor *monitor = nullptr; - if (!monitor) { - monitor = gst_device_monitor_new(); - GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); - gst_device_monitor_add_filter(monitor, "Audio/Source", caps); - gst_caps_unref(caps); - caps = gst_caps_new_empty_simple("video/x-raw"); - gst_device_monitor_add_filter(monitor, "Video/Source", caps); - gst_caps_unref(caps); - - GstBus *bus = gst_device_monitor_get_bus(monitor); - gst_bus_add_watch(bus, newBusMessage, nullptr); - gst_object_unref(bus); - if (!gst_device_monitor_start(monitor)) { - nhlog::ui()->error("WebRTC: failed to start device monitor"); - return; - } - } -} -#endif - -void -WebRTCSession::refreshDevices() -{ -#if GST_CHECK_VERSION(1, 18, 0) - return; -#else - if (!initialised_) - return; - - static GstDeviceMonitor *monitor = nullptr; - if (!monitor) { - monitor = gst_device_monitor_new(); - GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); - gst_device_monitor_add_filter(monitor, "Audio/Source", caps); - gst_caps_unref(caps); - caps = gst_caps_new_empty_simple("video/x-raw"); - gst_device_monitor_add_filter(monitor, "Video/Source", caps); - gst_caps_unref(caps); - } - - auto clearDevices = [](auto &sources) { - std::for_each( - sources.begin(), sources.end(), [](auto &s) { gst_object_unref(s.device); }); - sources.clear(); - }; - clearDevices(audioSources_); - clearDevices(videoSources_); - - GList *devices = gst_device_monitor_get_devices(monitor); - if (devices) { - for (GList *l = devices; l != nullptr; l = l->next) - addDevice(GST_DEVICE_CAST(l->data)); - g_list_free(devices); - } - emit devicesChanged(); -#endif -} - -std::vector -WebRTCSession::getDeviceNames(bool isVideo, const std::string &defaultDevice) const -{ - return isVideo ? deviceNames(videoSources_, defaultDevice) - : deviceNames(audioSources_, defaultDevice); -} - -std::vector -WebRTCSession::getResolutions(const std::string &cameraName) const -{ - std::vector ret; - if (auto it = std::find_if(videoSources_.cbegin(), - videoSources_.cend(), - [&cameraName](const auto &s) { return s.name == cameraName; }); - it != videoSources_.cend()) { - ret.reserve(it->caps.size()); - for (const auto &c : it->caps) - ret.push_back(c.resolution); - } - return ret; -} - -std::vector -WebRTCSession::getFrameRates(const std::string &cameraName, const std::string &resolution) const -{ - if (auto i = std::find_if(videoSources_.cbegin(), - videoSources_.cend(), - [&](const auto &s) { return s.name == cameraName; }); - i != videoSources_.cend()) { - if (auto j = - std::find_if(i->caps.cbegin(), - i->caps.cend(), - [&](const auto &s) { return s.resolution == resolution; }); - j != i->caps.cend()) - return j->frameRates; - } - return {}; -} - #else bool @@ -1400,25 +1073,4 @@ void WebRTCSession::end() {} -void -WebRTCSession::refreshDevices() -{} - -std::vector -WebRTCSession::getDeviceNames(bool, const std::string &) const -{ - return {}; -} - -std::vector -WebRTCSession::getResolutions(const std::string &) const -{ - return {}; -} - -std::vector -WebRTCSession::getFrameRates(const std::string &, const std::string &) const -{ - return {}; -} #endif diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index 2f0fb70e..0fe8a864 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -5,6 +5,7 @@ #include +#include "CallDevices.h" #include "mtx/events/voip.hpp" typedef struct _GstElement GstElement; @@ -59,13 +60,6 @@ public: void setTurnServers(const std::vector &uris) { turnServers_ = uris; } - void refreshDevices(); - std::vector getDeviceNames(bool isVideo, - const std::string &defaultDevice) const; - std::vector getResolutions(const std::string &cameraName) const; - std::vector getFrameRates(const std::string &cameraName, - const std::string &resolution) const; - void setVideoItem(QQuickItem *item) { videoItem_ = item; } QQuickItem *getVideoItem() const { return videoItem_; } @@ -76,7 +70,6 @@ signals: const std::vector &); void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &); void stateChanged(webrtc::State); - void devicesChanged(); private slots: void setState(webrtc::State state) { state_ = state; } @@ -84,6 +77,7 @@ private slots: private: WebRTCSession(); + CallDevices &devices_; bool initialised_ = false; bool haveVoicePlugins_ = false; bool haveVideoPlugins_ = false; @@ -101,7 +95,6 @@ private: bool startPipeline(int opusPayloadType, int vp8PayloadType); bool createPipeline(int opusPayloadType, int vp8PayloadType); bool addVideoPipeline(int vp8PayloadType); - void startDeviceMonitor(); public: WebRTCSession(WebRTCSession const &) = delete; From 04b920fbee4547b65a21af03ba709281250e5960 Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sun, 7 Feb 2021 22:18:04 +0530 Subject: [PATCH 15/50] linting fix --- src/UserSettingsPage.cpp | 3 +-- src/UserSettingsPage.h | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index bf1bdb86..f0d35aa6 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -741,8 +741,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge fontSelectionCombo_->setCurrentIndex( fontSelectionCombo_->findText(tr(currentFont.toStdString().c_str()))); } else { - fontSelectionCombo_->setCurrentIndex( - fontSelectionCombo_->findText(currentFont)); + fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); } emojiFontSelectionCombo_->setCurrentIndex( diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index ab82c282..49de94b3 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -177,12 +177,13 @@ public: int timelineMaxWidth() const { return timelineMaxWidth_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } - QString emojiFont() const { + QString emojiFont() const + { if (emojiFont_ == "Default") { return tr("Default"); } - return emojiFont_; + return emojiFont_; } Presence presence() const { return presence_; } QString ringtone() const { return ringtone_; } From 8d68534456481605c89c4ec5656792c83e2d8d01 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 13:54:18 -0500 Subject: [PATCH 16/50] Add Duplex call devices --- src/CallDevices.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CallDevices.cpp b/src/CallDevices.cpp index 32ae6e69..0b9809e5 100644 --- a/src/CallDevices.cpp +++ b/src/CallDevices.cpp @@ -263,9 +263,11 @@ CallDevices::init() monitor = gst_device_monitor_new(); GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); gst_device_monitor_add_filter(monitor, "Audio/Source", caps); + gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps); gst_caps_unref(caps); caps = gst_caps_new_empty_simple("video/x-raw"); gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_device_monitor_add_filter(monitor, "Video/Duplex", caps); gst_caps_unref(caps); GstBus *bus = gst_device_monitor_get_bus(monitor); @@ -289,9 +291,11 @@ CallDevices::refresh() monitor = gst_device_monitor_new(); GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw"); gst_device_monitor_add_filter(monitor, "Audio/Source", caps); + gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps); gst_caps_unref(caps); caps = gst_caps_new_empty_simple("video/x-raw"); gst_device_monitor_add_filter(monitor, "Video/Source", caps); + gst_device_monitor_add_filter(monitor, "Video/Duplex", caps); gst_caps_unref(caps); } From 974c336c5e95fc3a51b473d5e1899c49817ab704 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 13:58:32 -0500 Subject: [PATCH 17/50] make lint --- resources/qml/MessageView.qml | 5 +++-- resources/qml/UserProfile.qml | 13 +++++++++---- src/ChatPage.cpp | 7 +++---- src/Utils.cpp | 3 ++- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 29115b00..dafca0f6 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -102,6 +102,7 @@ ListView { Avatar { id: messageUserAvatar + width: avatarSize height: avatarSize url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" @@ -112,8 +113,8 @@ ListView { Connections { target: chat.model - onRoomAvatarUrlChanged: { - messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" + onRoomAvatarUrlChanged: { + messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""; } } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 37ae6de8..4797a38e 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -49,6 +49,7 @@ ApplicationWindow { Text { id: errorText + text: "Error Text" color: "red" visible: opacity > 0 @@ -58,24 +59,28 @@ ApplicationWindow { SequentialAnimation { id: hideErrorAnimation + running: false + PauseAnimation { duration: 4000 } + NumberAnimation { target: errorText property: 'opacity' to: 0 duration: 1000 } + } - Connections{ + Connections { target: profile onDisplayError: { - errorText.text = errorMessage - errorText.opacity = 1 - hideErrorAnimation.restart() + errorText.text = errorMessage; + errorText.opacity = 1; + hideErrorAnimation.restart(); } } diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index d8907740..6d67e6f2 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -449,10 +449,9 @@ ChatPage::deleteConfigs() { QSettings settings; - if (UserSettings::instance()->profile() != "") - { - settings.beginGroup("profile"); - settings.beginGroup(UserSettings::instance()->profile()); + if (UserSettings::instance()->profile() != "") { + settings.beginGroup("profile"); + settings.beginGroup(UserSettings::instance()->profile()); } settings.beginGroup("auth"); settings.remove(""); diff --git a/src/Utils.cpp b/src/Utils.cpp index 1b2808b3..f90e5049 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -70,7 +70,8 @@ utils::replaceEmoji(const QString &body) for (auto &code : utf32_string) { if (utils::codepointIsEmoji(code)) { if (!insideFontBlock) { - fmtBody += QString("font() + "\">"); + fmtBody += QString("font() + "\">"); insideFontBlock = true; } From 2fcc9c2fbbabfc9a0204248818fa78c9ee96cab4 Mon Sep 17 00:00:00 2001 From: tverrbjelke Date: Tue, 9 Feb 2021 13:21:42 +0100 Subject: [PATCH 18/50] Fixes issue 463 Installation description for debian (buster) misses package qtdeclarative5-dev modified: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be7d076f..2ee06940 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ and mtxclient (needs to be build separately). ```bash sudo apt install cmake gcc make automake liblmdb-dev \ qt5-default libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev \ - qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools \ + qml-module-qtgstreamer qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev \ qml-module-qtgraphicaleffects qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts \ qt5keychain-dev ``` From 50f994bd235f4f551ebdbef6df0d83209ea2042b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 16:26:18 +0100 Subject: [PATCH 19/50] Clean up config names a bit --- src/UserSettingsPage.cpp | 11 ++++------- src/main.cpp | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 820669a6..b6fdf504 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -93,7 +93,7 @@ UserSettings::load(std::optional profile) sortByImportance_ = settings.value("user/sort_by_unread", true).toBool(); readReceipts_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); - font_ = settings.value("user/font_family", "").toString(); + font_ = settings.value("user/font_family", "default").toString(); avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); @@ -101,7 +101,7 @@ UserSettings::load(std::optional profile) shareKeysWithTrustedUsers_ = settings.value("user/share_keys_with_trusted_users", true).toBool(); mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - emojiFont_ = settings.value("user/emoji_font_family", "Default").toString(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); auto tempPresence = settings.value("user/presence", "").toString().toStdString(); auto presenceValue = QMetaEnum::fromType().keyToValue(tempPresence.c_str()); @@ -343,7 +343,7 @@ UserSettings::setEmojiFontFamily(QString family) return; if (family == tr("Default")) { - emojiFont_ = "Default"; + emojiFont_ = "default"; } else { emojiFont_ = family; } @@ -737,10 +737,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge } QString currentFont = settings_->font(); - if (currentFont == "Default") { - fontSelectionCombo_->setCurrentIndex( - fontSelectionCombo_->findText(tr(currentFont.toStdString().c_str()))); - } else { + if (currentFont != "default" || currentFont != "") { fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); } diff --git a/src/main.cpp b/src/main.cpp index 07962b9b..0c7c9f60 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -214,7 +214,7 @@ main(int argc, char *argv[]) QFont font; QString userFontFamily = settings.lock()->font(); - if (!userFontFamily.isEmpty()) { + if (!userFontFamily.isEmpty() && userFontFamily != "default") { font.setFamily(userFontFamily); } font.setPointSizeF(settings.lock()->fontSize()); From 0285bf5e4e625e5156034c942261afc731fd649a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 16:31:33 +0100 Subject: [PATCH 20/50] Remove unused variables --- src/ui/UserProfile.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index 274ed927..eb15705c 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -316,7 +316,6 @@ UserProfile::changeAvatar() const auto bin = file.peek(file.size()); const auto payload = std::string(bin.data(), bin.size()); - const auto dimensions = QImageReader(&file).size(); isLoading_ = true; emit loadingChanged(); @@ -328,7 +327,6 @@ UserProfile::changeAvatar() mime.name().toStdString(), QFileInfo(fileName).fileName().toStdString(), [this, - dimensions, payload, mimetype = mime.name().toStdString(), size = payload.size(), @@ -371,7 +369,7 @@ UserProfile::updateRoomMemberState(mtx::events::state::Member member) roomid_.toStdString(), http::client()->user_id().to_string(), member, - [this](mtx::responses::EventId, mtx::http::RequestErr err) { + [](mtx::responses::EventId, mtx::http::RequestErr err) { if (err) nhlog::net()->error("Failed to update room member state : ", err->matrix_error.error); From 8d95532b2840916897233e505a47e52472afb873 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 17:00:06 +0100 Subject: [PATCH 21/50] Fix linting --- src/ui/UserProfile.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index eb15705c..77f6ced5 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -314,8 +314,8 @@ UserProfile::changeAvatar() return; } - const auto bin = file.peek(file.size()); - const auto payload = std::string(bin.data(), bin.size()); + const auto bin = file.peek(file.size()); + const auto payload = std::string(bin.data(), bin.size()); isLoading_ = true; emit loadingChanged(); @@ -365,15 +365,15 @@ UserProfile::changeAvatar() void UserProfile::updateRoomMemberState(mtx::events::state::Member member) { - http::client()->send_state_event( - roomid_.toStdString(), - http::client()->user_id().to_string(), - member, - [](mtx::responses::EventId, mtx::http::RequestErr err) { - if (err) - nhlog::net()->error("Failed to update room member state : ", - err->matrix_error.error); - }); + http::client()->send_state_event(roomid_.toStdString(), + http::client()->user_id().to_string(), + member, + [](mtx::responses::EventId, mtx::http::RequestErr err) { + if (err) + nhlog::net()->error( + "Failed to update room member state : ", + err->matrix_error.error); + }); } void From 463cee71460bfa64dfd64bf7018fb928e3589331 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 20:20:37 +0100 Subject: [PATCH 22/50] Fix wrong font used in emoji escape --- src/Utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index f90e5049..991fa550 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -71,7 +71,7 @@ utils::replaceEmoji(const QString &body) if (utils::codepointIsEmoji(code)) { if (!insideFontBlock) { fmtBody += QString("font() + "\">"); + UserSettings::instance()->emojiFont() + "\">"); insideFontBlock = true; } From 2e77a1554f1572b7c7e59f8177a48e5dffa16c23 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 26 Jan 2021 22:36:35 +0100 Subject: [PATCH 23/50] Switch to new relations format --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 3 +- src/Cache.cpp | 72 ++++++++++++---------------- src/DeviceVerificationFlow.cpp | 34 ++++++------- src/DeviceVerificationFlow.h | 10 ++-- src/EventAccessors.cpp | 35 ++++---------- src/EventAccessors.h | 6 +-- src/Olm.cpp | 27 ++++------- src/timeline/EventStore.cpp | 37 +++++++------- src/timeline/InputBar.cpp | 18 ++++--- src/timeline/TimelineModel.cpp | 4 +- src/timeline/TimelineViewManager.cpp | 8 ++-- 12 files changed, 112 insertions(+), 144 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d5245ef8..cf2b5959 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG v0.4.1 + GIT_TAG 70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index e6eeb123..98ab9629 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,8 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "4951190c938740defa0988d98d5e861038622936", - "tag": "v0.4.1", + "commit": "70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/src/Cache.cpp b/src/Cache.cpp index 3f2bf73a..94b9a6a6 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2713,23 +2713,19 @@ Cache::saveTimelineMessages(lmdb::txn &txn, lmdb::dbi_put(txn, evToOrderDb, event_id, txn_order); lmdb::dbi_del(txn, evToOrderDb, lmdb::val(txn_id)); - if (event.contains("content") && - event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = temp.contains("m.in_reply_to") && - temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) { - lmdb::dbi_del(txn, - relationsDb, - lmdb::val(relates_to), - lmdb::val(txn_id)); - lmdb::dbi_put( - txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_del(txn, + relationsDb, + lmdb::val(r.event_id), + lmdb::val(txn_id)); + lmdb::dbi_put(txn, + relationsDb, + lmdb::val(r.event_id), + event_id); + } } } @@ -2808,19 +2804,16 @@ Cache::saveTimelineMessages(lmdb::txn &txn, lmdb::val(&msgIndex, sizeof(msgIndex))); } - if (event.contains("content") && - event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = temp.contains("m.in_reply_to") && - temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) - lmdb::dbi_put( - txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_put(txn, + relationsDb, + lmdb::val(r.event_id), + event_id); + } + } } } } @@ -2901,17 +2894,14 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex))); } - if (event.contains("content") && event["content"].contains("m.relates_to")) { - auto temp = event["content"]["m.relates_to"]; - json relates_to_j = - temp.contains("m.in_reply_to") && temp["m.in_reply_to"].is_object() - ? temp["m.in_reply_to"]["event_id"] - : temp["event_id"]; - std::string relates_to = - relates_to_j.is_string() ? relates_to_j.get() : ""; - - if (!relates_to.empty()) - lmdb::dbi_put(txn, relationsDb, lmdb::val(relates_to), event_id); + auto relations = mtx::accessors::relations(e); + if (!relations.relations.empty()) { + for (const auto &r : relations.relations) { + if (!r.event_id.empty()) { + lmdb::dbi_put( + txn, relationsDb, lmdb::val(r.event_id), event_id); + } + } } } diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp index 51ef79fd..c6277a9d 100644 --- a/src/DeviceVerificationFlow.cpp +++ b/src/DeviceVerificationFlow.cpp @@ -105,8 +105,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && @@ -136,8 +136,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } error_ = User; @@ -152,8 +152,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } @@ -217,8 +217,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } @@ -385,8 +385,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if ((msg.relates_to.has_value() && sender)) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; else { this->deviceId = QString::fromStdString(msg.from_device); @@ -402,8 +402,8 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *, if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } nhlog::ui()->info("Flow done on other side"); @@ -526,8 +526,8 @@ DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificati if (msg.transaction_id.has_value()) { if (msg.transaction_id.value() != this->transaction_id) return; - } else if (msg.relates_to.has_value()) { - if (msg.relates_to.value().event_id != this->relation.event_id) + } else if (msg.relations.references()) { + if (msg.relations.references() != this->relation.event_id) return; } if ((std::find(msg.key_agreement_protocols.begin(), @@ -625,8 +625,10 @@ DeviceVerificationFlow::startVerificationRequest() req.transaction_id = this->transaction_id; this->canonical_json = nlohmann::json(req); } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - req.relates_to = this->relation; - this->canonical_json = nlohmann::json(req); + req.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + req.relations.synthesized = true; + this->canonical_json = nlohmann::json(req); } send(req); setState(WaitingForOtherToAccept); diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h index 34b78962..6c613545 100644 --- a/src/DeviceVerificationFlow.h +++ b/src/DeviceVerificationFlow.h @@ -206,7 +206,7 @@ private: std::vector sasList; UserKeyCache their_keys; TimelineModel *model_; - mtx::common::RelatesTo relation; + mtx::common::Relation relation; State state_ = PromptStartVerification; Error error_ = UnknownMethod; @@ -230,8 +230,12 @@ private: static_cast(err->status_code)); }); } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { - if constexpr (!std::is_same_v) - msg.relates_to = this->relation; + if constexpr (!std::is_same_v) { + msg.relations.relations.push_back(this->relation); + // Set synthesized to surpress the nheko relation extensions + msg.relations.synthesized = true; + } (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type); } diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 3ae781f0..4218f491 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -250,31 +250,17 @@ struct EventFilesize } }; -struct EventInReplyTo +struct EventRelations { template - using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id); + using related_ev_id_t = decltype(Content::relations); template - std::string operator()(const mtx::events::Event &e) + mtx::common::Relations operator()(const mtx::events::Event &e) { if constexpr (is_detected::value) { - return e.content.relates_to.in_reply_to.event_id; + return e.content.relations; } - return ""; - } -}; - -struct EventRelatesTo -{ - template - using related_ev_id_t = decltype(Content::relates_to.event_id); - template - std::string operator()(const mtx::events::Event &e) - { - if constexpr (is_detected::value) { - return e.content.relates_to.event_id; - } - return ""; + return {}; } }; @@ -434,15 +420,10 @@ mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) { return std::visit(EventMimeType{}, event); } -std::string -mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event) +mtx::common::Relations +mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) { - return std::visit(EventInReplyTo{}, event); -} -std::string -mtx::accessors::relates_to_event_id(const mtx::events::collections::TimelineEvents &event) -{ - return std::visit(EventRelatesTo{}, event); + return std::visit(EventRelations{}, event); } std::string diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 0cdc5f89..60912497 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -53,10 +53,8 @@ std::string blurhash(const mtx::events::collections::TimelineEvents &event); std::string mimetype(const mtx::events::collections::TimelineEvents &event); -std::string -in_reply_to_event(const mtx::events::collections::TimelineEvents &event); -std::string -relates_to_event_id(const mtx::events::collections::TimelineEvents &event); +mtx::common::Relations +relations(const mtx::events::collections::TimelineEvents &event); std::string transaction_id(const mtx::events::collections::TimelineEvents &event); diff --git a/src/Olm.cpp b/src/Olm.cpp index 4ccf8ab9..54be4751 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -575,29 +575,19 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, if (!sendSessionTo.empty()) olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - mtx::common::ReplyRelatesTo relation; - mtx::common::RelatesTo r_relation; - // relations shouldn't be encrypted... - if (body["content"].contains("m.relates_to")) { - if (body["content"]["m.relates_to"].contains("m.in_reply_to")) { - relation = body["content"]["m.relates_to"]; - } else if (body["content"]["m.relates_to"].contains("event_id")) { - r_relation = body["content"]["m.relates_to"]; - } - } + mtx::common::Relations relations = mtx::common::parse_relations(body["content"]); auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); // Prepare the m.room.encrypted event. msg::Encrypted data; - data.ciphertext = std::string((char *)payload.data(), payload.size()); - data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = mtx::crypto::session_id(session.get()); - data.device_id = device_id; - data.algorithm = MEGOLM_ALGO; - data.relates_to = relation; - data.r_relates_to = r_relation; + data.ciphertext = std::string((char *)payload.data(), payload.size()); + data.sender_key = olm::client()->identity_keys().curve25519; + data.session_id = mtx::crypto::session_id(session.get()); + data.device_id = device_id; + data.algorithm = MEGOLM_ALGO; + data.relations = relations; group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); @@ -910,8 +900,7 @@ decryptEvent(const MegolmSessionIndex &index, body["unsigned"] = event.unsigned_data; // relations are unencrypted in content... - if (json old_ev = event; old_ev["content"].count("m.relates_to") != 0) - body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + mtx::common::add_relations(body["content"], event.content.relations); mtx::events::collections::TimelineEvent te; try { diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index be4bc09e..4a90222f 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -293,16 +293,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events) } for (const auto &event : events.events) { - std::string relates_to; + std::set relates_to; if (auto redaction = std::get_if>( &event)) { // fixup reactions auto redacted = events_by_id_.object({room_id_, redaction->redacts}); if (redacted) { - auto id = mtx::accessors::relates_to_event_id(*redacted); - if (!id.empty()) { - auto idx = idToIndex(id); + auto id = mtx::accessors::relations(*redacted); + if (id.annotates()) { + auto idx = idToIndex(id.annotates()->event_id); if (idx) { events_by_id_.remove( {room_id_, redaction->redacts}); @@ -312,20 +312,17 @@ EventStore::handleSync(const mtx::responses::Timeline &events) } } - relates_to = redaction->redacts; - } else if (auto reaction = - std::get_if>( - &event)) { - relates_to = reaction->content.relates_to.event_id; + relates_to.insert(redaction->redacts); } else { - relates_to = mtx::accessors::in_reply_to_event(event); + for (const auto &r : mtx::accessors::relations(event).relations) + relates_to.insert(r.event_id); } - if (!relates_to.empty()) { - auto idx = cache::client()->getTimelineIndex(room_id_, relates_to); + for (const auto &relates_to_id : relates_to) { + auto idx = cache::client()->getTimelineIndex(room_id_, relates_to_id); if (idx) { - events_by_id_.remove({room_id_, relates_to}); - decryptedEvents_.remove({room_id_, relates_to}); + events_by_id_.remove({room_id_, relates_to_id}); + decryptedEvents_.remove({room_id_, relates_to_id}); events_.remove({room_id_, *idx}); emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); } @@ -430,13 +427,14 @@ EventStore::reactions(const std::string &event_id) if (auto reaction = std::get_if>( related_event); - reaction && reaction->content.relates_to.key) { - auto &agg = aggregation[reaction->content.relates_to.key.value()]; + reaction && reaction->content.relations.annotates() && + reaction->content.relations.annotates()->key) { + auto key = reaction->content.relations.annotates()->key.value(); + auto &agg = aggregation[key]; if (agg.count == 0) { Reaction temp{}; - temp.key_ = - QString::fromStdString(reaction->content.relates_to.key.value()); + temp.key_ = QString::fromStdString(key); reactions.push_back(temp); } @@ -691,8 +689,7 @@ EventStore::decryptEvent(const IdIndex &idx, body["unsigned"] = e.unsigned_data; // relations are unencrypted in content... - if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) - body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + mtx::common::add_relations(body["content"], e.content.relations); json event_array = json::array(); event_array.push_back(body); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b31c1f76..738fb37c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -294,7 +294,8 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) text.formatted_body = utils::getFormattedQuoteBody(related, msg.toHtmlEscaped()).toStdString(); - text.relates_to.in_reply_to.event_id = related.related_event; + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, related.related_event}); room->resetReply(); } @@ -316,7 +317,8 @@ InputBar::emote(QString msg) } if (!room->reply().isEmpty()) { - emote.relates_to.in_reply_to.event_id = room->reply().toStdString(); + emote.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -346,7 +348,8 @@ InputBar::image(const QString &filename, image.url = url.toStdString(); if (!room->reply().isEmpty()) { - image.relates_to.in_reply_to.event_id = room->reply().toStdString(); + image.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -371,7 +374,8 @@ InputBar::file(const QString &filename, file.url = url.toStdString(); if (!room->reply().isEmpty()) { - file.relates_to.in_reply_to.event_id = room->reply().toStdString(); + file.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -397,7 +401,8 @@ InputBar::audio(const QString &filename, audio.url = url.toStdString(); if (!room->reply().isEmpty()) { - audio.relates_to.in_reply_to.event_id = room->reply().toStdString(); + audio.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } @@ -422,7 +427,8 @@ InputBar::video(const QString &filename, video.url = url.toStdString(); if (!room->reply().isEmpty()) { - video.relates_to.in_reply_to.event_id = room->reply().toStdString(); + video.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 968ec3c7..c47194f5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -360,7 +360,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r const static QRegularExpression replyFallback( ".*", QRegularExpression::DotMatchesEverythingOption); - bool isReply = !in_reply_to_event(event).empty(); + bool isReply = relations(event).reply_to().has_value(); auto formattedBody_ = QString::fromStdString(formatted_body(event)); if (formattedBody_.isEmpty()) { @@ -442,7 +442,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return cache::isRoomEncrypted(room_id_.toStdString()); } case ReplyTo: - return QVariant(QString::fromStdString(in_reply_to_event(event))); + return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { auto id = event_id(event); return QVariant::fromValue(events.reactions(id)); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 9e045e83..e1e2b681 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -503,9 +503,11 @@ TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QSt // If selfReactedEvent is empty, that means we haven't previously reacted if (selfReactedEvent.isEmpty()) { mtx::events::msg::Reaction reaction; - reaction.relates_to.rel_type = mtx::common::RelationType::Annotation; - reaction.relates_to.event_id = reactedEvent.toStdString(); - reaction.relates_to.key = reactionKey.toStdString(); + mtx::common::Relation rel; + rel.rel_type = mtx::common::RelationType::Annotation; + rel.event_id = reactedEvent.toStdString(); + rel.key = reactionKey.toStdString(); + reaction.relations.relations.push_back(rel); timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); // Otherwise, we have previously reacted and the reaction should be redacted From d6504812c71ff7251a5319113c580ab322469eb3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 02:45:33 +0100 Subject: [PATCH 24/50] Render edits --- src/Cache.cpp | 30 +++++++++++++++++++++ src/Cache_p.h | 2 ++ src/timeline/EventStore.cpp | 54 ++++++++++++++++++++++++++++++++++--- src/timeline/EventStore.h | 4 ++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 94b9a6a6..49861a9a 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -108,6 +108,11 @@ Cache::isHiddenEvent(lmdb::txn &txn, const std::string &room_id) { using namespace mtx::events; + + // Always hide edits + if (mtx::accessors::relations(e).replaces()) + return true; + if (auto encryptedEvent = std::get_if>(&e)) { MegolmSessionIndex index; index.room_id = room_id; @@ -1891,6 +1896,31 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) return *val.data(); } +std::optional +Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val indexVal{event_id.data(), event_id.size()}, val; + + bool success = lmdb::dbi_get(txn, orderDb, indexVal, val); + if (!success) { + return {}; + } + + return *val.data(); +} + std::optional Cache::getTimelineEventId(const std::string &room_id, uint64_t index) { diff --git a/src/Cache_p.h b/src/Cache_p.h index e2ce1668..c96a3f30 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -205,6 +205,8 @@ public: std::optional getTimelineIndex(const std::string &room_id, std::string_view event_id); std::optional getTimelineEventId(const std::string &room_id, uint64_t index); + std::optional getArrivalIndex(const std::string &room_id, + std::string_view event_id); std::string previousBatchToken(const std::string &room_id); uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 4a90222f..ebf2f024 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -405,6 +405,41 @@ EventStore::handle_room_verification(mtx::events::collections::TimelineEvents ev event); } +std::vector +EventStore::edits(const std::string &event_id) +{ + auto event_ids = cache::client()->relatedEvents(room_id_, event_id); + + auto original_event = get(event_id, "", false, false); + if (!original_event) + return {}; + + auto original_sender = mtx::accessors::sender(*original_event); + + std::vector edits; + for (const auto &id : event_ids) { + auto related_event = get(id, event_id, false, false); + if (!related_event) + continue; + + auto edit_rel = mtx::accessors::relations(*related_event); + if (edit_rel.replaces() == event_id && + original_sender == mtx::accessors::sender(*related_event)) + edits.push_back(*related_event); + } + + auto c = cache::client(); + std::sort(edits.begin(), + edits.end(), + [this, c](const mtx::events::collections::TimelineEvents &a, + const mtx::events::collections::TimelineEvents &b) { + return c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(a)) < + c->getArrivalIndex(this->room_id_, mtx::accessors::event_id(b)); + }); + + return edits; +} + QVariantList EventStore::reactions(const std::string &event_id) { @@ -487,7 +522,13 @@ EventStore::get(int idx, bool decrypt) if (!event_id) return nullptr; - auto event = cache::client()->getEvent(room_id_, *event_id); + std::optional event; + auto edits_ = edits(*event_id); + if (edits_.empty()) + event = cache::client()->getEvent(room_id_, *event_id); + else + event = {edits_.back()}; + if (!event) return nullptr; else @@ -714,7 +755,7 @@ EventStore::decryptEvent(const IdIndex &idx, } mtx::events::collections::TimelineEvents * -EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) +EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); @@ -722,7 +763,14 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) if (id.empty()) return nullptr; - IdIndex index{room_id_, std::string(id.data(), id.size())}; + std::string id_ = std::string(id); + if (resolve_edits) { + auto edits_ = edits(id_); + if (!edits_.empty()) + id_ = mtx::accessors::event_id(edits_.back()); + } + + IdIndex index{room_id_, id_}; auto event_ptr = events_by_id_.object(index); if (!event_ptr) { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index f8eff9a9..ced7bdc0 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -66,7 +66,8 @@ public: // relatedFetched event mtx::events::collections::TimelineEvents *get(std::string_view id, std::string_view related_to, - bool decrypt = true); + bool decrypt = true, + bool resolve_edits = true); // always returns a proper event as long as the idx is valid mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true); @@ -110,6 +111,7 @@ public slots: void clearTimeline(); private: + std::vector edits(const std::string &event_id); mtx::events::collections::TimelineEvents *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent &e); From faeaf9dc6bfae5bf56f6edee4e21bb54db08b2e1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 16:14:03 +0100 Subject: [PATCH 25/50] Fix edited replies --- src/EventAccessors.cpp | 21 +++++++++++++++++++++ src/EventAccessors.h | 2 ++ src/timeline/EventStore.cpp | 19 +++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 4218f491..212c2970 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -264,6 +264,20 @@ struct EventRelations } }; +struct SetEventRelations +{ + mtx::common::Relations new_relations; + template + using related_ev_id_t = decltype(Content::relations); + template + void operator()(mtx::events::Event &e) + { + if constexpr (is_detected::value) { + e.content.relations = std::move(new_relations); + } + } +}; + struct EventTransactionId { template @@ -426,6 +440,13 @@ mtx::accessors::relations(const mtx::events::collections::TimelineEvents &event) return std::visit(EventRelations{}, event); } +void +mtx::accessors::set_relations(mtx::events::collections::TimelineEvents &event, + mtx::common::Relations relations) +{ + std::visit(SetEventRelations{std::move(relations)}, event); +} + std::string mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) { diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 60912497..95e5df24 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -55,6 +55,8 @@ std::string mimetype(const mtx::events::collections::TimelineEvents &event); mtx::common::Relations relations(const mtx::events::collections::TimelineEvents &event); +void +set_relations(mtx::events::collections::TimelineEvents &event, mtx::common::Relations relations); std::string transaction_id(const mtx::events::collections::TimelineEvents &event); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index ebf2f024..e5a66e19 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -414,7 +414,8 @@ EventStore::edits(const std::string &event_id) if (!original_event) return {}; - auto original_sender = mtx::accessors::sender(*original_event); + auto original_sender = mtx::accessors::sender(*original_event); + auto original_relations = mtx::accessors::relations(*original_event); std::vector edits; for (const auto &id : event_ids) { @@ -422,10 +423,20 @@ EventStore::edits(const std::string &event_id) if (!related_event) continue; - auto edit_rel = mtx::accessors::relations(*related_event); + auto related_ev = *related_event; + + auto edit_rel = mtx::accessors::relations(related_ev); if (edit_rel.replaces() == event_id && - original_sender == mtx::accessors::sender(*related_event)) - edits.push_back(*related_event); + original_sender == mtx::accessors::sender(related_ev)) { + if (edit_rel.synthesized && original_relations.reply_to() && + !edit_rel.reply_to()) { + edit_rel.relations.push_back( + {mtx::common::RelationType::InReplyTo, + original_relations.reply_to().value()}); + mtx::accessors::set_relations(related_ev, std::move(edit_rel)); + } + edits.push_back(std::move(related_ev)); + } } auto c = cache::client(); From 00fd4eecec1af3a38bc69f3849f9e49f826cef26 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 31 Jan 2021 22:41:43 +0100 Subject: [PATCH 26/50] Display edits correctly --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 2 +- resources/qml/TimelineRow.qml | 14 ++++++++++++ src/EventAccessors.cpp | 20 ++++++++++++++++ src/EventAccessors.h | 3 +++ src/timeline/EventStore.cpp | 14 +++++++----- src/timeline/TimelineModel.cpp | 39 +++++++++++++++++++++++++++++--- src/timeline/TimelineModel.h | 17 +++++++++++++- 8 files changed, 99 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf2b5959..577cbffc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b + GIT_TAG 31e300546eb63ea25b0b879fb255beee6022da03 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index 98ab9629..f498dd5a 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,7 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "70fa15de3ec84cf0c0ab6250f2e5e62f34a6d05b", + "commit": "31e300546eb63ea25b0b879fb255beee6022da03", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 95a025cf..e4dc267b 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -85,6 +85,20 @@ Item { width: 16 } + ImageButton { + id: editButton + + visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/edit.png" + ToolTip.visible: hovered + ToolTip.text: model.isEditable ? qsTr("Edit") : qsTr("Edited") + onClicked: if (model.isEditable) chat.model.editAction(model.id) + } + EmojiButton { id: reactButton diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp index 212c2970..e6bc61b0 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp @@ -34,6 +34,20 @@ struct detector>, Op, Args...> template class Op, class... Args> using is_detected = typename detail::detector::value_t; +struct IsStateEvent +{ + template + bool operator()(const mtx::events::StateEvent &) + { + return true; + } + template + bool operator()(const mtx::events::Event &) + { + return false; + } +}; + struct EventMsgType { template @@ -476,3 +490,9 @@ mtx::accessors::serialize_event(const mtx::events::collections::TimelineEvents & { return std::visit([](const auto &e) { return nlohmann::json(e); }, event); } + +bool +mtx::accessors::is_state_event(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(IsStateEvent{}, event); +} diff --git a/src/EventAccessors.h b/src/EventAccessors.h index 95e5df24..7bf695fc 100644 --- a/src/EventAccessors.h +++ b/src/EventAccessors.h @@ -17,6 +17,9 @@ room_id(const mtx::events::collections::TimelineEvents &event); std::string sender(const mtx::events::collections::TimelineEvents &event); +bool +is_state_event(const mtx::events::collections::TimelineEvents &event); + QDateTime origin_server_ts(const mtx::events::collections::TimelineEvents &event); diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index e5a66e19..94d43a83 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -774,15 +774,17 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, if (id.empty()) return nullptr; - std::string id_ = std::string(id); + IdIndex index{room_id_, std::string(id)}; if (resolve_edits) { - auto edits_ = edits(id_); - if (!edits_.empty()) - id_ = mtx::accessors::event_id(edits_.back()); + auto edits_ = edits(index.id); + if (!edits_.empty()) { + index.id = mtx::accessors::event_id(edits_.back()); + auto event_ptr = + new mtx::events::collections::TimelineEvents(std::move(edits_.back())); + events_by_id_.insert(index, event_ptr); + } } - IdIndex index{room_id_, id_}; - auto event_ptr = events_by_id_.object(index); if (!event_ptr) { auto event = cache::client()->getEvent(room_id_, index.id); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index c47194f5..dd4f8696 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -288,6 +288,8 @@ TimelineModel::roleNames() const {ProportionalHeight, "proportionalHeight"}, {Id, "id"}, {State, "state"}, + {IsEdited, "isEdited"}, + {IsEditable, "isEditable"}, {IsEncrypted, "isEncrypted"}, {IsRoomEncrypted, "isRoomEncrypted"}, {ReplyTo, "replyTo"}, @@ -409,8 +411,12 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(prop > 0 ? prop : 1.); } - case Id: - return QVariant(QString::fromStdString(event_id(event))); + case Id: { + if (auto replaces = relations(event).replaces()) + return QVariant(QString::fromStdString(replaces.value())); + else + return QVariant(QString::fromStdString(event_id(event))); + } case State: { auto id = QString::fromStdString(event_id(event)); auto containsOthers = [](const auto &vec) { @@ -430,6 +436,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r else return qml_mtx_events::Received; } + case IsEdited: + return QVariant(relations(event).replaces().has_value()); + case IsEditable: + return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == + http::client()->user_id().to_string()); case IsEncrypted: { auto id = event_id(event); auto encrypted_event = events.get(id, id, false); @@ -444,7 +455,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case ReplyTo: return QVariant(QString::fromStdString(relations(event).reply_to().value_or(""))); case Reactions: { - auto id = event_id(event); + auto id = relations(event).replaces().value_or(event_id(event)); return QVariant::fromValue(events.reactions(id)); } case RoomId: @@ -813,6 +824,12 @@ TimelineModel::replyAction(QString id) setReply(id); } +void +TimelineModel::editAction(QString id) +{ + setEdit(id); +} + RelatedInfo TimelineModel::relatedInfo(QString id) { @@ -1501,6 +1518,22 @@ TimelineModel::formatMemberEvent(QString id) return rendered; } +void +TimelineModel::setEdit(QString newEdit) +{ + if (edit_ != newEdit) { + edit_ = newEdit; + emit editChanged(edit_); + + auto ev = events.get(newEdit.toStdString(), ""); + if (ev) { + setReply(QString::fromStdString( + mtx::accessors::relations(*ev).reply_to().value_or(""))); + // input()->setText(mtx::accessors::body(*ev)); + } + } +} + QString TimelineModel::roomName() const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 51b8049e..463d8705 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -145,6 +145,7 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(std::vector typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged) Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) + Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) Q_PROPERTY( bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged) Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) @@ -181,6 +182,8 @@ public: ProportionalHeight, Id, State, + IsEdited, + IsEditable, IsEncrypted, IsRoomEncrypted, ReplyTo, @@ -213,6 +216,7 @@ public: Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; Q_INVOKABLE void openUserProfile(QString userid, bool global = false); + Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; Q_INVOKABLE void redactEvent(QString id); @@ -268,6 +272,16 @@ public slots: emit replyChanged(reply_); } } + QString edit() const { return edit_; } + void setEdit(QString newEdit); + void resetEdit() + { + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + resetReply(); + } + } void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } void clearTimeline() { events.clearTimeline(); } void receivedSessionKey(const std::string &session_key) @@ -292,6 +306,7 @@ signals: void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void typingUsersChanged(std::vector users); void replyChanged(QString reply); + void editChanged(QString reply); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); @@ -322,7 +337,7 @@ private: bool m_paginationInProgress = false; QString currentId; - QString reply_; + QString reply_, edit_; std::vector typingUsers_; TimelineViewManager *manager_; From 9b7d33e847b02031fdc153716614f125992b3734 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 1 Feb 2021 02:22:53 +0100 Subject: [PATCH 27/50] Implement message editing The UI still looks ugly, but I have no good idea atm. fixes #134 --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.json | 2 +- resources/qml/MessageInput.qml | 4 ++++ resources/qml/ReplyPopup.qml | 21 ++++++++++++++--- resources/qml/TimelineRow.qml | 1 + src/timeline/InputBar.cpp | 40 +++++++++++++++++++++++++++++++- src/timeline/InputBar.h | 2 ++ src/timeline/TimelineModel.cpp | 24 ++++++++++++++++++- src/timeline/TimelineModel.h | 9 +------ 9 files changed, 90 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 577cbffc..2d3c189f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 31e300546eb63ea25b0b879fb255beee6022da03 + GIT_TAG fee5298f068394958c2de935836a2c145f273906 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index f498dd5a..453d6c8a 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -220,7 +220,7 @@ "name": "mtxclient", "sources": [ { - "commit": "31e300546eb63ea25b0b879fb255beee6022da03", + "commit": "fee5298f068394958c2de935836a2c145f273906", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 9a83b52b..d665566c 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -261,6 +261,10 @@ Rectangle { Connections { ignoreUnknownSignals: true onInsertText: messageInput.insert(messageInput.cursorPosition, text) + onTextChanged: { + messageInput.text = newText; + messageInput.cursorPosition = newText.length; + } target: TimelineManager.timeline ? TimelineManager.timeline.input : null } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 4659e075..85b64114 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -10,14 +10,15 @@ Rectangle { property var room: TimelineManager.timeline Layout.fillWidth: true - visible: room && room.reply + visible: room && (room.reply || room.edit) // Height of child, plus margins, plus border - implicitHeight: replyPreview.height + 10 + implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10 color: colors.window z: 3 Reply { id: replyPreview + visible: room && room.reply anchors.left: parent.left anchors.leftMargin: 2 * 22 + 3 * 16 @@ -31,9 +32,10 @@ Rectangle { ImageButton { id: closeReplyButton + visible: room && room.reply anchors.right: parent.right - anchors.rightMargin: 15 + anchors.rightMargin: 16 anchors.top: replyPreview.top hoverEnabled: true width: 16 @@ -44,4 +46,17 @@ Rectangle { onClicked: room.reply = undefined } + Button { + id: closeEditButton + visible: room && room.edit + + anchors.left: parent.left + anchors.rightMargin: 16 + anchors.topMargin: 10 + anchors.top: parent.top + //height: 16 + text: qsTr("Abort edit") + onClicked: room.edit = undefined + } + } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index e4dc267b..d4f058e5 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -89,6 +89,7 @@ Item { id: editButton visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited + buttonTextColor: chat.model.edit == model.id ? colors.highlight : colors.buttonText Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index 738fb37c..08cbd15b 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -268,7 +268,18 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) text.format = "org.matrix.custom.html"; } - if (!room->reply().isEmpty()) { + if (!room->edit().isEmpty()) { + if (!room->reply().isEmpty()) { + text.relations.relations.push_back( + {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); + room->resetReply(); + } + + text.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + + } else if (!room->reply().isEmpty()) { auto related = room->relatedInfo(room->reply()); QString body; @@ -321,6 +332,11 @@ InputBar::emote(QString msg) {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + emote.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage); } @@ -352,6 +368,11 @@ InputBar::image(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + image.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(image, mtx::events::EventType::RoomMessage); } @@ -378,6 +399,11 @@ InputBar::file(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + file.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(file, mtx::events::EventType::RoomMessage); } @@ -405,6 +431,11 @@ InputBar::audio(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + audio.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage); } @@ -431,6 +462,11 @@ InputBar::video(const QString &filename, {mtx::common::RelationType::InReplyTo, room->reply().toStdString()}); room->resetReply(); } + if (!room->edit().isEmpty()) { + video.relations.relations.push_back( + {mtx::common::RelationType::Replace, room->edit().toStdString()}); + room->resetEdit(); + } room->sendMessageEvent(video, mtx::events::EventType::RoomMessage); } @@ -524,6 +560,8 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList & [this](const QByteArray data, const QString &mime, const QString &fn) { setUploading(true); + setText(""); + auto payload = std::string(data.data(), data.size()); std::optional encryptedFile; if (cache::isRoomEncrypted(room->roomId().toStdString())) { diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index f173bbc0..696a0dd9 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -41,6 +41,7 @@ public slots: QString text() const; QString previousText(); QString nextText(); + void setText(QString newText) { emit textChanged(newText); } void send(); void paste(bool fromMouse); @@ -58,6 +59,7 @@ private slots: signals: void insertText(QString text); + void textChanged(QString newText); void uploadingChanged(bool value); private: diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index dd4f8696..de43d5ea 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1529,11 +1529,33 @@ TimelineModel::setEdit(QString newEdit) if (ev) { setReply(QString::fromStdString( mtx::accessors::relations(*ev).reply_to().value_or(""))); - // input()->setText(mtx::accessors::body(*ev)); + + auto msgType = mtx::accessors::msg_type(*ev); + if (msgType == mtx::events::MessageType::Text || + msgType == mtx::events::MessageType::Notice) { + input()->setText(relatedInfo(newEdit).quoted_body); + } else if (msgType == mtx::events::MessageType::Emote) { + input()->setText("/me " + relatedInfo(newEdit).quoted_body); + } else { + input()->setText(""); + } + } else { + input()->setText(""); } } } +void +TimelineModel::resetEdit() +{ + if (!edit_.isEmpty()) { + edit_ = ""; + emit editChanged(edit_); + input()->setText(""); + resetReply(); + } +} + QString TimelineModel::roomName() const { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 463d8705..0aec27a1 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -274,14 +274,7 @@ public slots: } QString edit() const { return edit_; } void setEdit(QString newEdit); - void resetEdit() - { - if (!edit_.isEmpty()) { - edit_ = ""; - emit editChanged(edit_); - resetReply(); - } - } + void resetEdit(); void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; } void clearTimeline() { events.clearTimeline(); } void receivedSessionKey(const std::string &session_key) From 6e2ae1d81204cffc28d2bf5e3e1cda3f4136c0f4 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 2 Feb 2021 18:54:45 +0100 Subject: [PATCH 28/50] Add edit shortcuts and fix some focus stuff --- resources/qml/MessageInput.qml | 6 +++++- resources/qml/MessageView.qml | 12 +++++++++++- resources/qml/ReplyPopup.qml | 6 +++--- resources/qml/TimelineRow.qml | 8 ++++++-- resources/qml/TimelineView.qml | 5 +++++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index d665566c..1b40931f 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -139,6 +139,7 @@ Rectangle { if (TimelineManager.timeline) TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + forceActiveFocus(); } onCursorRectangleChanged: textInput.ensureVisible(cursorRectangle) onCursorPositionChanged: { @@ -260,7 +261,9 @@ Rectangle { Connections { ignoreUnknownSignals: true - onInsertText: messageInput.insert(messageInput.cursorPosition, text) + onInsertText: { + messageInput.insert(messageInput.cursorPosition, text); + } onTextChanged: { messageInput.text = newText; messageInput.cursorPosition = newText.length; @@ -271,6 +274,7 @@ Rectangle { Connections { ignoreUnknownSignals: true onReplyChanged: messageInput.forceActiveFocus() + onEditChanged: messageInput.forceActiveFocus() target: TimelineManager.timeline } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index dafca0f6..0f058830 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -50,7 +50,12 @@ ListView { Shortcut { sequence: StandardKey.Cancel - onActivated: chat.model.reply = undefined + onActivated: { + if (chat.model.edit) + chat.model.edit = undefined; + else + chat.model.reply = undefined; + } } Shortcut { @@ -66,6 +71,11 @@ ListView { } } + Shortcut { + sequence: "Ctrl+E" + onActivated: chat.model.edit = chat.model.reply + } + Component { id: sectionHeader diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 85b64114..9e97d7aa 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -18,8 +18,8 @@ Rectangle { Reply { id: replyPreview - visible: room && room.reply + visible: room && room.reply anchors.left: parent.left anchors.leftMargin: 2 * 22 + 3 * 16 anchors.right: closeReplyButton.left @@ -32,8 +32,8 @@ Rectangle { ImageButton { id: closeReplyButton - visible: room && room.reply + visible: room && room.reply anchors.right: parent.right anchors.rightMargin: 16 anchors.top: replyPreview.top @@ -48,8 +48,8 @@ Rectangle { Button { id: closeEditButton - visible: room && room.edit + visible: room && room.edit anchors.left: parent.left anchors.rightMargin: 16 anchors.topMargin: 10 diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index d4f058e5..9f054b9b 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -88,7 +88,7 @@ Item { ImageButton { id: editButton - visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited + visible: (Settings.buttonsInTimeline && model.isEditable) || model.isEdited buttonTextColor: chat.model.edit == model.id ? colors.highlight : colors.buttonText Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 @@ -97,7 +97,11 @@ Item { image: ":/icons/icons/ui/edit.png" ToolTip.visible: hovered ToolTip.text: model.isEditable ? qsTr("Edit") : qsTr("Edited") - onClicked: if (model.isEditable) chat.model.editAction(model.id) + onClicked: { + if (model.isEditable) + chat.model.editAction(model.id); + + } } EmojiButton { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c03e8d31..4b3c006a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -91,6 +91,11 @@ Page { onClicked: TimelineManager.timeline.replyAction(messageContextMenu.eventId) } + MenuItem { + text: qsTr("Edit") + onClicked: TimelineManager.timeline.editAction(messageContextMenu.eventId) + } + MenuItem { text: qsTr("Read receipts") onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) From bdb6e6b79e9beeaabfbde99cd760de77247d11a4 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 01:03:20 +0100 Subject: [PATCH 29/50] Fix stuck notifications because of edits Does not fix the read status yet, for that we need to compare read receipts for all events after the last visible event. --- src/Cache.cpp | 90 ++++++++++++++++++++++++++++++++++ src/Cache.h | 6 +++ src/Cache_p.h | 5 ++ src/timeline/TimelineModel.cpp | 21 ++++++-- src/timeline/TimelineModel.h | 2 +- 5 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 49861a9a..109fc60d 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1896,6 +1896,84 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) return *val.data(); } +std::optional +Cache::getEventIndex(const std::string &room_id, std::string_view event_id) +{ + if (event_id.empty()) + return {}; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val indexVal{event_id.data(), event_id.size()}, val; + + bool success = lmdb::dbi_get(txn, orderDb, indexVal, val); + if (!success) { + nhlog::db()->critical("Could not find event id: {}", event_id); + return {}; + } + + return *val.data(); +} + +std::optional> +Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) +{ + if (event_id.empty()) + return {}; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::dbi orderDb{0}; + lmdb::dbi eventOrderDb{0}; + lmdb::dbi timelineDb{0}; + try { + orderDb = getEventToOrderDb(txn, room_id); + eventOrderDb = getEventOrderDb(txn, room_id); + timelineDb = getMessageToOrderDb(txn, room_id); + } catch (lmdb::runtime_error &e) { + nhlog::db()->error("Can't open db for room '{}', probably doesn't exist yet. ({})", + room_id, + e.what()); + return {}; + } + + lmdb::val eventIdVal{event_id.data(), event_id.size()}, indexVal; + + bool success = lmdb::dbi_get(txn, orderDb, eventIdVal, indexVal); + if (!success) { + return {}; + } + uint64_t prevIdx = *indexVal.data(); + std::string prevId{eventIdVal.data(), eventIdVal.size()}; + + auto cursor = lmdb::cursor::open(txn, eventOrderDb); + cursor.get(indexVal, MDB_SET); + while (cursor.get(indexVal, eventIdVal, MDB_NEXT)) { + std::string evId = + json::parse(std::string_view(eventIdVal.data(), eventIdVal.size()))["event_id"] + .get(); + lmdb::val temp; + if (lmdb::dbi_get(txn, timelineDb, lmdb::val(evId.data(), evId.size()), temp)) { + return std::pair{prevIdx, std::string(prevId)}; + } else { + prevIdx = *indexVal.data(); + prevId = std::move(evId); + } + } + + return std::pair{prevIdx, std::string(prevId)}; +} + std::optional Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id) { @@ -4253,6 +4331,18 @@ readReceipts(const QString &event_id, const QString &room_id) return instance_->readReceipts(event_id, room_id); } +std::optional +getEventIndex(const std::string &room_id, std::string_view event_id) +{ + return instance_->getEventIndex(room_id, event_id); +} + +std::optional> +lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id) +{ + return instance_->lastInvisibleEventAfter(room_id, event_id); +} + QByteArray image(const QString &url) { diff --git a/src/Cache.h b/src/Cache.h index 91956725..e60fc970 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -168,6 +168,12 @@ using UserReceipts = std::multimap UserReceipts readReceipts(const QString &event_id, const QString &room_id); +//! get index of the event in the event db, not representing the visual index +std::optional +getEventIndex(const std::string &room_id, std::string_view event_id); +std::optional> +lastInvisibleEventAfter(const std::string &room_id, std::string_view event_id); + QByteArray image(const QString &url); QByteArray diff --git a/src/Cache_p.h b/src/Cache_p.h index c96a3f30..431e7bc3 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -204,6 +204,11 @@ public: std::optional getTimelineRange(const std::string &room_id); std::optional getTimelineIndex(const std::string &room_id, std::string_view event_id); + std::optional getEventIndex(const std::string &room_id, + std::string_view event_id); + std::optional> lastInvisibleEventAfter( + const std::string &room_id, + std::string_view event_id); std::optional getTimelineEventId(const std::string &room_id, uint64_t index); std::optional getArrivalIndex(const std::string &room_id, std::string_view event_id); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index de43d5ea..1163d931 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -740,10 +740,25 @@ TimelineModel::setCurrentIndex(int index) auto oldIndex = idToIndex(currentId); currentId = indexToId(index); - emit currentIndexChanged(index); + if (index != oldIndex) + emit currentIndexChanged(index); - if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) { - readEvent(currentId.toStdString()); + if (!currentId.startsWith("m")) { + auto oldReadIndex = + cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString()); + auto nextEventIndexAndId = + cache::lastInvisibleEventAfter(roomId().toStdString(), currentId.toStdString()); + + if (nextEventIndexAndId && + (!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) { + readEvent(nextEventIndexAndId->second); + currentReadId = QString::fromStdString(nextEventIndexAndId->second); + + nhlog::net()->info("Marked as read {}, index {}, oldReadIndex {}", + nextEventIndexAndId->second, + nextEventIndexAndId->first, + *oldReadIndex); + } } } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0aec27a1..017b6589 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -329,7 +329,7 @@ private: bool decryptDescription = true; bool m_paginationInProgress = false; - QString currentId; + QString currentId, currentReadId; QString reply_, edit_; std::vector typingUsers_; From 6d678a108f0f1c551565c124de478f366dbe4ee2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 02:37:47 +0100 Subject: [PATCH 30/50] Use fully read marker and fix stuck read marker with edits --- src/Cache.cpp | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index 109fc60d..8cf66d21 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1202,25 +1202,24 @@ Cache::calculateRoomReadStatus(const std::string &room_id) const auto last_event_id = getLastEventId(txn, room_id); const auto localUser = utils::localUser().toStdString(); + std::string fullyReadEventId; + if (auto ev = getAccountData(txn, mtx::events::EventType::FullyRead, room_id)) { + if (auto fr = std::get_if< + mtx::events::AccountDataEvent>( + &ev.value())) { + fullyReadEventId = fr->content.event_id; + } + } txn.commit(); - if (last_event_id.empty()) + if (last_event_id.empty() || fullyReadEventId.empty()) + return true; + + if (last_event_id == fullyReadEventId) return false; // Retrieve all read receipts for that event. - const auto receipts = - readReceipts(QString::fromStdString(last_event_id), QString::fromStdString(room_id)); - - if (receipts.size() == 0) - return true; - - // Check if the local user has a read receipt for it. - for (auto it = receipts.cbegin(); it != receipts.cend(); it++) { - if (it->second == localUser) - return false; - } - - return true; + return getEventIndex(room_id, last_event_id) > getEventIndex(room_id, fullyReadEventId); } void @@ -1918,7 +1917,6 @@ Cache::getEventIndex(const std::string &room_id, std::string_view event_id) bool success = lmdb::dbi_get(txn, orderDb, indexVal, val); if (!success) { - nhlog::db()->critical("Could not find event id: {}", event_id); return {}; } @@ -3320,9 +3318,12 @@ Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::st lmdb::val data; if (lmdb::dbi_get(txn, db, lmdb::val(to_string(type)), data)) { mtx::responses::utils::RoomAccountDataEvents events; - mtx::responses::utils::parse_room_account_data_events( - std::string_view(data.data(), data.size()), events); - return events.front(); + json j = json::array({ + json::parse(std::string_view(data.data(), data.size())), + }); + mtx::responses::utils::parse_room_account_data_events(j, events); + if (events.size() == 1) + return events.front(); } } catch (...) { } From 29c89b1b9eb3e0e36f52341bcdc470274724a90a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 14:11:55 +0100 Subject: [PATCH 31/50] Abort -> Cancel --- resources/qml/ReplyPopup.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 9e97d7aa..c07c2c44 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -55,7 +55,7 @@ Rectangle { anchors.topMargin: 10 anchors.top: parent.top //height: 16 - text: qsTr("Abort edit") + text: qsTr("Cancel edit") onClicked: room.edit = undefined } From a62276c28933986907022662cea965cf6269eb5e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 14:32:16 +0100 Subject: [PATCH 32/50] Fix UI allowing edits of foreign messages in some cases --- resources/qml/MessageInput.qml | 4 ++-- resources/qml/MessageView.qml | 4 +++- resources/qml/TimelineRow.qml | 6 +++--- resources/qml/TimelineView.qml | 6 +++++- resources/qml/emoji/EmojiButton.qml | 2 +- src/timeline/TimelineModel.cpp | 10 ++++++---- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 1b40931f..b5c96660 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -165,7 +165,7 @@ Rectangle { event.accepted = true; } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon - if(cursorPosition == completerTriggeredAt + 1) + if (cursorPosition == completerTriggeredAt + 1) popup.close(); if (popup.opened && popup.count <= 0) @@ -310,7 +310,7 @@ Rectangle { ToolTip.text: qsTr("Emoji") onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) { messageInput.insert(messageInput.cursorPosition, emoji); - TimelineManager.focusMessageInput() + TimelineManager.focusMessageInput(); }) } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 0f058830..50e051ab 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -73,7 +73,9 @@ ListView { Shortcut { sequence: "Ctrl+E" - onActivated: chat.model.edit = chat.model.reply + onActivated: { + chat.model.edit = chat.model.reply; + } } Component { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 9f054b9b..5ec23d62 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -26,12 +26,12 @@ Item { acceptedButtons: Qt.AllButtons onClicked: { if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, row); + messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row); else mouse.accepted = false; } onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)); + messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, mouse.x, mouse.y)); } } @@ -143,7 +143,7 @@ Item { image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip.visible: hovered ToolTip.text: qsTr("Options") - onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) + onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, optionsButton) } Label { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4b3c006a..b0880493 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -66,11 +66,13 @@ Page { property string eventId property int eventType property bool isEncrypted + property bool isEditable - function show(eventId_, eventType_, isEncrypted_, showAt_, position) { + function show(eventId_, eventType_, isEncrypted_, isEditable_, showAt_, position) { eventId = eventId_; eventType = eventType_; isEncrypted = isEncrypted_; + isEditable = isEditable_; if (position) popup(position, showAt_); else @@ -92,6 +94,8 @@ Page { } MenuItem { + visible: messageContextMenu.isEditable + height: visible ? implicitHeight : 0 text: qsTr("Edit") onClicked: TimelineManager.timeline.editAction(messageContextMenu.eventId) } diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml index 622f8aa2..dd7530a6 100644 --- a/resources/qml/emoji/EmojiButton.qml +++ b/resources/qml/emoji/EmojiButton.qml @@ -14,6 +14,6 @@ ImageButton { image: ":/icons/icons/ui/smile.png" onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { TimelineManager.queueReactionMessage(event_id, emoji); - TimelineManager.focusMessageInput() + TimelineManager.focusMessageInput(); }) } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 1163d931..493f755b 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1537,11 +1537,11 @@ void TimelineModel::setEdit(QString newEdit) { if (edit_ != newEdit) { - edit_ = newEdit; - emit editChanged(edit_); - auto ev = events.get(newEdit.toStdString(), ""); - if (ev) { + if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { + edit_ = newEdit; + emit editChanged(edit_); + setReply(QString::fromStdString( mtx::accessors::relations(*ev).reply_to().value_or(""))); @@ -1555,6 +1555,8 @@ TimelineModel::setEdit(QString newEdit) input()->setText(""); } } else { + edit_ = ""; + emit editChanged(edit_); input()->setText(""); } } From 6a2e8a695237142390c938ea5638132905f48aff Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 14:38:41 +0100 Subject: [PATCH 33/50] Try to avoid QTBUG-89568 --- resources/qml/MessageView.qml | 2 +- resources/qml/delegates/NoticeMessage.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 50e051ab..b0498c4e 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -194,7 +194,7 @@ ListView { Connections { target: chat onMovementEnded: { - if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) + if (y + height + 2 * chat.spacing > chat.contentY + timelineRoot.height && y < chat.contentY + timelineRoot.height) chat.model.currentIndex = index; } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index 67a69055..c38cb555 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,6 +1,6 @@ TextMessage { font.italic: true color: colors.buttonText - height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined + height: isReply ? Math.min(timelineRoot.height / 8, implicitHeight) : undefined clip: isReply } From 2606568376e032a5b66275397a52267f2e4c4578 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 15:24:00 +0100 Subject: [PATCH 34/50] Fix messages sometimes not being rendered, when they are too large --- resources/qml/MessageView.qml | 1 - src/timeline/DelegateChooser.cpp | 4 ---- 2 files changed, 5 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index b0498c4e..09dc4e36 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -13,7 +13,6 @@ ListView { Layout.fillWidth: true Layout.fillHeight: true - cacheBuffer: 400 model: TimelineManager.timeline boundsBehavior: Flickable.StopAtBounds pixelAligned: true diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp index 1f5fae7e..8598fa77 100644 --- a/src/timeline/DelegateChooser.cpp +++ b/src/timeline/DelegateChooser.cpp @@ -123,10 +123,6 @@ DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) } chooser.child_->setParentItem(&chooser); - connect(chooser.child_, &QQuickItem::heightChanged, &chooser, [this]() { - chooser.setHeight(chooser.child_->height()); - }); - chooser.setHeight(chooser.child_->height()); QQmlEngine::setObjectOwnership(chooser.child_, QQmlEngine::ObjectOwnership::JavaScriptOwnership); emit chooser.childChanged(); From 6c65136101b09b4f883b7bfedfc4dc125f7766ed Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 15:31:33 +0100 Subject: [PATCH 35/50] Update translations --- resources/langs/nheko_cs.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_de.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_el.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_en.ts | 139 ++++++++++++++++++++++++++------- resources/langs/nheko_eo.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_et.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_fi.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_fr.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_hu.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_it.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_ja.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_ml.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_nl.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_pl.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_pt_PT.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_ro.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_ru.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_si.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_sv.ts | 136 +++++++++++++++++++++++++------- resources/langs/nheko_zh_CN.ts | 136 +++++++++++++++++++++++++------- 20 files changed, 2183 insertions(+), 540 deletions(-) diff --git a/resources/langs/nheko_cs.ts b/resources/langs/nheko_cs.ts index 22bbd88b..8ab0dd25 100644 --- a/resources/langs/nheko_cs.ts +++ b/resources/langs/nheko_cs.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1300,12 +1305,12 @@ Example: https://server.my:8787 - + You joined this room. - + Rejected the knock from %1. @@ -1329,7 +1334,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1347,7 +1362,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1356,6 +1371,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1480,7 +1500,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1503,7 +1528,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1522,11 +1547,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray @@ -1546,12 +1600,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1604,6 +1663,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1714,7 +1796,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1724,7 +1806,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1799,7 +1881,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1819,17 +1901,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + Touchscreen mode @@ -1899,7 +1981,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File @@ -1995,7 +2077,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index 9b827a91..18327ea3 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Du bist dem Raum beigetreten. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Nutzer konnte nicht eingeladen werden: %1 - + Invited user: %1 Eingeladener Benutzer: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Verschlüsseltes Event (keine Schlüssel zur Entschlüsselung gefunden) -- @@ -568,7 +568,7 @@ Alle Dateien (*) - + Failed to upload media. Please try again. Medienupload fehlgeschlagen. Bitte versuche es erneut. @@ -801,12 +801,12 @@ Beispiel: https://mein.server:8787 Schreibe eine Nachricht… - + Emoji Emoji - + Send Senden @@ -1003,15 +1003,20 @@ Beispiel: https://mein.server:8787 ReplyPopup - + Close Schließen + + + Cancel edit + + RoomInfo - + no version stored keine Version gespeichert @@ -1153,7 +1158,7 @@ Beispiel: https://mein.server:8787 TimelineModel - + Message redaction failed: %1 Nachricht zurückziehen fehlgeschlagen: %1 @@ -1303,12 +1308,12 @@ Beispiel: https://mein.server:8787 %1 hat das Anklopfen zurückgezogen. - + You joined this room. Du bist dem Raum beigetreten. - + Rejected the knock from %1. Hat das Anklopfen von %1 abgewiesen. @@ -1332,7 +1337,17 @@ Beispiel: https://mein.server:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Reaktion senden @@ -1350,7 +1365,7 @@ Beispiel: https://mein.server:8787 TimelineView - + React Reaktion senden @@ -1359,6 +1374,11 @@ Beispiel: https://mein.server:8787 Reply Antworten + + + Edit + + Read receipts @@ -1483,7 +1503,12 @@ Beispiel: https://mein.server:8787 Status: - + + User Profile Settings + + + + Set presence automatically Setze Präsenz automatisch @@ -1506,7 +1531,7 @@ Beispiel: https://mein.server:8787 UserProfile - + Verify Verifizieren @@ -1525,11 +1550,40 @@ Beispiel: https://mein.server:8787 Kick the user Kicke den Nutzer + + + Select an avatar + Wähle einen Avatar + + + + All Files (*) + Alle Dateien (*) + + + + The selected file is not an image + Die ausgewählte Datei ist kein Bild + + + + Error while reading file: %1 + Fehler beim Lesen der DateI: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Ins Benachrichtigungsfeld minimieren @@ -1549,12 +1603,17 @@ Beispiel: https://mein.server:8787 Runde Profilbilder - + profile: %1 Profil: %1 - + + Default + + + + CALLS TELEFONATE @@ -1609,6 +1668,29 @@ Betrifft nur Nachrichten in verschlüsselten Chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Zeige Buttons in der Historie @@ -1725,7 +1807,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.Teile Schlüssel mit verifizierten Nutzern und Geräten - + CACHED IM CACHE @@ -1735,7 +1817,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.NICHT IM CACHE - + Scale factor Skalierungsfaktor @@ -1810,7 +1892,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.Gerätefingerabdruck - + Session Keys Sitzungsschlüssel @@ -1830,17 +1912,17 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.VERSCHLÜSSELUNG - + GENERAL ALLGEMEINES - + INTERFACE OBERFLÄCHE - + Touchscreen mode Touchscreenmodus @@ -1910,7 +1992,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein.Alle Dateien (*) - + Open Sessions File Öffne Sessions Datei @@ -2006,7 +2088,7 @@ Normalerweise animiert das den Taskbaricon oder färbt das Fenster orange ein. descriptiveTime - + Yesterday Gestern diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index e24539ce..f86dfcab 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ Όλα τα αρχεία (*) - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 Γράψε ένα μήνυμα... - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + Όλα τα αρχεία (*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Ελαχιστοποίηση @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL ΓΕΝΙΚΑ - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash Όλα τα αρχεία (*) - + Open Sessions File @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index 9919e89e..35c70c0b 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Failed to invite user: %1 - + Invited user: %1 Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Encrypted Event (No keys found for decryption) -- @@ -568,7 +568,7 @@ All Files (*) - + Failed to upload media. Please try again. Failed to upload media. Please try again. @@ -801,12 +801,12 @@ Example: https://server.my:8787 Write a message… - + Emoji Emoji - + Send Send @@ -1003,15 +1003,20 @@ Example: https://server.my:8787 ReplyPopup - + Close Close + + + Cancel edit + Cancel edit + RoomInfo - + no version stored no version stored @@ -1153,7 +1158,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Message redaction failed: %1 @@ -1303,12 +1308,12 @@ Example: https://server.my:8787 %1 redacted their knock. - + You joined this room. You joined this room. - + Rejected the knock from %1. Rejected the knock from %1. @@ -1332,7 +1337,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + Edit + + + + Edited + Edited + + + React React @@ -1350,7 +1365,7 @@ Example: https://server.my:8787 TimelineView - + React React @@ -1359,6 +1374,11 @@ Example: https://server.my:8787 Reply Reply + + + Edit + Edit + Read receipts @@ -1483,7 +1503,12 @@ Example: https://server.my:8787 Status: - + + User Profile Settings + User Profile Settings + + + Set presence automatically Set presence automatically @@ -1506,7 +1531,7 @@ Example: https://server.my:8787 UserProfile - + Verify Verify @@ -1525,11 +1550,40 @@ Example: https://server.my:8787 Kick the user Kick the user + + + Select an avatar + Select an avatar + + + + All Files (*) + All Files (*) + + + + The selected file is not an image + The selected file is not an image + + + + Error while reading file: %1 + Error while reading file: %1 + + + + UserSettings + + + + Default + Default + UserSettingsPage - + Minimize to tray Minimize to tray @@ -1549,12 +1603,17 @@ Example: https://server.my:8787 Circular Avatars - + profile: %1 profile: %1 - + + Default + Default + + + CALLS CALLS @@ -1609,6 +1668,32 @@ Only affects messages in encrypted chats. + Privacy Screen + Privacy Screen + + + + When the window loses focus, the timeline will +be blurred. + When the window loses focus, the timeline will +be blurred. + + + + Privacy screen timeout (in seconds [0 - 3600]) + Privacy screen timeout (in seconds [0 - 3600]) + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + Show buttons in timeline Show buttons in timeline @@ -1725,7 +1810,7 @@ This usually causes the application icon in the task bar to animate in some fash Share keys with verified users and devices - + CACHED CACHED @@ -1735,7 +1820,7 @@ This usually causes the application icon in the task bar to animate in some fash NOT CACHED - + Scale factor Scale factor @@ -1810,7 +1895,7 @@ This usually causes the application icon in the task bar to animate in some fash Device Fingerprint - + Session Keys Session Keys @@ -1830,17 +1915,17 @@ This usually causes the application icon in the task bar to animate in some fash ENCRYPTION - + GENERAL GENERAL - + INTERFACE INTERFACE - + Touchscreen mode Touchscreen mode @@ -1910,7 +1995,7 @@ This usually causes the application icon in the task bar to animate in some fash All Files (*) - + Open Sessions File Open Sessions File @@ -2006,7 +2091,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Yesterday diff --git a/resources/langs/nheko_eo.ts b/resources/langs/nheko_eo.ts index b09d8240..5f0eade7 100644 --- a/resources/langs/nheko_eo.ts +++ b/resources/langs/nheko_eo.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Vi aliĝis ĉi tiun ĉambron. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -480,7 +480,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -569,7 +569,7 @@ Ĉiuj dosieroj (*) - + Failed to upload media. Please try again. @@ -802,12 +802,12 @@ Ekzemplo: https://servisto.mia:8787 Skribu mesaĝon... - + Emoji - + Send Sendu @@ -1004,15 +1004,20 @@ Ekzemplo: https://servisto.mia:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1156,7 +1161,7 @@ Ekzemplo: https://servisto.mia:8787 TimelineModel - + Message redaction failed: %1 @@ -1308,12 +1313,12 @@ Ekzemplo: https://servisto.mia:8787 - + You joined this room. Vi aliĝis ĉi tiun ĉambron. - + Rejected the knock from %1. @@ -1337,7 +1342,17 @@ Ekzemplo: https://servisto.mia:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Reagu @@ -1355,7 +1370,7 @@ Ekzemplo: https://servisto.mia:8787 TimelineView - + React Reagu @@ -1364,6 +1379,11 @@ Ekzemplo: https://servisto.mia:8787 Reply Respondu + + + Edit + + Read receipts @@ -1488,7 +1508,12 @@ Ekzemplo: https://servisto.mia:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1511,7 +1536,7 @@ Ekzemplo: https://servisto.mia:8787 UserProfile - + Verify @@ -1530,11 +1555,40 @@ Ekzemplo: https://servisto.mia:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + Ĉiuj dosieroj (*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray @@ -1554,12 +1608,17 @@ Ekzemplo: https://servisto.mia:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1612,6 +1671,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1722,7 +1804,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1732,7 +1814,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1807,7 +1889,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1827,17 +1909,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + Touchscreen mode @@ -1907,7 +1989,7 @@ This usually causes the application icon in the task bar to animate in some fash Ĉiuj dosieroj (*) - + Open Sessions File @@ -2004,7 +2086,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Hieraŭ diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts index e3b46f95..1c7f31b2 100644 --- a/resources/langs/nheko_et.ts +++ b/resources/langs/nheko_et.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Sa liitusid selle jututoaga. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Kutse saatmine kasutajale ei õnnestunud: %1 - + Invited user: %1 Kutsutud kasutaja: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Krüptitud sündmus (Dekrüptimisvõtmeid ei leidunud) -- @@ -568,7 +568,7 @@ Kõik failid (*) - + Failed to upload media. Please try again. Meediafailide üleslaadimine ei õnnestunud. Palun proovi uuesti. @@ -801,12 +801,12 @@ Näiteks: https://server.minu:8787 Kirjuta sõnum… - + Emoji Emoji - + Send Saada @@ -1003,15 +1003,20 @@ Näiteks: https://server.minu:8787 ReplyPopup - + Close Sulge + + + Cancel edit + + RoomInfo - + no version stored salvestatud versiooni ei leidu @@ -1153,7 +1158,7 @@ Näiteks: https://server.minu:8787 TimelineModel - + Message redaction failed: %1 Sõnumi ümbersõnastamine ebaõnnestus: %1 @@ -1303,12 +1308,12 @@ Näiteks: https://server.minu:8787 %1 muutis oma koputust jututoa uksele. - + You joined this room. Sa liitusid jututoaga. - + Rejected the knock from %1. Lükkas tagasi %1 koputuse jututoa uksele. @@ -1332,7 +1337,17 @@ Näiteks: https://server.minu:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Reageeri @@ -1350,7 +1365,7 @@ Näiteks: https://server.minu:8787 TimelineView - + React Reageeri @@ -1359,6 +1374,11 @@ Näiteks: https://server.minu:8787 Reply Vasta + + + Edit + + Read receipts @@ -1483,7 +1503,12 @@ Näiteks: https://server.minu:8787 Olek: - + + User Profile Settings + + + + Set presence automatically Määra oma võrguolek automaatselt @@ -1506,7 +1531,7 @@ Näiteks: https://server.minu:8787 UserProfile - + Verify Verifitseeri @@ -1525,11 +1550,40 @@ Näiteks: https://server.minu:8787 Kick the user Müksa kasutaja välja + + + Select an avatar + Vali tunnuspilt + + + + All Files (*) + Kõik failid (*) + + + + The selected file is not an image + Valitud fail ei ole pildifail + + + + Error while reading file: %1 + Viga faili lugemisel: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Vähenda tegumiribale @@ -1549,12 +1603,17 @@ Näiteks: https://server.minu:8787 Ümmargused tunnuspildid - + profile: %1 Profiil: %1 - + + Default + + + + CALLS KÕNED @@ -1609,6 +1668,29 @@ Kehtib vaid läbivalt krüptitud vestluste puhul. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Näita ajajoonel nuppe @@ -1725,7 +1807,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Jaga võtmeid verifitseeritud kasutajate ja seadmetega - + CACHED PUHVERDATUD @@ -1735,7 +1817,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim PUHVERDAMATA - + Scale factor Mastaabitegur @@ -1810,7 +1892,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Seadme sõrmejälg - + Session Keys Sessioonivõtmed @@ -1830,17 +1912,17 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim KRÜPTIMINE - + GENERAL ÜLDISED SEADISTUSED - + INTERFACE LIIDES - + Touchscreen mode Puuteekraani režiim @@ -1910,7 +1992,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim Kõik failid (*) - + Open Sessions File Ava sessioonide fail @@ -2006,7 +2088,7 @@ See tavaliselt tähendab, et rakenduse ikoon tegumiribal annab mingit sorti anim descriptiveTime - + Yesterday Eile diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index f4a518e5..001baed6 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Salattu viesti (salauksen purkuavaimia ei löydetty) -- @@ -568,7 +568,7 @@ Kaikki Tiedostot (*) - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 Kirjoita viesti… - + Emoji Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close Sulje + + + Cancel edit + + RoomInfo - + no version stored ei tallennettua versiota @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Viestin muokkaus epäonnistui: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + Valitse profiilikuva + + + + All Files (*) + Kaikki Tiedostot (*) + + + + The selected file is not an image + Valittu tiedosto ei ole kuva + + + + Error while reading file: %1 + Virhe lukiessa tiedostoa: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Pienennä ilmoitusalueelle @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Mittakerroin @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash Laitteen sormenjälki - + Session Keys Istunnon avaimet @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash SALAUS - + GENERAL YLEISET ASETUKSET - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash Kaikki Tiedostot (*) - + Open Sessions File Avaa Istuntoavaintiedosto @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Eilen diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index 130bc004..090e2d1b 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Vous avez rejoint ce salon. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Échec lors de l'invitation de %1 - + Invited user: %1 %1 a été invité(e) @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Évènement chiffré (pas de clé trouvée pour le déchiffrement) -- @@ -568,7 +568,7 @@ Tous les types de fichiers (*) - + Failed to upload media. Please try again. Échec de l'envoi du média. Veuillez réessayer. @@ -801,12 +801,12 @@ Exemple : https ://monserveur.example.com :8787 Écrivez un message… - + Emoji Émoji - + Send Envoyer @@ -1003,15 +1003,20 @@ Exemple : https ://monserveur.example.com :8787 ReplyPopup - + Close Fermer + + + Cancel edit + + RoomInfo - + no version stored pas de version enregistrée @@ -1153,7 +1158,7 @@ Exemple : https ://monserveur.example.com :8787 TimelineModel - + Message redaction failed: %1 Échec de la suppression du message : %1 @@ -1303,12 +1308,12 @@ Exemple : https ://monserveur.example.com :8787 %1 ne frappe plus au salon. - + You joined this room. Vous avez rejoint ce salon. - + Rejected the knock from %1. %1 a été rejeté après avoir frappé au salon. @@ -1332,7 +1337,17 @@ Exemple : https ://monserveur.example.com :8787 TimelineRow - + + Edit + + + + + Edited + + + + React Réagir @@ -1350,7 +1365,7 @@ Exemple : https ://monserveur.example.com :8787 TimelineView - + React Réagir @@ -1359,6 +1374,11 @@ Exemple : https ://monserveur.example.com :8787 Reply Réponse + + + Edit + + Read receipts @@ -1483,7 +1503,12 @@ Exemple : https ://monserveur.example.com :8787 Statut : - + + User Profile Settings + + + + Set presence automatically Changer la présence automatiquement @@ -1506,7 +1531,7 @@ Exemple : https ://monserveur.example.com :8787 UserProfile - + Verify Vérifier @@ -1525,11 +1550,40 @@ Exemple : https ://monserveur.example.com :8787 Kick the user Expulser l'utilisateur + + + Select an avatar + Sélectionner un avatar + + + + All Files (*) + Tous les types de fichiers (*) + + + + The selected file is not an image + Le fichier sélectionné n'est pas une image + + + + Error while reading file: %1 + Erreur lors de la lecture du fichier  : %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Réduire à la barre des tâches @@ -1549,12 +1603,17 @@ Exemple : https ://monserveur.example.com :8787 Avatars circulaires - + profile: %1 profil : %1 - + + Default + + + + CALLS APPELS @@ -1609,6 +1668,29 @@ Cela n'affecte que les messages des salons chiffrés. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Montrer les boutons dans la discussion @@ -1726,7 +1808,7 @@ Cela met l'application en évidence dans la barre des tâches.Partager vos clés avec les utilisateurs et appareils que vous avez vérifiés - + CACHED EN CACHE @@ -1736,7 +1818,7 @@ Cela met l'application en évidence dans la barre des tâches.PAS DANS LE CACHE - + Scale factor Facteur d'échelle @@ -1811,7 +1893,7 @@ Cela met l'application en évidence dans la barre des tâches.Empreinte de l'appareil - + Session Keys Clés de session @@ -1831,17 +1913,17 @@ Cela met l'application en évidence dans la barre des tâches.CHIFFREMENT - + GENERAL GÉNÉRAL - + INTERFACE INTERFACE - + Touchscreen mode Mode écran tactile @@ -1911,7 +1993,7 @@ Cela met l'application en évidence dans la barre des tâches.Tous les types de fichiers (*) - + Open Sessions File Ouvrir fichier de sessions @@ -2007,7 +2089,7 @@ Cela met l'application en évidence dans la barre des tâches. descriptiveTime - + Yesterday Hier diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index c00dc096..e36c5eef 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Csatlakoztál ehhez a szobához. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Nem sikerült meghívni a felhasználót: %1 - + Invited user: %1 A felhasználó meg lett hívva: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Titkosított esemény (Nem találhatók kulcsok a titkosítás feloldásához) -- @@ -568,7 +568,7 @@ Minden fájl (*) - + Failed to upload media. Please try again. Nem sikerült feltölteni a médiafájlt. Kérlek, próbáld újra! @@ -801,12 +801,12 @@ Példa: https://szerver.em:8787 Írj egy üzenetet… - + Emoji Hangulatjelek - + Send Küldés @@ -1003,15 +1003,20 @@ Példa: https://szerver.em:8787 ReplyPopup - + Close Bezárás + + + Cancel edit + + RoomInfo - + no version stored nincs tárolva verzió @@ -1153,7 +1158,7 @@ Példa: https://szerver.em:8787 TimelineModel - + Message redaction failed: %1 Az üzenet visszavonása nem sikerült: %1 @@ -1302,12 +1307,12 @@ Példa: https://szerver.em:8787 %1 visszavonta a kopogását. - + You joined this room. Csatlakoztál ehhez a szobához. - + Rejected the knock from %1. Kopogás elutasítva tőle: %1. @@ -1331,7 +1336,17 @@ Példa: https://szerver.em:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Reakció @@ -1349,7 +1364,7 @@ Példa: https://szerver.em:8787 TimelineView - + React Reakció @@ -1358,6 +1373,11 @@ Példa: https://szerver.em:8787 Reply Válasz + + + Edit + + Read receipts @@ -1482,7 +1502,12 @@ Példa: https://szerver.em:8787 Állapot: - + + User Profile Settings + + + + Set presence automatically Elérhetőség megadása automatikusan @@ -1505,7 +1530,7 @@ Példa: https://szerver.em:8787 UserProfile - + Verify Hitelesítés @@ -1524,11 +1549,40 @@ Példa: https://szerver.em:8787 Kick the user A felhasználó kirúgása + + + Select an avatar + + + + + All Files (*) + Minden fájl (*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Kicsinyítés a tálcára @@ -1548,12 +1602,17 @@ Példa: https://szerver.em:8787 Kerekített profilképek - + profile: %1 profil: %1 - + + Default + + + + CALLS HÍVÁSOK @@ -1608,6 +1667,29 @@ Csak a titkosított csevegések üzeneteire vonatkozik. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Gombok mutatása az idővonalban @@ -1724,7 +1806,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Kulcsok megosztása hitelesített felhasználókkal és eszközökkel - + CACHED GYORSÍTÓTÁRAZVA @@ -1734,7 +1816,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő NINCS GYORSÍTÓTÁRAZVA - + Scale factor Nagyítási tényező @@ -1809,7 +1891,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Eszközujjlenyomat - + Session Keys Munkamenetkulcsok @@ -1829,17 +1911,17 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő TITKOSÍTÁS - + GENERAL ÁLTALÁNOS - + INTERFACE FELÜLET - + Touchscreen mode Érintő képernyős mód @@ -1909,7 +1991,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Minden fájl (*) - + Open Sessions File Munkameneti fájl megnyitása @@ -2005,7 +2087,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő descriptiveTime - + Yesterday Tegnap diff --git a/resources/langs/nheko_it.ts b/resources/langs/nheko_it.ts index e05cea70..2c01a1bb 100644 --- a/resources/langs/nheko_it.ts +++ b/resources/langs/nheko_it.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Sei entrato in questa stanza. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Impossibile invitare l'utente: %1 - + Invited user: %1 Invitato utente: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Evento Criptato (Chiavi per la decriptazione non trovate) -- @@ -568,7 +568,7 @@ Tutti i File (*) - + Failed to upload media. Please try again. Impossibile inviare il file multimediale. Per favore riprova. @@ -801,12 +801,12 @@ Esempio: https://server.mio:8787 Scrivi un messaggio… - + Emoji Emoji - + Send @@ -1003,15 +1003,20 @@ Esempio: https://server.mio:8787 ReplyPopup - + Close Chiudi + + + Cancel edit + + RoomInfo - + no version stored nessuna versione memorizzata @@ -1153,7 +1158,7 @@ Esempio: https://server.mio:8787 TimelineModel - + Message redaction failed: %1 Oscuramento del messaggio fallito: %1 @@ -1303,12 +1308,12 @@ Esempio: https://server.mio:8787 %1 ha oscurato la sua bussata. - + You joined this room. Sei entrato in questa stanza. - + Rejected the knock from %1. Rifiutata la bussata di %1. @@ -1332,7 +1337,17 @@ Esempio: https://server.mio:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1350,7 +1365,7 @@ Esempio: https://server.mio:8787 TimelineView - + React @@ -1359,6 +1374,11 @@ Esempio: https://server.mio:8787 Reply Risposta + + + Edit + + Read receipts @@ -1483,7 +1503,12 @@ Esempio: https://server.mio:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1506,7 +1531,7 @@ Esempio: https://server.mio:8787 UserProfile - + Verify @@ -1525,11 +1550,40 @@ Esempio: https://server.mio:8787 Kick the user + + + Select an avatar + Scegli un avatar + + + + All Files (*) + Tutti i File (*) + + + + The selected file is not an image + Il file selezionato non è un'immagine + + + + Error while reading file: %1 + Errore durante la lettura del file: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Minimizza nella tray @@ -1549,12 +1603,17 @@ Esempio: https://server.mio:8787 Avatar Circolari - + profile: %1 - + + Default + + + + CALLS @@ -1607,6 +1666,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Mostra pulsanti nella timeline @@ -1717,7 +1799,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1727,7 +1809,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Fattore di scala @@ -1802,7 +1884,7 @@ This usually causes the application icon in the task bar to animate in some fash Impronta digitale del dispositivo - + Session Keys Chiavi di Sessione @@ -1822,17 +1904,17 @@ This usually causes the application icon in the task bar to animate in some fash CRITTOGRAFIA - + GENERAL GENERALE - + INTERFACE INTERFACCIA - + Touchscreen mode @@ -1902,7 +1984,7 @@ This usually causes the application icon in the task bar to animate in some fash Tutti i File (*) - + Open Sessions File Apri File delle Sessioni @@ -1998,7 +2080,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Ieri diff --git a/resources/langs/nheko_ja.ts b/resources/langs/nheko_ja.ts index 47cb27b8..18e18c20 100644 --- a/resources/langs/nheko_ja.ts +++ b/resources/langs/nheko_ja.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 ユーザーを招待できませんでした: %1 - + Invited user: %1 招待されたユーザー: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- 暗号化イベント (復号鍵が見つかりません) -- @@ -568,7 +568,7 @@ 全てのファイル (*) - + Failed to upload media. Please try again. メディアをアップロードできませんでした。やり直して下さい。 @@ -797,12 +797,12 @@ Example: https://server.my:8787 メッセージを書く... - + Emoji 絵文字 - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close 閉じる + + + Cancel edit + + RoomInfo - + no version stored バージョンが保存されていません @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 メッセージを編集できませんでした: %1 @@ -1298,12 +1303,12 @@ Example: https://server.my:8787 %1がノックを編集しました。 - + You joined this room. - + Rejected the knock from %1. %1からのノックを拒否しました。 @@ -1327,7 +1332,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1345,7 +1360,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1354,6 +1369,11 @@ Example: https://server.my:8787 Reply 返信 + + + Edit + + Read receipts @@ -1478,7 +1498,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1501,7 +1526,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1520,11 +1545,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + アバターを選択 + + + + All Files (*) + 全てのファイル (*) + + + + The selected file is not an image + 選択したファイルは画像ではありません + + + + Error while reading file: %1 + ファイルの読み込み時にエラーが発生しました: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray トレイへ最小化 @@ -1544,12 +1598,17 @@ Example: https://server.my:8787 円形アバター - + profile: %1 - + + Default + + + + CALLS @@ -1602,6 +1661,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1712,7 +1794,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1722,7 +1804,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor 尺度係数 @@ -1797,7 +1879,7 @@ This usually causes the application icon in the task bar to animate in some fash デバイスの指紋 - + Session Keys セッション鍵 @@ -1817,17 +1899,17 @@ This usually causes the application icon in the task bar to animate in some fash 暗号化 - + GENERAL 全般 - + INTERFACE - + Touchscreen mode @@ -1897,7 +1979,7 @@ This usually causes the application icon in the task bar to animate in some fash 全てのファイル (*) - + Open Sessions File セッションファイルを開く @@ -1993,7 +2075,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday 昨日 diff --git a/resources/langs/nheko_ml.ts b/resources/langs/nheko_ml.ts index fcceacab..9905ff35 100644 --- a/resources/langs/nheko_ml.ts +++ b/resources/langs/nheko_ml.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. നിങ്ങൾ ഈ മുറിയിൽ ചേർന്നു. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 ഉപയോക്താവിനെ ക്ഷണിക്കുന്നതിൽ പരാജയപ്പെട്ടു: %1 - + Invited user: %1 ക്ഷണിച്ച ഉപയോക്താവ്:% 1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 ഒരു സന്ദേശം എഴുതുക…. - + Emoji ഇമോജി - + Send അയക്കുക @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close അടയ്‌ക്കുക + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. നിങ്ങൾ ഈ മുറിയിൽ ചേർന്നു. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index d4f6e5f0..520f0f17 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Je bent lid geworden van deze kamer. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Gebruiker uitnodigen mislukt: %1 - + Invited user: %1 Gebruiker uitgenodigd: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ Alle bestanden (*) - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 Typ een bericht... - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. Je bent lid geworden van deze kamer. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + Alle bestanden (*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Minimaliseren naar systeemvak @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL ALGEMEEN - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash Alle bestanden (*) - + Open Sessions File @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index b0f83014..53468903 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Dołączyłeś(-łaś) do tego pokoju. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Nie udało się zaprosić użytkownika: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Zdarzenie szyfrowania (Nie znaleziono kluczy deszyfrujących) @@ -568,7 +568,7 @@ Wszystkie pliki (*) - + Failed to upload media. Please try again. @@ -799,12 +799,12 @@ Example: https://server.my:8787 Napisz wiadomość… - + Emoji Emoji - + Send @@ -1001,15 +1001,20 @@ Example: https://server.my:8787 ReplyPopup - + Close Zamknij + + + Cancel edit + + RoomInfo - + no version stored @@ -1151,7 +1156,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Redagowanie wiadomości nie powiodło się: %1 @@ -1302,12 +1307,12 @@ Example: https://server.my:8787 - + You joined this room. Dołączyłeś(-łaś) do tego pokoju. - + Rejected the knock from %1. @@ -1331,7 +1336,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1349,7 +1364,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1358,6 +1373,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1482,7 +1502,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1505,7 +1530,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1524,11 +1549,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + Wybierz awatar + + + + All Files (*) + Wszystkie pliki (*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Zminimalizuj do paska zadań @@ -1548,12 +1602,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1606,6 +1665,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1716,7 +1798,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1726,7 +1808,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1801,7 +1883,7 @@ This usually causes the application icon in the task bar to animate in some fash Odcisk palca urządzenia - + Session Keys @@ -1821,17 +1903,17 @@ This usually causes the application icon in the task bar to animate in some fash SZYFROWANIE - + GENERAL OGÓLNE - + INTERFACE - + Touchscreen mode @@ -1901,7 +1983,7 @@ This usually causes the application icon in the task bar to animate in some fash Wszystkie pliki (*) - + Open Sessions File @@ -1997,7 +2079,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts index 31be9ea9..caa970b5 100644 --- a/resources/langs/nheko_pt_PT.ts +++ b/resources/langs/nheko_pt_PT.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_ro.ts b/resources/langs/nheko_ro.ts index 7b36ea59..662377f5 100644 --- a/resources/langs/nheko_ro.ts +++ b/resources/langs/nheko_ro.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Te-ai alăturat camerei. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Nu s-a putut invita utilizatorul: %1 - + Invited user: %1 Utilizator invitat: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ Toate fișierele (*) - + Failed to upload media. Please try again. @@ -801,12 +801,12 @@ Exemplu: https://serverul.meu:8787 - + Emoji - + Send @@ -1003,15 +1003,20 @@ Exemplu: https://serverul.meu:8787 ReplyPopup - + Close Închide + + + Cancel edit + + RoomInfo - + no version stored nicio versiune stocată @@ -1153,7 +1158,7 @@ Exemplu: https://serverul.meu:8787 TimelineModel - + Message redaction failed: %1 Redactare mesaj eșuată: %1 @@ -1304,12 +1309,12 @@ Exemplu: https://serverul.meu:8787 %1 și-a redactat ciocănitul. - + You joined this room. Te-ai alăturat camerei. - + Rejected the knock from %1. Ciocănit refuzat de la %1. @@ -1333,7 +1338,17 @@ Exemplu: https://serverul.meu:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1351,7 +1366,7 @@ Exemplu: https://serverul.meu:8787 TimelineView - + React @@ -1360,6 +1375,11 @@ Exemplu: https://serverul.meu:8787 Reply Răspuns + + + Edit + + Read receipts @@ -1484,7 +1504,12 @@ Exemplu: https://serverul.meu:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1507,7 +1532,7 @@ Exemplu: https://serverul.meu:8787 UserProfile - + Verify @@ -1526,11 +1551,40 @@ Exemplu: https://serverul.meu:8787 Kick the user + + + Select an avatar + Selectează un avatar + + + + All Files (*) + Toate fișierele (*) + + + + The selected file is not an image + Fișierul selectat nu este imagine + + + + Error while reading file: %1 + Eroare întâmpinată la citirea fișierului: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Minimizează în bara de notificări @@ -1550,12 +1604,17 @@ Exemplu: https://serverul.meu:8787 Avatare rotunde - + profile: %1 - + + Default + + + + CALLS @@ -1608,6 +1667,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Arată butoanele în cronologie @@ -1718,7 +1800,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1728,7 +1810,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Factor de dimensiune @@ -1803,7 +1885,7 @@ This usually causes the application icon in the task bar to animate in some fash Amprentă Dispozitiv - + Session Keys Chei de sesiune @@ -1823,17 +1905,17 @@ This usually causes the application icon in the task bar to animate in some fash CRIPTARE - + GENERAL GENERAL - + INTERFACE INTERFAȚĂ - + Touchscreen mode @@ -1903,7 +1985,7 @@ This usually causes the application icon in the task bar to animate in some fash Toate fișierele (*) - + Open Sessions File Deschide fișierul de sesiuni @@ -1999,7 +2081,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Ieri diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 21b31b33..112e8c8c 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Вы присоединились к этой комнате. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Не удалось пригласить пользователя: %1 - + Invited user: %1 Приглашенный пользователь: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Зашифрованное событие (Нет найдено ключей для дешифрования) -- @@ -568,7 +568,7 @@ Все файлы (*) - + Failed to upload media. Please try again. Не удалось загрузить медиа. Пожалуйста попробуйте ещё раз @@ -801,12 +801,12 @@ Example: https://server.my:8787 Написать сообщение… - + Emoji Эмоджи - + Send Отправить @@ -1003,15 +1003,20 @@ Example: https://server.my:8787 ReplyPopup - + Close Закрыть + + + Cancel edit + + RoomInfo - + no version stored нет сохраненной версии @@ -1153,7 +1158,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Ошибка редактирования сообщения: %1 @@ -1304,12 +1309,12 @@ Example: https://server.my:8787 %1 отредактировал его "стук". - + You joined this room. Вы присоединились к этой комнате. - + Rejected the knock from %1. Отверг "стук" от %1 @@ -1333,7 +1338,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Отреагировать @@ -1351,7 +1366,7 @@ Example: https://server.my:8787 TimelineView - + React Отреагировать @@ -1360,6 +1375,11 @@ Example: https://server.my:8787 Reply Ответить + + + Edit + + Read receipts @@ -1484,7 +1504,12 @@ Example: https://server.my:8787 Статус: - + + User Profile Settings + + + + Set presence automatically Установить 'presence'автоматически @@ -1507,7 +1532,7 @@ Example: https://server.my:8787 UserProfile - + Verify Верифицировать @@ -1526,11 +1551,40 @@ Example: https://server.my:8787 Kick the user Выгнать пользователя + + + Select an avatar + Выберите аватар + + + + All Files (*) + Все файлы (*) + + + + The selected file is not an image + Выбранный файл не является картинкой + + + + Error while reading file: %1 + Ошибка во время прочтения файла: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Сворачивать в системную панель @@ -1550,12 +1604,17 @@ Example: https://server.my:8787 Округлый Аватар - + profile: %1 профиль: %1 - + + Default + + + + CALLS ЗВОНКИ @@ -1608,6 +1667,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Отображать кнопки в таймлайне @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash Делиться ключами с проверенными участниками и устройствами - + CACHED @@ -1733,7 +1815,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor Масштаб @@ -1808,7 +1890,7 @@ This usually causes the application icon in the task bar to animate in some fash Отпечаток устройства - + Session Keys Ключи сеанса @@ -1828,17 +1910,17 @@ This usually causes the application icon in the task bar to animate in some fash ШИФРОВАНИЕ - + GENERAL ГЛАВНОЕ - + INTERFACE ИНТЕРФЕЙС - + Touchscreen mode Сенсорный режим @@ -1908,7 +1990,7 @@ This usually causes the application icon in the task bar to animate in some fash Все файлы (*) - + Open Sessions File Открыть файл сеансов @@ -2005,7 +2087,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday Вчера diff --git a/resources/langs/nheko_si.ts b/resources/langs/nheko_si.ts index 6366b35a..d665081c 100644 --- a/resources/langs/nheko_si.ts +++ b/resources/langs/nheko_si.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 - + Invited user: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 @@ -1299,12 +1304,12 @@ Example: https://server.my:8787 - + You joined this room. - + Rejected the knock from %1. @@ -1328,7 +1333,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1346,7 +1361,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1355,6 +1370,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1479,7 +1499,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1502,7 +1527,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1521,11 +1546,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + + + + + All Files (*) + + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray @@ -1545,12 +1599,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1603,6 +1662,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1713,7 +1795,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1723,7 +1805,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1798,7 +1880,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Session Keys @@ -1818,17 +1900,17 @@ This usually causes the application icon in the task bar to animate in some fash - + GENERAL - + INTERFACE - + Touchscreen mode @@ -1898,7 +1980,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Open Sessions File @@ -1994,7 +2076,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday diff --git a/resources/langs/nheko_sv.ts b/resources/langs/nheko_sv.ts index 18266ad4..49a825aa 100644 --- a/resources/langs/nheko_sv.ts +++ b/resources/langs/nheko_sv.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. Du gick med i detta rum. @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 Kunde inte bjuda in användare: %1 - + Invited user: %1 Bjöd in användare: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. -- Krypterat Event (Inga nycklar kunde hittas för dekryptering) -- @@ -568,7 +568,7 @@ Alla Filer (*) - + Failed to upload media. Please try again. Kunde inte ladda upp media. Vänligen försök igen. @@ -801,12 +801,12 @@ Exempel: https://server.my:8787 Skriv ett meddelande… - + Emoji Emoji - + Send Skicka @@ -1003,15 +1003,20 @@ Exempel: https://server.my:8787 ReplyPopup - + Close Stäng + + + Cancel edit + + RoomInfo - + no version stored ingen version lagrad @@ -1153,7 +1158,7 @@ Exempel: https://server.my:8787 TimelineModel - + Message redaction failed: %1 Kunde inte maskera meddelande: %1 @@ -1303,12 +1308,12 @@ Exempel: https://server.my:8787 %1 maskerade sin knackning. - + You joined this room. Du gick med i detta rum. - + Rejected the knock from %1. Avvisade knackningen från %1. @@ -1332,7 +1337,17 @@ Exempel: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React Reagera @@ -1350,7 +1365,7 @@ Exempel: https://server.my:8787 TimelineView - + React Reagera @@ -1359,6 +1374,11 @@ Exempel: https://server.my:8787 Reply Svara + + + Edit + + Read receipts @@ -1483,7 +1503,12 @@ Exempel: https://server.my:8787 Status: - + + User Profile Settings + + + + Set presence automatically Sätt närvaro automatiskt @@ -1506,7 +1531,7 @@ Exempel: https://server.my:8787 UserProfile - + Verify Bekräfta @@ -1525,11 +1550,40 @@ Exempel: https://server.my:8787 Kick the user Sparka ut användaren + + + Select an avatar + Välj en avatar + + + + All Files (*) + Alla Filer (*) + + + + The selected file is not an image + Den valda filen är inte en bild + + + + Error while reading file: %1 + Kunde inte läsa filen: %1 + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray Minimera till systemtråg @@ -1549,12 +1603,17 @@ Exempel: https://server.my:8787 Cirkulära avatarer - + profile: %1 profil: %1 - + + Default + + + + CALLS SAMTAL @@ -1609,6 +1668,29 @@ Påverkar endast krypterade chattar. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline Visa knappar i tidslinje @@ -1725,7 +1807,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< Dela nycklar med verifierade användare och enheter - + CACHED SPARAD @@ -1735,7 +1817,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< EJ SPARAD - + Scale factor Storleksfaktor @@ -1810,7 +1892,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< Enhetsfingeravtryck - + Session Keys Sessionsnycklar @@ -1830,17 +1912,17 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< KRYPTERING - + GENERAL ALLMÄNT - + INTERFACE GRÄNSSNITT - + Touchscreen mode Touchskärmsläge @@ -1910,7 +1992,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< Alla Filer (*) - + Open Sessions File Öppna sessionsfil @@ -2006,7 +2088,7 @@ Detta gör vanligtvis att ikonen i aktivitetsfältet animeras på något sätt.< descriptiveTime - + Yesterday Igår diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index ce25bcbe..b371ce70 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -51,7 +51,7 @@ Cache - + You joined this room. 您已加入此房间 @@ -120,13 +120,13 @@ ChatPage - + Failed to invite user: %1 邀请用户失败: %1 - + Invited user: %1 邀请已发送: %1 @@ -479,7 +479,7 @@ EventStore - + -- Encrypted Event (No keys found for decryption) -- Placeholder, when the message was not decrypted yet or can't be decrypted. @@ -568,7 +568,7 @@ 所有文件(*) - + Failed to upload media. Please try again. @@ -797,12 +797,12 @@ Example: https://server.my:8787 写一条消息… - + Emoji - + Send @@ -999,15 +999,20 @@ Example: https://server.my:8787 ReplyPopup - + Close + + + Cancel edit + + RoomInfo - + no version stored @@ -1149,7 +1154,7 @@ Example: https://server.my:8787 TimelineModel - + Message redaction failed: %1 删除消息失败:%1 @@ -1298,12 +1303,12 @@ Example: https://server.my:8787 - + You joined this room. 您已加入此房间 - + Rejected the knock from %1. @@ -1327,7 +1332,17 @@ Example: https://server.my:8787 TimelineRow - + + Edit + + + + + Edited + + + + React @@ -1345,7 +1360,7 @@ Example: https://server.my:8787 TimelineView - + React @@ -1354,6 +1369,11 @@ Example: https://server.my:8787 Reply + + + Edit + + Read receipts @@ -1478,7 +1498,12 @@ Example: https://server.my:8787 - + + User Profile Settings + + + + Set presence automatically @@ -1501,7 +1526,7 @@ Example: https://server.my:8787 UserProfile - + Verify @@ -1520,11 +1545,40 @@ Example: https://server.my:8787 Kick the user + + + Select an avatar + 选择一个头像 + + + + All Files (*) + 所有文件(*) + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + UserSettings + + + + Default + + UserSettingsPage - + Minimize to tray 最小化至托盘 @@ -1544,12 +1598,17 @@ Example: https://server.my:8787 - + profile: %1 - + + Default + + + + CALLS @@ -1602,6 +1661,29 @@ Only affects messages in encrypted chats. + Privacy Screen + + + + + When the window loses focus, the timeline will +be blurred. + + + + + Privacy screen timeout (in seconds [0 - 3600]) + + + + + Set timeout (in seconds) for how long after window loses +focus before the screen will be blurred. +Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) + + + + Show buttons in timeline @@ -1712,7 +1794,7 @@ This usually causes the application icon in the task bar to animate in some fash - + CACHED @@ -1722,7 +1804,7 @@ This usually causes the application icon in the task bar to animate in some fash - + Scale factor @@ -1797,7 +1879,7 @@ This usually causes the application icon in the task bar to animate in some fash 设备指纹 - + Session Keys 会话密钥 @@ -1817,17 +1899,17 @@ This usually causes the application icon in the task bar to animate in some fash 加密 - + GENERAL 通用 - + INTERFACE - + Touchscreen mode @@ -1897,7 +1979,7 @@ This usually causes the application icon in the task bar to animate in some fash 所有文件(*) - + Open Sessions File 打开会话文件 @@ -1993,7 +2075,7 @@ This usually causes the application icon in the task bar to animate in some fash descriptiveTime - + Yesterday From 2a5e20dc6ff331b42d5dce99ee45ce19c5b60b56 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 18:00:52 +0100 Subject: [PATCH 36/50] Fix cancel edit order on Escape key --- resources/qml/MessageView.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 09dc4e36..13b4c82c 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -50,10 +50,10 @@ ListView { Shortcut { sequence: StandardKey.Cancel onActivated: { - if (chat.model.edit) - chat.model.edit = undefined; - else + if (chat.model.reply) chat.model.reply = undefined; + else + chat.model.edit = undefined; } } From 0db4d71ec2483c7ac5a7b536737fee8fc53a76d7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 18:07:55 +0100 Subject: [PATCH 37/50] Prevent edits of unsent messages --- src/timeline/TimelineModel.cpp | 3 +++ src/timeline/TimelineModel.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 493f755b..b6ebeb84 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1536,6 +1536,9 @@ TimelineModel::formatMemberEvent(QString id) void TimelineModel::setEdit(QString newEdit) { + if (edit_.startsWith('m')) + return; + if (edit_ != newEdit) { auto ev = events.get(newEdit.toStdString(), ""); if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 017b6589..83012cd8 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -260,6 +260,9 @@ public slots: QString reply() const { return reply_; } void setReply(QString newReply) { + if (edit_.startsWith('m')) + return; + if (reply_ != newReply) { reply_ = newReply; emit replyChanged(reply_); From 6ff8db1799ce2c974eea2d82f25df7826c658388 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 10 Feb 2021 16:37:08 -0500 Subject: [PATCH 38/50] Translated using Weblate (Estonian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (457 of 457 strings) Co-authored-by: Priit Jõerüüt Translate-URL: https://weblate.nheko.im/projects/nheko/nheko-master/et/ Translation: Nheko/nheko --- resources/langs/nheko_et.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/resources/langs/nheko_et.ts b/resources/langs/nheko_et.ts index 1c7f31b2..7b73e0e5 100644 --- a/resources/langs/nheko_et.ts +++ b/resources/langs/nheko_et.ts @@ -1010,7 +1010,7 @@ Näiteks: https://server.minu:8787 Cancel edit - + Tühista muudatused @@ -1339,12 +1339,12 @@ Näiteks: https://server.minu:8787 Edit - + Muuda Edited - + Muudetud @@ -1377,7 +1377,7 @@ Näiteks: https://server.minu:8787 Edit - + Muuda @@ -1505,7 +1505,7 @@ Näiteks: https://server.minu:8787 User Profile Settings - + Kasutajaprofiili seadistused @@ -1553,22 +1553,22 @@ Näiteks: https://server.minu:8787 Select an avatar - Vali tunnuspilt + Vali tunnuspilt All Files (*) - Kõik failid (*) + Kõik failid (*) The selected file is not an image - Valitud fail ei ole pildifail + Valitud fail ei ole pildifail Error while reading file: %1 - Viga faili lugemisel: %1 + Viga faili lugemisel: %1 @@ -1577,7 +1577,7 @@ Näiteks: https://server.minu:8787 Default - + Vaikimisi @@ -1610,7 +1610,7 @@ Näiteks: https://server.minu:8787 Default - + Vaikimisi @@ -1669,25 +1669,28 @@ Kehtib vaid läbivalt krüptitud vestluste puhul. Privacy Screen - + Privaatsust tagav sirm When the window loses focus, the timeline will be blurred. - + Kui aken kaotab fookuse, +siis ajajoone vaade hägustub. Privacy screen timeout (in seconds [0 - 3600]) - + Viivitus privaatsussirmi sisselülitamisel (sekundites [0 - 3600]) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Seadista aeg sekundites, mille möödumisel +peale akna fookuse kadumist ajajoone vaade hägustub. +0 hägustab vaate koheselt. Pikim viivitus saab olla 1 tund (3600 sekundit) From cc1f9a079b899ac2200bbbfbe6d716fc08241334 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 11 Feb 2021 14:21:16 -0500 Subject: [PATCH 39/50] Translated using Weblate (Finnish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 43.9% (201 of 457 strings) Co-authored-by: Priit Jõerüüt Translate-URL: https://weblate.nheko.im/projects/nheko/nheko-master/fi/ Translation: Nheko/nheko --- resources/langs/nheko_fi.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 001baed6..4dbe14ca 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -6,13 +6,13 @@ Calling... - + Soitetaan... Connecting... - + Yhdistetään... @@ -89,7 +89,7 @@ Devices - Laitteet + Laitteet @@ -560,7 +560,7 @@ Select a file - Valitse tiedosto + Valitse tiedosto @@ -789,7 +789,7 @@ Example: https://server.my:8787 Send a file - Lähetä tiedosto + Lähetä tiedosto @@ -799,7 +799,7 @@ Example: https://server.my:8787 Emoji - Emoji + Emoji @@ -1549,22 +1549,22 @@ Example: https://server.my:8787 Select an avatar - Valitse profiilikuva + Valitse profiilikuva All Files (*) - Kaikki Tiedostot (*) + Kaikki Tiedostot (*) The selected file is not an image - Valittu tiedosto ei ole kuva + Valittu tiedosto ei ole kuva Error while reading file: %1 - Virhe lukiessa tiedostoa: %1 + Virhe lukiessa tiedostoa: %1 @@ -1972,7 +1972,7 @@ This usually causes the application icon in the task bar to animate in some fash Select a file - Valitse tiedosto + Valitse tiedosto From 192c3b7a77041e92c7d1a1b221101bd7994695ef Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 11 Feb 2021 15:44:02 -0500 Subject: [PATCH 40/50] Translated using Weblate (Hungarian) Currently translated at 94.5% (432 of 457 strings) Co-authored-by: maxigaz Translate-URL: https://weblate.nheko.im/projects/nheko/nheko-master/hu/ Translation: Nheko/nheko --- resources/langs/nheko_hu.ts | 59 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/resources/langs/nheko_hu.ts b/resources/langs/nheko_hu.ts index e36c5eef..fc9eaef1 100644 --- a/resources/langs/nheko_hu.ts +++ b/resources/langs/nheko_hu.ts @@ -1010,7 +1010,7 @@ Példa: https://szerver.em:8787 Cancel edit - + Szerkesztés megszakítása @@ -1338,12 +1338,12 @@ Példa: https://szerver.em:8787 Edit - + Szerkesztés Edited - + Szerkesztve @@ -1376,7 +1376,7 @@ Példa: https://szerver.em:8787 Edit - + Szerkesztés @@ -1504,7 +1504,7 @@ Példa: https://szerver.em:8787 User Profile Settings - + Felhasználói profil beállításai @@ -1552,22 +1552,22 @@ Példa: https://szerver.em:8787 Select an avatar - + Profilkép kiválasztása All Files (*) - Minden fájl (*) + Minden fájl (*) The selected file is not an image - + A kiválasztott fájl nem egy kép Error while reading file: %1 - + Hiba a fájl olvasása közben: %1 @@ -1576,7 +1576,7 @@ Példa: https://szerver.em:8787 Default - + Alapértelmezett @@ -1609,7 +1609,7 @@ Példa: https://szerver.em:8787 Default - + Alapértelmezett @@ -1668,25 +1668,29 @@ Csak a titkosított csevegések üzeneteire vonatkozik. Privacy Screen - + Idővonal automatikus kitakarása When the window loses focus, the timeline will be blurred. - + Amikor az ablak elveszíti a fókuszt, +az idővonal homályosítva lesz. Privacy screen timeout (in seconds [0 - 3600]) - + Idővonal kitakarása ennyi idő után (másodpercben, 0 és 3600 között) Set timeout (in seconds) for how long after window loses focus before the screen will be blurred. Set to 0 to blur immediately after focus loss. Max value of 1 hour (3600 seconds) - + Add meg, hány másodperc elteltével homályosuljon el az idővonal, +miután az ablak elveszíti a fókuszt! +Ha azonnal el legyen homályosítva fókuszvesztéskor, állítsd +0-ra! A maximális érték 1 óra (3600 másodperc). @@ -2140,7 +2144,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Open Fallback in Browser - + Fallback megnyitása böngészőben @@ -2155,7 +2159,7 @@ Ettől általában animálttá válik az alkalmazásablakok listáján szereplő Open the fallback, follow the steps and confirm after completing them. - + Nyisd meg a fallback-ket, kövesd az utasításokat, és erősítsd meg, ha végeztél velük! @@ -2358,34 +2362,35 @@ Média mérete: %2 End-to-End Encryption - + Végponttól végpontig titkosítás Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards. - + A titkosítás jelenleg kísérleti stádiumú és váratlan furcsaságok történhetnek. <br# Kérlek, vedd vigyelembe, hogy ha egyszer aktiváltad, nem lehet utána kikapcsolni. Respond to key requests - + Válasz kulcskérelmekre %n member(s) - - + + %n tag + %n tag Failed to enable encryption: %1 - + Nem sikerült a titkosítás aktiválása: %1 Select an avatar - + Profilkép kiválasztása @@ -2395,18 +2400,18 @@ Média mérete: %2 The selected file is not an image - + A kiválasztott fájl nem egy kép Error while reading file: %1 - + Hiba a fájl olvasása közben: %1 Failed to upload image: %s - + Nem sikerült a kép feltöltése: %s From 3c91b5b47befb588a7c8005745a1e8dadf47df03 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 12 Feb 2021 16:10:48 +0100 Subject: [PATCH 41/50] Fix crash when editing an edited message pointing to itself --- src/timeline/TimelineModel.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b6ebeb84..0f35a290 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -1542,13 +1542,11 @@ TimelineModel::setEdit(QString newEdit) if (edit_ != newEdit) { auto ev = events.get(newEdit.toStdString(), ""); if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) { - edit_ = newEdit; - emit editChanged(edit_); - + auto e = *ev; setReply(QString::fromStdString( - mtx::accessors::relations(*ev).reply_to().value_or(""))); + mtx::accessors::relations(e).reply_to().value_or(""))); - auto msgType = mtx::accessors::msg_type(*ev); + auto msgType = mtx::accessors::msg_type(e); if (msgType == mtx::events::MessageType::Text || msgType == mtx::events::MessageType::Notice) { input()->setText(relatedInfo(newEdit).quoted_body); @@ -1557,11 +1555,15 @@ TimelineModel::setEdit(QString newEdit) } else { input()->setText(""); } + + edit_ = newEdit; } else { - edit_ = ""; - emit editChanged(edit_); + resetReply(); + input()->setText(""); + edit_ = ""; } + emit editChanged(edit_); } } From e2fc676c77692a4edc56f0f8a266bfabfdfadd9d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 12 Feb 2021 18:22:41 +0100 Subject: [PATCH 42/50] Revert keeping whitespace in html, it breaks lists --- resources/qml/delegates/TextMessage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 3ff771dc..2c449ca2 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -4,7 +4,7 @@ import im.nheko 1.0 MatrixText { property string formatted: model.data.formattedBody - text: "" + formatted.replace("
", "
") + ""
+    text: "" + formatted.replace("
", "
")
     width: parent ? parent.width : undefined
     height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
     clip: isReply

From 7ddcab3902a6b39c3ed8328c245f58a495b4c43f Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 13 Feb 2021 01:41:09 +0100
Subject: [PATCH 43/50] Mark messages as read, when Nheko gets focused

fixes #235
---
 resources/qml/MessageView.qml  | 15 ++++++++++++++-
 src/timeline/TimelineModel.cpp |  6 +++---
 2 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 13b4c82c..35b5cac4 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -77,6 +77,19 @@ ListView {
         }
     }
 
+    Connections {
+        target: TimelineManager
+        onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
+    }
+
+    Timer {
+        id: readTimer
+
+        // force current read index to update
+        onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
+        interval: 1000
+    }
+
     Component {
         id: sectionHeader
 
@@ -193,7 +206,7 @@ ListView {
         Connections {
             target: chat
             onMovementEnded: {
-                if (y + height + 2 * chat.spacing > chat.contentY + timelineRoot.height && y < chat.contentY + timelineRoot.height)
+                if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
                     chat.model.currentIndex = index;
 
             }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 0f35a290..5c904932 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -735,14 +735,14 @@ TimelineModel::updateLastMessage()
 void
 TimelineModel::setCurrentIndex(int index)
 {
-        if (!ChatPage::instance()->isActiveWindow())
-                return;
-
         auto oldIndex = idToIndex(currentId);
         currentId     = indexToId(index);
         if (index != oldIndex)
                 emit currentIndexChanged(index);
 
+        if (!ChatPage::instance()->isActiveWindow())
+                return;
+
         if (!currentId.startsWith("m")) {
                 auto oldReadIndex =
                   cache::getEventIndex(roomId().toStdString(), currentReadId.toStdString());

From 299c486a2bd1aff872fcf0b2e76300b569920fc5 Mon Sep 17 00:00:00 2001
From: Loren Burkholder 
Date: Fri, 12 Feb 2021 11:28:41 -0500
Subject: [PATCH 44/50] Display notifications for emote messages properly

---
 src/ChatPage.cpp                   |  9 ++++++++-
 src/notifications/Manager.h        |  3 ++-
 src/notifications/ManagerLinux.cpp | 18 ++++++++++++------
 src/notifications/ManagerMac.mm    |  8 ++++++--
 src/notifications/ManagerWin.cpp   | 11 +++++++++--
 5 files changed, 37 insertions(+), 12 deletions(-)

diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 6d67e6f2..656ddab0 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -691,13 +691,20 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
                                           this,
                                           [this, room_id, event_id, item, user_id, info](
                                             QPixmap image) {
+                                                  bool isEmote = false;
+                                                  auto ev      = cache::client()->getEvent(
+                                                    room_id.toStdString(), event_id);
+                                                  if (ev && mtx::accessors::msg_type(ev->data) ==
+                                                              mtx::events::MessageType::Emote)
+                                                          isEmote = true;
                                                   notificationsManager.postNotification(
                                                     room_id,
                                                     QString::fromStdString(event_id),
                                                     QString::fromStdString(info.name),
                                                     cache::displayName(room_id, user_id),
                                                     utils::event_body(item.event),
-                                                    image.toImage());
+                                                    image.toImage(),
+                                                    isEmote);
                                           });
                                 }
                         }
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index 2b869efc..46f398d7 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -32,7 +32,8 @@ public:
                               const QString &roomName,
                               const QString &senderName,
                               const QString &text,
-                              const QImage &icon);
+                              const QImage &icon,
+                              const bool &isEmoteMsg = false);
 
 signals:
         void notificationClicked(const QString roomId, const QString eventId);
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 8f7261e6..7dbf663d 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -50,17 +50,23 @@ NotificationsManager::postNotification(const QString &roomid,
                                        const QString &roomname,
                                        const QString &sender,
                                        const QString &text,
-                                       const QImage &icon)
+                                       const QImage &icon,
+                                       const bool &isEmoteMessage)
 {
         QVariantMap hints;
         hints["image-data"] = icon;
         hints["sound-name"] = "message-new-instant";
         QList argumentList;
-        argumentList << "nheko";              // app_name
-        argumentList << (uint)0;              // replace_id
-        argumentList << "";                   // app_icon
-        argumentList << roomname;             // summary
-        argumentList << sender + ": " + text; // body
+        argumentList << "nheko";  // app_name
+        argumentList << (uint)0;  // replace_id
+        argumentList << "";       // app_icon
+        argumentList << roomname; // summary
+
+        // body
+        if (isEmoteMessage)
+                argumentList << "* " + sender + " " + text;
+        else
+                argumentList << sender + ": " + text;
         // The list of actions has always the action name and then a localized version of that
         // action. Currently we just use an empty string for that.
         // TODO(Nico): Look into what to actually put there.
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index c09e894c..e21cc904 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -19,7 +19,8 @@ NotificationsManager::postNotification(
                 const QString &roomName,
                 const QString &senderName,
                 const QString &text,
-                const QImage &icon)
+                const QImage &icon,
+                const bool &isEmoteMessage)
 {
     Q_UNUSED(roomId);
     Q_UNUSED(eventId);
@@ -29,7 +30,10 @@ NotificationsManager::postNotification(
 
     notif.title           = roomName.toNSString();
     notif.subtitle        = QString("%1 sent a message").arg(senderName).toNSString();
-    notif.informativeText = text.toNSString();
+    if (isEmoteMessage)
+            notif.informativeText = QString("* ").append(senderName).append(" ").append(text).toNSString();
+    else
+            notif.informativeText = text.toNSString();
     notif.soundName       = NSUserNotificationDefaultSoundName;
 
     [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: notif];
diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index cc61c645..43e6517b 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -37,7 +37,8 @@ NotificationsManager::postNotification(const QString &room_id,
                                        const QString &room_name,
                                        const QString &sender,
                                        const QString &text,
-                                       const QImage &icon)
+                                       const QImage &icon,
+                                       const bool &isEmoteMessage)
 {
         Q_UNUSED(room_id)
         Q_UNUSED(event_id)
@@ -53,7 +54,13 @@ NotificationsManager::postNotification(const QString &room_id,
         else
                 templ.setTextField(QString("%1").arg(sender).toStdWString(),
                                    WinToastTemplate::FirstLine);
-        templ.setTextField(QString("%1").arg(text).toStdWString(), WinToastTemplate::SecondLine);
+        if (isEmoteMessage)
+                templ.setTextField(
+                  QString("* ").append(sender).append(" ").append(text).toStdWString(),
+                  WinToastTemplate::SecondLine);
+        else
+                templ.setTextField(QString("%1").arg(text).toStdWString(),
+                                   WinToastTemplate::SecondLine);
         // TODO: implement room or user avatar
         // templ.setImagePath(L"C:/example.png");
 

From 9f9c499cb2a136aaa5e74fa20c931d8c70885351 Mon Sep 17 00:00:00 2001
From: Loren Burkholder 
Date: Sat, 13 Feb 2021 10:58:09 -0500
Subject: [PATCH 45/50] Fix typo

---
 src/notifications/ManagerWin.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index 43e6517b..1a4058b3 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -23,7 +23,7 @@ init()
         WinToast::instance()->setAppName(L"Nheko");
         WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"nheko", L"nheko"));
         if (!WinToast::instance()->initialize())
-                std::wcout << "Your system in not compatible with toast notifications\n";
+                std::wcout << "Your system is not compatible with toast notifications\n";
 }
 }
 

From 567b2d05effd32c8804e3039250e3b44d4e7c91e Mon Sep 17 00:00:00 2001
From: Loren Burkholder 
Date: Sat, 13 Feb 2021 12:10:49 -0500
Subject: [PATCH 46/50] Move notification parsing into postNotification

---
 src/ChatPage.cpp                   | 19 ++-------------
 src/notifications/Manager.h        | 10 +++-----
 src/notifications/ManagerLinux.cpp | 37 +++++++++++++++++++-----------
 src/notifications/ManagerMac.mm    | 29 ++++++++++++-----------
 src/notifications/ManagerWin.cpp   | 26 +++++++++++++--------
 5 files changed, 59 insertions(+), 62 deletions(-)

diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 656ddab0..45802789 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -668,8 +668,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
 
                         if (!cache::isNotificationSent(event_id)) {
                                 const auto room_id = QString::fromStdString(item.room_id);
-                                const auto user_id =
-                                  QString::fromStdString(mtx::accessors::sender(item.event));
 
                                 // We should only sent one notification per event.
                                 cache::markSentNotification(event_id);
@@ -689,22 +687,9 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
                                           QString::fromStdString(info.avatar_url),
                                           96,
                                           this,
-                                          [this, room_id, event_id, item, user_id, info](
-                                            QPixmap image) {
-                                                  bool isEmote = false;
-                                                  auto ev      = cache::client()->getEvent(
-                                                    room_id.toStdString(), event_id);
-                                                  if (ev && mtx::accessors::msg_type(ev->data) ==
-                                                              mtx::events::MessageType::Emote)
-                                                          isEmote = true;
+                                          [this, item](QPixmap image) {
                                                   notificationsManager.postNotification(
-                                                    room_id,
-                                                    QString::fromStdString(event_id),
-                                                    QString::fromStdString(info.name),
-                                                    cache::displayName(room_id, user_id),
-                                                    utils::event_body(item.event),
-                                                    image.toImage(),
-                                                    isEmote);
+                                                    item, image.toImage());
                                           });
                                 }
                         }
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index 46f398d7..e2f9f431 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -4,6 +4,8 @@
 #include 
 #include 
 
+#include 
+
 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
 #include 
 #include 
@@ -27,13 +29,7 @@ class NotificationsManager : public QObject
 public:
         NotificationsManager(QObject *parent = nullptr);
 
-        void postNotification(const QString &roomId,
-                              const QString &eventId,
-                              const QString &roomName,
-                              const QString &senderName,
-                              const QString &text,
-                              const QImage &icon,
-                              const bool &isEmoteMsg = false);
+        void postNotification(const mtx::responses::Notification ¬ification, const QImage &icon);
 
 signals:
         void notificationClicked(const QString roomId, const QString eventId);
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 7dbf663d..66592c99 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -8,6 +8,12 @@
 #include 
 #include 
 
+#include "Cache.h"
+#include "EventAccessors.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include 
+
 NotificationsManager::NotificationsManager(QObject *parent)
   : QObject(parent)
   , dbus("org.freedesktop.Notifications",
@@ -45,28 +51,31 @@ NotificationsManager::NotificationsManager(QObject *parent)
  * Licensed under the GNU General Public License, version 3
  */
 void
-NotificationsManager::postNotification(const QString &roomid,
-                                       const QString &eventid,
-                                       const QString &roomname,
-                                       const QString &sender,
-                                       const QString &text,
-                                       const QImage &icon,
-                                       const bool &isEmoteMessage)
+NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
+                                       const QImage &icon)
 {
+        const auto room_id  = QString::fromStdString(notification.room_id);
+        const auto event_id = QString::fromStdString(mtx::accessors::event_id(notification.event));
+        const auto sender   = cache::displayName(
+          room_id, QString::fromStdString(mtx::accessors::sender(notification.event)));
+        const auto text = utils::event_body(notification.event);
+
         QVariantMap hints;
         hints["image-data"] = icon;
         hints["sound-name"] = "message-new-instant";
         QList argumentList;
-        argumentList << "nheko";  // app_name
-        argumentList << (uint)0;  // replace_id
-        argumentList << "";       // app_icon
-        argumentList << roomname; // summary
+        argumentList << "nheko"; // app_name
+        argumentList << (uint)0; // replace_id
+        argumentList << "";      // app_icon
+        argumentList << QString::fromStdString(
+          cache::singleRoomInfo(notification.room_id).name); // summary
 
         // body
-        if (isEmoteMessage)
+        if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
                 argumentList << "* " + sender + " " + text;
         else
                 argumentList << sender + ": " + text;
+
         // The list of actions has always the action name and then a localized version of that
         // action. Currently we just use an empty string for that.
         // TODO(Nico): Look into what to actually put there.
@@ -82,12 +91,12 @@ NotificationsManager::postNotification(const QString &roomid,
         QDBusPendingCall call = notifyApp.asyncCallWithArgumentList("Notify", argumentList);
         auto watcher          = new QDBusPendingCallWatcher{call, this};
         connect(
-          watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, roomid, eventid]() {
+          watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this, room_id, event_id]() {
                   if (watcher->reply().type() == QDBusMessage::ErrorMessage)
                           qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
                   else
                           notificationIds[watcher->reply().arguments().first().toUInt()] =
-                            roomEventId{roomid, eventid};
+                            roomEventId{room_id, event_id};
                   watcher->deleteLater();
           });
 }
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index e21cc904..e50bee89 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -3,6 +3,12 @@
 #include 
 #include 
 
+#include "Cache.h"
+#include "EventAccessors.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include 
+
 @interface NSUserNotification (CFIPrivate)
 - (void)set_identityImage:(NSImage *)image;
 @end
@@ -13,25 +19,20 @@ NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
 }
 
 void
-NotificationsManager::postNotification(
-                const QString &roomId,
-                const QString &eventId,
-                const QString &roomName,
-                const QString &senderName,
-                const QString &text,
-                const QImage &icon,
-                const bool &isEmoteMessage)
+NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
+                                       const QImage &icon)
 {
-    Q_UNUSED(roomId);
-    Q_UNUSED(eventId);
     Q_UNUSED(icon);
 
+    const auto sender   = cache::displayName(QString::fromStdString(notification.room_id), QString::fromStdString(mtx::accessors::sender(notification.event)));
+    const auto text     = utils::event_body(notification.event);
+
     NSUserNotification * notif = [[NSUserNotification alloc] init];
 
-    notif.title           = roomName.toNSString();
-    notif.subtitle        = QString("%1 sent a message").arg(senderName).toNSString();
-    if (isEmoteMessage)
-            notif.informativeText = QString("* ").append(senderName).append(" ").append(text).toNSString();
+    notif.title           = QString::fromStdString(cache::singleRoomInfo(notification.room_id).name).toNSString();
+    notif.subtitle        = QString("%1 sent a message").arg(sender).toNSString();
+    if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
+            notif.informativeText = QString("* ").append(sender).append(" ").append(text).toNSString();
     else
             notif.informativeText = text.toNSString();
     notif.soundName       = NSUserNotificationDefaultSoundName;
diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index 1a4058b3..7df11308 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -1,6 +1,12 @@
 #include "notifications/Manager.h"
 #include "wintoastlib.h"
 
+#include "Cache.h"
+#include "EventAccessors.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include 
+
 using namespace WinToastLib;
 
 class CustomHandler : public IWinToastHandler
@@ -32,18 +38,18 @@ NotificationsManager::NotificationsManager(QObject *parent)
 {}
 
 void
-NotificationsManager::postNotification(const QString &room_id,
-                                       const QString &event_id,
-                                       const QString &room_name,
-                                       const QString &sender,
-                                       const QString &text,
-                                       const QImage &icon,
-                                       const bool &isEmoteMessage)
+NotificationsManager::postNotification(const mtx::responses::Notification ¬ification,
+                                       const QImage &icon)
 {
-        Q_UNUSED(room_id)
-        Q_UNUSED(event_id)
         Q_UNUSED(icon)
 
+        const auto room_name =
+          QString::fromStdString(cache::singleRoomInfo(notification.room_id).name);
+        const auto sender =
+          cache::displayName(QString::fromStdString(notification.room_id),
+                             QString::fromStdString(mtx::accessors::sender(notification.event)));
+        const auto text = utils::event_body(notification.event);
+
         if (!isInitialized)
                 init();
 
@@ -54,7 +60,7 @@ NotificationsManager::postNotification(const QString &room_id,
         else
                 templ.setTextField(QString("%1").arg(sender).toStdWString(),
                                    WinToastTemplate::FirstLine);
-        if (isEmoteMessage)
+        if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Emote)
                 templ.setTextField(
                   QString("* ").append(sender).append(" ").append(text).toStdWString(),
                   WinToastTemplate::SecondLine);

From 8c62df1bab3f19d0cdef140e7f8a27edd355d9e6 Mon Sep 17 00:00:00 2001
From: Loren Burkholder 
Date: Sat, 13 Feb 2021 12:59:50 -0500
Subject: [PATCH 47/50] Include notifications header instead of responses
 header

---
 src/notifications/Manager.h        | 2 +-
 src/notifications/ManagerLinux.cpp | 2 +-
 src/notifications/ManagerMac.mm    | 2 +-
 src/notifications/ManagerWin.cpp   | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index e2f9f431..950740ba 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -4,7 +4,7 @@
 #include 
 #include 
 
-#include 
+#include 
 
 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_HAIKU)
 #include 
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index 66592c99..fb424b2a 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -12,7 +12,7 @@
 #include "EventAccessors.h"
 #include "MatrixClient.h"
 #include "Utils.h"
-#include 
+#include 
 
 NotificationsManager::NotificationsManager(QObject *parent)
   : QObject(parent)
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index e50bee89..5609d3de 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -7,7 +7,7 @@
 #include "EventAccessors.h"
 #include "MatrixClient.h"
 #include "Utils.h"
-#include 
+#include 
 
 @interface NSUserNotification (CFIPrivate)
 - (void)set_identityImage:(NSImage *)image;
diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index 7df11308..85abe642 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -5,7 +5,7 @@
 #include "EventAccessors.h"
 #include "MatrixClient.h"
 #include "Utils.h"
-#include 
+#include 
 
 using namespace WinToastLib;
 

From 0d61f4bff1409070ff9a8ddbb0d3150a81a834d7 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 13 Feb 2021 23:52:45 +0100
Subject: [PATCH 48/50] Improve scroll to message a bit by using a ScrollView

---
 resources/qml/MessageView.qml | 384 +++++++++++++++++-----------------
 1 file changed, 193 insertions(+), 191 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 35b5cac4..179f6f54 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -6,220 +6,222 @@ import QtQuick.Layouts 1.2
 import QtQuick.Window 2.2
 import im.nheko 1.0
 
-ListView {
-    id: chat
+ScrollView {
+    contentWidth: availableWidth
+    clip: false
+    palette: colors
+    padding: 8
 
-    property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2 - 8)
+    ListView {
+        id: chat
 
-    Layout.fillWidth: true
-    Layout.fillHeight: true
-    model: TimelineManager.timeline
-    boundsBehavior: Flickable.StopAtBounds
-    pixelAligned: true
-    spacing: 4
-    verticalLayoutDirection: ListView.BottomToTop
-    onCountChanged: {
-        // Mark timeline as read
-        if (atYEnd)
-            model.currentIndex = 0;
+        property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding
 
-    }
+        model: TimelineManager.timeline
+        boundsBehavior: Flickable.StopAtBounds
+        pixelAligned: true
+        spacing: 4
+        verticalLayoutDirection: ListView.BottomToTop
+        onCountChanged: {
+            // Mark timeline as read
+            if (atYEnd)
+                model.currentIndex = 0;
 
-    ScrollHelper {
-        flickable: parent
-        anchors.fill: parent
-        enabled: !Settings.mobileMode
-    }
-
-    Shortcut {
-        sequence: StandardKey.MoveToPreviousPage
-        onActivated: {
-            chat.contentY = chat.contentY - chat.height / 2;
-            chat.returnToBounds();
         }
-    }
 
-    Shortcut {
-        sequence: StandardKey.MoveToNextPage
-        onActivated: {
-            chat.contentY = chat.contentY + chat.height / 2;
-            chat.returnToBounds();
+        ScrollHelper {
+            flickable: parent
+            anchors.fill: parent
+            enabled: !Settings.mobileMode
         }
-    }
-
-    Shortcut {
-        sequence: StandardKey.Cancel
-        onActivated: {
-            if (chat.model.reply)
-                chat.model.reply = undefined;
-            else
-                chat.model.edit = undefined;
-        }
-    }
-
-    Shortcut {
-        sequence: "Alt+Up"
-        onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
-    }
-
-    Shortcut {
-        sequence: "Alt+Down"
-        onActivated: {
-            var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
-            chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
-        }
-    }
-
-    Shortcut {
-        sequence: "Ctrl+E"
-        onActivated: {
-            chat.model.edit = chat.model.reply;
-        }
-    }
-
-    Connections {
-        target: TimelineManager
-        onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
-    }
-
-    Timer {
-        id: readTimer
-
-        // force current read index to update
-        onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
-        interval: 1000
-    }
-
-    Component {
-        id: sectionHeader
-
-        Column {
-            topPadding: 4
-            bottomPadding: 4
-            spacing: 8
-            visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
-            width: parentWidth
-            height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
-
-            Label {
-                id: dateBubble
-
-                anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                visible: modelData && modelData.previousMessageDay !== modelData.day
-                text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
-                color: colors.text
-                height: Math.round(fontMetrics.height * 1.4)
-                width: contentWidth * 1.2
-                horizontalAlignment: Text.AlignHCenter
-                verticalAlignment: Text.AlignVCenter
-
-                background: Rectangle {
-                    radius: parent.height / 2
-                    color: colors.window
-                }
 
+        Shortcut {
+            sequence: StandardKey.MoveToPreviousPage
+            onActivated: {
+                chat.contentY = chat.contentY - chat.height / 2;
+                chat.returnToBounds();
             }
+        }
 
-            Row {
-                height: userName.height
-                spacing: 8
-
-                Avatar {
-                    id: messageUserAvatar
-
-                    width: avatarSize
-                    height: avatarSize
-                    url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
-                    displayName: modelData ? modelData.userName : ""
-                    userid: modelData ? modelData.userId : ""
-                    onClicked: chat.model.openUserProfile(modelData.userId)
-                }
-
-                Connections {
-                    target: chat.model
-                    onRoomAvatarUrlChanged: {
-                        messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
-                    }
-                }
-
-                Label {
-                    id: userName
-
-                    text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
-                    color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
-                    textFormat: Text.RichText
-
-                    MouseArea {
-                        anchors.fill: parent
-                        Layout.alignment: Qt.AlignHCenter
-                        onClicked: chat.model.openUserProfile(modelData.userId)
-                        cursorShape: Qt.PointingHandCursor
-                        propagateComposedEvents: true
-                    }
-
-                }
-
-                Label {
-                    color: colors.buttonText
-                    text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
-                    textFormat: Text.PlainText
-                    elide: Text.ElideRight
-                    width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
-                    font.italic: true
-                }
-
+        Shortcut {
+            sequence: StandardKey.MoveToNextPage
+            onActivated: {
+                chat.contentY = chat.contentY + chat.height / 2;
+                chat.returnToBounds();
             }
-
         }
 
-    }
-
-    ScrollBar.vertical: ScrollBar {
-        id: scrollbar
-    }
-
-    delegate: Item {
-        id: wrapper
-
-        anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-        width: chat.delegateMaxWidth
-        height: section ? section.height + timelinerow.height : timelinerow.height
-
-        Loader {
-            id: section
-
-            property var modelData: model
-            property int parentWidth: parent.width
-
-            active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
-            //asynchronous: true
-            sourceComponent: sectionHeader
-            visible: status == Loader.Ready
+        Shortcut {
+            sequence: StandardKey.Cancel
+            onActivated: {
+                if (chat.model.reply)
+                    chat.model.reply = undefined;
+                else
+                    chat.model.edit = undefined;
+            }
         }
 
-        TimelineRow {
-            id: timelinerow
+        Shortcut {
+            sequence: "Alt+Up"
+            onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
+        }
 
-            y: section.active && section.visible ? section.y + section.height : 0
+        Shortcut {
+            sequence: "Alt+Down"
+            onActivated: {
+                var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
+                chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
+            }
+        }
+
+        Shortcut {
+            sequence: "Ctrl+E"
+            onActivated: {
+                chat.model.edit = chat.model.reply;
+            }
         }
 
         Connections {
-            target: chat
-            onMovementEnded: {
-                if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
-                    chat.model.currentIndex = index;
+            target: TimelineManager
+            onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
+        }
+
+        Timer {
+            id: readTimer
+
+            // force current read index to update
+            onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
+            interval: 1000
+        }
+
+        Component {
+            id: sectionHeader
+
+            Column {
+                topPadding: 4
+                bottomPadding: 4
+                spacing: 8
+                visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
+                width: parentWidth
+                height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
+
+                Label {
+                    id: dateBubble
+
+                    anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+                    visible: modelData && modelData.previousMessageDay !== modelData.day
+                    text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
+                    color: colors.text
+                    height: Math.round(fontMetrics.height * 1.4)
+                    width: contentWidth * 1.2
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+
+                    background: Rectangle {
+                        radius: parent.height / 2
+                        color: colors.window
+                    }
+
+                }
+
+                Row {
+                    height: userName.height
+                    spacing: 8
+
+                    Avatar {
+                        id: messageUserAvatar
+
+                        width: avatarSize
+                        height: avatarSize
+                        url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
+                        displayName: modelData ? modelData.userName : ""
+                        userid: modelData ? modelData.userId : ""
+                        onClicked: chat.model.openUserProfile(modelData.userId)
+                    }
+
+                    Connections {
+                        target: chat.model
+                        onRoomAvatarUrlChanged: {
+                            messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
+                        }
+                    }
+
+                    Label {
+                        id: userName
+
+                        text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
+                        color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
+                        textFormat: Text.RichText
+
+                        MouseArea {
+                            anchors.fill: parent
+                            Layout.alignment: Qt.AlignHCenter
+                            onClicked: chat.model.openUserProfile(modelData.userId)
+                            cursorShape: Qt.PointingHandCursor
+                            propagateComposedEvents: true
+                        }
+
+                    }
+
+                    Label {
+                        color: colors.buttonText
+                        text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
+                        textFormat: Text.PlainText
+                        elide: Text.ElideRight
+                        width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
+                        font.italic: true
+                    }
+
+                }
 
             }
+
+        }
+
+        delegate: Item {
+            id: wrapper
+
+            anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+            width: chat.delegateMaxWidth
+            height: section ? section.height + timelinerow.height : timelinerow.height
+
+            Loader {
+                id: section
+
+                property var modelData: model
+                property int parentWidth: parent.width
+
+                active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
+                //asynchronous: true
+                sourceComponent: sectionHeader
+                visible: status == Loader.Ready
+            }
+
+            TimelineRow {
+                id: timelinerow
+
+                y: section.active && section.visible ? section.y + section.height : 0
+            }
+
+            Connections {
+                target: chat
+                onMovementEnded: {
+                    if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
+                        chat.model.currentIndex = index;
+
+                }
+            }
+
+        }
+
+        footer: BusyIndicator {
+            anchors.horizontalCenter: parent.horizontalCenter
+            running: chat.model && chat.model.paginationInProgress
+            height: 50
+            width: 50
+            z: 3
         }
 
     }
 
-    footer: BusyIndicator {
-        anchors.horizontalCenter: parent.horizontalCenter
-        running: chat.model && chat.model.paginationInProgress
-        height: 50
-        width: 50
-        z: 3
-    }
-
 }

From d43607d01c63e003c54c6ba56bb1108cb38cace1 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 14 Feb 2021 01:28:28 +0100
Subject: [PATCH 49/50] Fix hover handling in the timeline

---
 CMakeLists.txt                                |  2 +
 README.md                                     |  4 +-
 resources/qml/Avatar.qml                      |  5 +++
 resources/qml/EncryptionIndicator.qml         |  9 ++---
 resources/qml/ImageButton.qml                 |  4 +-
 resources/qml/MatrixText.qml                  |  8 ++--
 resources/qml/MessageView.qml                 | 13 ++++---
 resources/qml/TimelineRow.qml                 | 38 ++++++++-----------
 resources/qml/delegates/FileMessage.qml       | 12 +++++-
 resources/qml/delegates/ImageMessage.qml      | 16 ++++----
 .../qml/delegates/PlayableMediaMessage.qml    | 11 ++++--
 resources/qml/delegates/Reply.qml             | 16 ++++----
 resources/qml/delegates/TextMessage.qml       |  1 +
 src/timeline/TimelineViewManager.cpp          |  4 +-
 src/ui/NhekoCursorShape.cpp                   | 25 ++++++++++++
 src/ui/NhekoCursorShape.h                     | 26 +++++++++++++
 16 files changed, 127 insertions(+), 67 deletions(-)
 create mode 100644 src/ui/NhekoCursorShape.cpp
 create mode 100644 src/ui/NhekoCursorShape.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2d3c189f..72190947 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -281,6 +281,7 @@ set(SRC_FILES
 	src/ui/InfoMessage.cpp
 	src/ui/Label.cpp
 	src/ui/LoadingIndicator.cpp
+	src/ui/NhekoCursorShape.cpp
 	src/ui/NhekoDropArea.cpp
 	src/ui/OverlayModal.cpp
 	src/ui/OverlayWidget.cpp
@@ -495,6 +496,7 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/ui/Label.h
 	src/ui/FloatingButton.h
 	src/ui/Menu.h
+	src/ui/NhekoCursorShape.h
 	src/ui/NhekoDropArea.h
 	src/ui/OverlayWidget.h
 	src/ui/SnackBar.h
diff --git a/README.md b/README.md
index 2ee06940..b9690fc5 100644
--- a/README.md
+++ b/README.md
@@ -116,9 +116,7 @@ brew install --cask nheko
 
 ### Build Requirements
 
-- Qt5 (5.10 or greater). Qt 5.7 adds support for color font rendering with
-  Freetype, which is essential to properly support emoji, 5.8 adds some features
-  to make interopability with Qml easier, 5.10 makes sliders actually visible with different palettes.
+- Qt5 (5.12 or greater). Required for overlapping hover handlers in Qml.
 - CMake 3.15 or greater. (Lower version may work, but may break boost linking)
 - [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
 - [LMDB](https://symas.com/lightning-memory-mapped-database/)
diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index a459fe5a..f01911bb 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -90,4 +90,9 @@ Rectangle {
         }
     }
 
+    CursorShape {
+        anchors.fill: parent
+        cursorShape: Qt.PointingHandCursor
+    }
+
 }
diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml
index 46ca62c5..00efe9e4 100644
--- a/resources/qml/EncryptionIndicator.qml
+++ b/resources/qml/EncryptionIndicator.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.5
+import QtQuick 2.12
 import QtQuick.Controls 2.1
 import im.nheko 1.0
 
@@ -24,14 +24,11 @@ Rectangle {
     color: "transparent"
     width: 16
     height: 16
-    ToolTip.visible: ma.containsMouse && indicator.visible
+    ToolTip.visible: ma.hovered && indicator.visible
     ToolTip.text: getEncryptionTooltip()
 
-    MouseArea {
+    HoverHandler {
         id: ma
-
-        anchors.fill: parent
-        hoverEnabled: true
     }
 
     Image {
diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index 9c0faef3..159c750f 100644
--- a/resources/qml/ImageButton.qml
+++ b/resources/qml/ImageButton.qml
@@ -1,6 +1,7 @@
 import "./ui"
 import QtQuick 2.3
 import QtQuick.Controls 2.3
+import im.nheko 1.0 // for cursor shape
 
 AbstractButton {
     id: button
@@ -23,11 +24,10 @@ AbstractButton {
         source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
     }
 
-    MouseArea {
+    CursorShape {
         id: mouseArea
 
         anchors.fill: parent
-        onPressed: mouse.accepted = false
         cursorShape: Qt.PointingHandCursor
     }
 
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index bb3b4296..4ea15518 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -8,6 +8,7 @@ TextEdit {
     focus: false
     wrapMode: Text.Wrap
     selectByMouse: !Settings.mobileMode
+    enabled: selectByMouse
     color: colors.text
     onLinkActivated: {
         if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) {
@@ -25,12 +26,9 @@ TextEdit {
     ToolTip.visible: hoveredLink
     ToolTip.text: hoveredLink
 
-    MouseArea {
-        id: ma
-
+    CursorShape {
         anchors.fill: parent
-        acceptedButtons: Qt.NoButton
-        cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
+        cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
     }
 
 }
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 179f6f54..e1641a36 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -1,6 +1,6 @@
 import "./delegates"
 import QtGraphicalEffects 1.0
-import QtQuick 2.9
+import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.2
@@ -153,12 +153,15 @@ ScrollView {
                         color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
                         textFormat: Text.RichText
 
-                        MouseArea {
+                        TapHandler {
+                            //cursorShape: Qt.PointingHandCursor
+
+                            onSingleTapped: chat.model.openUserProfile(modelData.userId)
+                        }
+
+                        CursorShape {
                             anchors.fill: parent
-                            Layout.alignment: Qt.AlignHCenter
-                            onClicked: chat.model.openUserProfile(modelData.userId)
                             cursorShape: Qt.PointingHandCursor
-                            propagateComposedEvents: true
                         }
 
                     }
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 5ec23d62..3a2ed627 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -1,6 +1,6 @@
 import "./delegates"
 import "./emoji"
-import QtQuick 2.6
+import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.2
@@ -12,27 +12,23 @@ Item {
     height: row.height
 
     Rectangle {
-        color: (Settings.messageHoverHighlight && hoverHandler.containsMouse) ? colors.alternateBase : "transparent"
+        color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent"
         anchors.fill: row
     }
 
-    MouseArea {
+    HoverHandler {
         id: hoverHandler
 
-        anchors.fill: parent
-        propagateComposedEvents: true
-        preventStealing: false
-        hoverEnabled: true
-        acceptedButtons: Qt.AllButtons
-        onClicked: {
-            if (mouse.button === Qt.RightButton)
-                messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row);
-            else
-                mouse.accepted = false;
-        }
-        onPressAndHold: {
-            messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, mouse.x, mouse.y));
-        }
+        acceptedDevices: PointerDevice.GenericPointer
+    }
+
+    TapHandler {
+        acceptedButtons: Qt.RightButton
+        onSingleTapped: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, eventPoint.position.x, eventPoint.position.y))
+    }
+
+    TapHandler {
+        onLongPressed: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, point.position.x, point.position.y))
     }
 
     RowLayout {
@@ -151,15 +147,11 @@ Item {
             text: model.timestamp.toLocaleTimeString("HH:mm")
             width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
             color: inactiveColors.text
-            ToolTip.visible: ma.containsMouse
+            ToolTip.visible: ma.hovered
             ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
 
-            MouseArea {
+            HoverHandler {
                 id: ma
-
-                anchors.fill: parent
-                hoverEnabled: true
-                propagateComposedEvents: true
             }
 
         }
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index ffd1e82b..4bc202eb 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.6
+import QtQuick 2.12
 import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
@@ -31,7 +31,15 @@ Item {
 
             MouseArea {
                 anchors.fill: parent
-                onClicked: TimelineManager.timeline.saveMedia(model.data.id)
+                cursorShape: Qt.PointingHandCursor
+            }
+
+            TapHandler {
+                onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
+            }
+
+            CursorShape {
+                anchors.fill: parent
                 cursorShape: Qt.PointingHandCursor
             }
 
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index e8e325f0..3bb9eb05 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.6
+import QtQuick 2.12
 import im.nheko 1.0
 
 Item {
@@ -32,20 +32,20 @@ Item {
         smooth: true
         mipmap: true
 
-        MouseArea {
-            id: mouseArea
-
+        TapHandler {
             enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
-            hoverEnabled: true
-            anchors.fill: parent
-            onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id)
+            onSingleTapped: TimelineManager.openImageOverlay(model.data.url, model.data.id)
+        }
+
+        HoverHandler {
+            id: mouseArea
         }
 
         Item {
             id: overlay
 
             anchors.fill: parent
-            visible: mouseArea.containsMouse
+            visible: mouseArea.hovered
 
             Rectangle {
                 id: container
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 1534da2e..70f39e43 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -1,5 +1,5 @@
 import QtMultimedia 5.6
-import QtQuick 2.6
+import QtQuick 2.12
 import QtQuick.Controls 2.1
 import QtQuick.Layouts 1.2
 import im.nheko 1.0
@@ -140,9 +140,8 @@ Rectangle {
                     fillMode: Image.Pad
                 }
 
-                MouseArea {
-                    anchors.fill: parent
-                    onClicked: {
+                TapHandler {
+                    onSingleTapped: {
                         switch (button.state) {
                         case "":
                             TimelineManager.timeline.cacheMedia(model.data.id);
@@ -159,6 +158,10 @@ Rectangle {
                             break;
                         }
                     }
+                }
+
+                CursorShape {
+                    anchors.fill: parent
                     cursorShape: Qt.PointingHandCursor
                 }
 
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index ff1fa657..28c4bf6e 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.6
+import QtQuick 2.12
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.2
@@ -13,10 +13,12 @@ Item {
     width: parent.width
     height: replyContainer.height
 
-    MouseArea {
+    TapHandler {
+        onSingleTapped: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
+    }
+
+    CursorShape {
         anchors.fill: parent
-        preventStealing: false
-        onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain)
         cursorShape: Qt.PointingHandCursor
     }
 
@@ -43,10 +45,8 @@ Item {
             color: replyComponent.userColor
             textFormat: Text.RichText
 
-            MouseArea {
-                anchors.fill: parent
-                onClicked: chat.model.openUserProfile(reply.modelData.userId)
-                cursorShape: Qt.PointingHandCursor
+            TapHandler {
+                onSingleTapped: chat.model.openUserProfile(reply.modelData.userId)
             }
 
         }
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index 2c449ca2..82d0d0e4 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -8,5 +8,6 @@ MatrixText {
     width: parent ? parent.width : undefined
     height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
     clip: isReply
+    selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
 }
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e1e2b681..b7d2bfb1 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -21,6 +21,7 @@
 #include "dialogs/ImageOverlay.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "ui/NhekoCursorShape.h"
 #include "ui/NhekoDropArea.h"
 
 #include  //only for debugging
@@ -118,6 +119,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         qmlRegisterType("im.nheko", 1, 0, "DelegateChoice");
         qmlRegisterType("im.nheko", 1, 0, "DelegateChooser");
         qmlRegisterType("im.nheko", 1, 0, "NhekoDropArea");
+        qmlRegisterType("im.nheko", 1, 0, "CursorShape");
         qmlRegisterUncreatableType(
           "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
         qmlRegisterUncreatableType(
@@ -548,4 +550,4 @@ void
 TimelineViewManager::focusMessageInput()
 {
         emit focusInput();
-}
\ No newline at end of file
+}
diff --git a/src/ui/NhekoCursorShape.cpp b/src/ui/NhekoCursorShape.cpp
new file mode 100644
index 00000000..06b0a321
--- /dev/null
+++ b/src/ui/NhekoCursorShape.cpp
@@ -0,0 +1,25 @@
+#include "NhekoCursorShape.h"
+
+#include 
+
+NhekoCursorShape::NhekoCursorShape(QQuickItem *parent)
+  : QQuickItem(parent)
+  , currentShape_(Qt::CursorShape::ArrowCursor)
+{}
+
+Qt::CursorShape
+NhekoCursorShape::cursorShape() const
+{
+        return cursor().shape();
+}
+
+void
+NhekoCursorShape::setCursorShape(Qt::CursorShape cursorShape)
+{
+        if (currentShape_ == cursorShape)
+                return;
+
+        currentShape_ = cursorShape;
+        setCursor(cursorShape);
+        emit cursorShapeChanged();
+}
diff --git a/src/ui/NhekoCursorShape.h b/src/ui/NhekoCursorShape.h
new file mode 100644
index 00000000..2eab5e42
--- /dev/null
+++ b/src/ui/NhekoCursorShape.h
@@ -0,0 +1,26 @@
+#pragma once
+
+// see
+// https://stackoverflow.com/questions/27821054/how-to-change-cursor-shape-in-qml-when-mousearea-is-covered-with-another-mousear/29382092#29382092
+
+#include 
+
+class NhekoCursorShape : public QQuickItem
+{
+        Q_OBJECT
+
+        Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape NOTIFY
+                     cursorShapeChanged)
+
+public:
+        explicit NhekoCursorShape(QQuickItem *parent = 0);
+
+private:
+        Qt::CursorShape cursorShape() const;
+        void setCursorShape(Qt::CursorShape cursorShape);
+
+        Qt::CursorShape currentShape_;
+
+signals:
+        void cursorShapeChanged();
+};

From 734fb7e28690f3878ae60ff7aec025d2c8987078 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 14 Feb 2021 01:56:46 +0100
Subject: [PATCH 50/50] Add double tap to reply feature

Does not always work, since Text steals focus...

relates to #414
---
 resources/qml/TimelineRow.qml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 3a2ed627..b731d29d 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -29,6 +29,7 @@ Item {
 
     TapHandler {
         onLongPressed: messageContextMenu.show(model.id, model.type, model.isEncrypted, model.isEditable, row, mapToItem(timelineRoot, point.position.x, point.position.y))
+        onDoubleTapped: chat.model.reply = model.id
     }
 
     RowLayout {