From e8ff6c9486d325a8b09a0b0bd191a7e31321e709 Mon Sep 17 00:00:00 2001 From: Alexander von Gluck IV Date: Wed, 3 Feb 2021 14:40:14 -0600 Subject: notifications/mananger: Follow Linux code paths on Haiku as well --- src/notifications/Manager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') 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 & -- cgit 1.5.1 From 7874d61c337d7d24aec918ccdd6876ceaa20f989 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 4 Feb 2021 01:02:38 +0100 Subject: Fix scheme handler not passing arguments --- resources/nheko.desktop | 2 +- src/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') 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="); -- cgit 1.5.1 From eae09f8f14c046e12ff6d4123d9a09ed00a82dc0 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:41:00 -0500 Subject: Fix bug on logout of non-default profile --- src/ChatPage.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'src') 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(); -- cgit 1.5.1 From 46e15218d4bdc83d27b688b5856fcd18d37ddb1c Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:41:32 -0500 Subject: Use UserSettings where possible --- src/Utils.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'src') 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"; -- cgit 1.5.1 From 777b9bf20de698bf9dabd2782c212a3b819c8e51 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 18:43:33 -0500 Subject: Set profile to "" if it's the default for compatibility --- src/UserSettingsPage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') 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(); -- cgit 1.5.1 From 343c9c811630b1321f3f15458c5c802d0c5094eb Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Thu, 4 Feb 2021 19:01:48 -0500 Subject: Don't attempt to begin group "" --- src/ChatPage.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') 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(); -- cgit 1.5.1 From f02342fe227c6ab05e94d6ac72d05dd0b6f127a2 Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Fri, 5 Feb 2021 21:52:49 +0530 Subject: 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(+) (limited to 'src') 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); } -- cgit 1.5.1 From 4aefac08a4b57e6fb344cd8cffb0b6e32698e0ba Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Fri, 5 Feb 2021 22:42:08 +0530 Subject: 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(+) (limited to 'src') 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); -- cgit 1.5.1 From 8d195a4d11d6dc0584a626e72581782b494bfc8e Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sat, 6 Feb 2021 10:24:41 +0530 Subject: translation fix for default text in emoji combo --- src/UserSettingsPage.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') 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); } -- cgit 1.5.1 From 375e20462ba3641db3b71c12a66533c63ac2d597 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 7 Feb 2021 02:01:25 +0100 Subject: Native rendering breaks kerning --- src/timeline/TimelineViewManager.cpp | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') 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); }); -- cgit 1.5.1 From c2a56fc23301967927ac019db0ac8c2ad99618ac Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sun, 7 Feb 2021 22:15:06 +0530 Subject: emoji default translation fix --- src/UserSettingsPage.cpp | 21 +++++++++++++++++---- src/UserSettingsPage.h | 8 +++++++- 2 files changed, 24 insertions(+), 5 deletions(-) (limited to 'src') 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_; } -- cgit 1.5.1 From f1bc3ba587baacce84acbdb04343d5c4b74d18a2 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 11:47:47 -0500 Subject: 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 (limited to 'src') 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; -- cgit 1.5.1 From 04b920fbee4547b65a21af03ba709281250e5960 Mon Sep 17 00:00:00 2001 From: Jedi18 Date: Sun, 7 Feb 2021 22:18:04 +0530 Subject: linting fix --- src/UserSettingsPage.cpp | 3 +-- src/UserSettingsPage.h | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') 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_; } -- cgit 1.5.1 From 8d68534456481605c89c4ec5656792c83e2d8d01 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 13:54:18 -0500 Subject: Add Duplex call devices --- src/CallDevices.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') 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); } -- cgit 1.5.1 From 974c336c5e95fc3a51b473d5e1899c49817ab704 Mon Sep 17 00:00:00 2001 From: trilene Date: Sun, 7 Feb 2021 13:58:32 -0500 Subject: 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(-) (limited to 'src') 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; } -- cgit 1.5.1 From 50f994bd235f4f551ebdbef6df0d83209ea2042b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 16:26:18 +0100 Subject: Clean up config names a bit --- src/UserSettingsPage.cpp | 11 ++++------- src/main.cpp | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) (limited to 'src') 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()); -- cgit 1.5.1 From 0285bf5e4e625e5156034c942261afc731fd649a Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 16:31:33 +0100 Subject: Remove unused variables --- src/ui/UserProfile.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') 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); -- cgit 1.5.1 From 8d95532b2840916897233e505a47e52472afb873 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 17:00:06 +0100 Subject: Fix linting --- src/ui/UserProfile.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'src') 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 -- cgit 1.5.1 From 463cee71460bfa64dfd64bf7018fb928e3589331 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 9 Feb 2021 20:20:37 +0100 Subject: Fix wrong font used in emoji escape --- src/Utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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; } -- cgit 1.5.1 From 2e77a1554f1572b7c7e59f8177a48e5dffa16c23 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 26 Jan 2021 22:36:35 +0100 Subject: 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(-) (limited to 'src') 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) -{ - return std::visit(EventInReplyTo{}, event); -} -std::string -mtx::accessors::relates_to_event_id(const mtx::events::collections::TimelineEvents &event) +mtx::common::Relations +mtx::accessors::relations(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 -- cgit 1.5.1 From d6504812c71ff7251a5319113c580ab322469eb3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 02:45:33 +0100 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From faeaf9dc6bfae5bf56f6edee4e21bb54db08b2e1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 27 Jan 2021 16:14:03 +0100 Subject: Fix edited replies --- src/EventAccessors.cpp | 21 +++++++++++++++++++++ src/EventAccessors.h | 2 ++ src/timeline/EventStore.cpp | 19 +++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) (limited to 'src') 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(); -- cgit 1.5.1 From 00fd4eecec1af3a38bc69f3849f9e49f826cef26 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 31 Jan 2021 22:41:43 +0100 Subject: 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(-) (limited to 'src') 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_; -- cgit 1.5.1 From 9b7d33e847b02031fdc153716614f125992b3734 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 1 Feb 2021 02:22:53 +0100 Subject: 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(-) (limited to 'src') 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) -- cgit 1.5.1 From bdb6e6b79e9beeaabfbde99cd760de77247d11a4 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 01:03:20 +0100 Subject: 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 | 23 +++++++++-- src/timeline/TimelineModel.h | 2 +- 5 files changed, 121 insertions(+), 5 deletions(-) (limited to 'src') 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 ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m")) { - readEvent(currentId.toStdString()); + if (index != oldIndex) + emit currentIndexChanged(index); + + 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_; -- cgit 1.5.1 From 6d678a108f0f1c551565c124de478f366dbe4ee2 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 02:37:47 +0100 Subject: Use fully read marker and fix stuck read marker with edits --- src/Cache.cpp | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) (limited to 'src') 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()) - 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) + if (last_event_id.empty() || fullyReadEventId.empty()) 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; - } + if (last_event_id == fullyReadEventId) + return false; - return true; + // Retrieve all read receipts for that event. + 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 (...) { } -- cgit 1.5.1 From a62276c28933986907022662cea965cf6269eb5e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 14:32:16 +0100 Subject: 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(-) (limited to 'src') 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(""); } } -- cgit 1.5.1 From 2606568376e032a5b66275397a52267f2e4c4578 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 15:24:00 +0100 Subject: 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(-) (limited to 'src') 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(); -- cgit 1.5.1 From 0db4d71ec2483c7ac5a7b536737fee8fc53a76d7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 10 Feb 2021 18:07:55 +0100 Subject: Prevent edits of unsent messages --- src/timeline/TimelineModel.cpp | 3 +++ src/timeline/TimelineModel.h | 3 +++ 2 files changed, 6 insertions(+) (limited to 'src') 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_); -- cgit 1.5.1 From 3c91b5b47befb588a7c8005745a1e8dadf47df03 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 12 Feb 2021 16:10:48 +0100 Subject: Fix crash when editing an edited message pointing to itself --- src/timeline/TimelineModel.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'src') 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_); } } -- cgit 1.5.1 From 7ddcab3902a6b39c3ed8328c245f58a495b4c43f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 13 Feb 2021 01:41:09 +0100 Subject: 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(-) (limited to 'src') 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()); -- cgit 1.5.1 From 299c486a2bd1aff872fcf0b2e76300b569920fc5 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 12 Feb 2021 11:28:41 -0500 Subject: 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(-) (limited to 'src') 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"); -- cgit 1.5.1 From 9f9c499cb2a136aaa5e74fa20c931d8c70885351 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 13 Feb 2021 10:58:09 -0500 Subject: Fix typo --- src/notifications/ManagerWin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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"; } } -- cgit 1.5.1 From 567b2d05effd32c8804e3039250e3b44d4e7c91e Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 13 Feb 2021 12:10:49 -0500 Subject: 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(-) (limited to 'src') 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); -- cgit 1.5.1 From 8c62df1bab3f19d0cdef140e7f8a27edd355d9e6 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Sat, 13 Feb 2021 12:59:50 -0500 Subject: 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(-) (limited to 'src') 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; -- cgit 1.5.1 From d43607d01c63e003c54c6ba56bb1108cb38cace1 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 14 Feb 2021 01:28:28 +0100 Subject: 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 +++++----- resources/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 (limited to 'src') 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(); +}; -- cgit 1.5.1