From d1eb35197546a491ce3503c7c2582980e90b6c52 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 21 Aug 2024 00:14:34 +0200 Subject: [PATCH] Allow loading image only after explicit interactions --- resources/qml/delegates/ImageMessage.qml | 19 ++++++++-- src/UserSettingsPage.cpp | 47 ++++++++++++++++++++++++ src/UserSettingsPage.h | 14 +++++++ src/notifications/Manager.cpp | 24 ++++++++++++ src/notifications/Manager.h | 2 +- src/notifications/ManagerLinux.cpp | 2 +- src/notifications/ManagerMac.cpp | 4 +- src/notifications/ManagerWin.cpp | 5 ++- src/timeline/TimelineModel.cpp | 22 +++++++++++ src/timeline/TimelineModel.h | 1 + 10 files changed, 132 insertions(+), 8 deletions(-) diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index f75ad7b7..a97ba9f7 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -20,6 +20,8 @@ AbstractButton { required property int containerHeight property double divisor: EventDelegateChooser.isReply ? 10 : 4 + property bool showImage: room.showImage() + EventDelegateChooser.keepAspectRatio: true EventDelegateChooser.maxWidth: originalWidth EventDelegateChooser.maxHeight: containerHeight / divisor @@ -113,7 +115,7 @@ AbstractButton { visible: !mxcimage.loaded anchors.fill: parent - source: url != "" ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : "" + source: (url != "" && showImage) ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : "" asynchronous: true fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignLeft @@ -130,7 +132,7 @@ AbstractButton { visible: loaded roomm: room play: !Settings.animateImagesOnHover || parent.hovered - eventId: parent.eventId + eventId: showImage ? parent.eventId : "" anchors.fill: parent } @@ -147,7 +149,9 @@ AbstractButton { anchors.fill: parent } - onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight); + onClicked: { + Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight); + } Item { id: overlay @@ -180,4 +184,13 @@ AbstractButton { } + Button { + anchors.centerIn: parent + visible: !showImage && !parent.EventDelegateChooser.isReply + enabled: visible + text: qsTr("Show") + onClicked: { + showImage = true; + } + } } diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 2172c34c..a56aaa6d 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -133,6 +133,12 @@ UserSettings::load(std::optional profile) presenceValue = 0; presence_ = static_cast(presenceValue); + auto tempShowImage = settings.value(prefix + "user/show_images", "").toString().toStdString(); + auto showImageValue = QMetaEnum::fromType().keyToValue(tempShowImage.c_str()); + if (showImageValue < 0) + showImageValue = 0; + showImage_ = static_cast(showImageValue); + collapsedSpaces_.clear(); auto tempSpaces = settings.value(prefix + "user/collapsed_spaces", QList{}).toList(); for (const auto &e : std::as_const(tempSpaces)) @@ -612,6 +618,16 @@ UserSettings::setPresence(Presence state) save(); } +void +UserSettings::setShowImage(ShowImage state) +{ + if (state == showImage_) + return; + showImage_ = state; + emit showImageChanged(state); + save(); +} + void UserSettings::setTheme(QString theme) { @@ -955,6 +971,9 @@ UserSettings::save() settings.setValue( prefix + "user/presence", QString::fromUtf8(QMetaEnum::fromType().valueToKey(static_cast(presence_)))); + settings.setValue( + prefix + "user/show_images", + QString::fromUtf8(QMetaEnum::fromType().valueToKey(static_cast(showImage_)))); QVariantList v; v.reserve(collapsedSpaces_.size()); @@ -1024,6 +1043,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Enable small Avatars"); case AnimateImagesOnHover: return tr("Play animated images only on hover"); + case ShowImage: + return tr("Show images automatically"); case TypingNotifications: return tr("Typing notifications"); case SortByImportance: @@ -1182,6 +1203,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return i->smallAvatars(); case AnimateImagesOnHover: return i->animateImagesOnHover(); + case ShowImage: + return static_cast(i->showImage()); case TypingNotifications: return i->typingNotifications(); case SortByImportance: @@ -1349,6 +1372,10 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Avatars are resized to fit above the message."); case AnimateImagesOnHover: return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them."); + case ShowImage: + return tr("If images should be automatically displayed. You can select between always " + "showing images by default, only show them by default in private rooms or " + "always require interaction to show images."); case TypingNotifications: return tr( "Show who is typing in a room.\nThis will also enable or disable sending typing " @@ -1504,6 +1531,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case CameraResolution: case CameraFrameRate: case Ringtone: + case ShowImage: return Options; case TimelineMaxWidth: case PrivacyScreenTimeout: @@ -1631,6 +1659,12 @@ UserSettingsModel::data(const QModelIndex &index, int role) const QStringLiteral("Dark"), QStringLiteral("System"), }; + case ShowImage: + return QStringList{ + tr("Always"), + tr("Only in private rooms"), + tr("Never"), + }; case Microphone: return vecToList(CallDevices::instance().names(false, i->microphone().toStdString())); case Camera: @@ -1710,6 +1744,16 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int } else return false; } + case ShowImage: { + auto showImageValue = value.toInt(); + if (showImageValue < 0 || + + QMetaEnum::fromType().keyCount() <= showImageValue) + return false; + + i->setShowImage(static_cast(showImageValue)); + return true; + } case MessageHoverHighlight: { if (value.userType() == QMetaType::Bool) { i->setMessageHoverHighlight(value.toBool()); @@ -2241,6 +2285,9 @@ UserSettingsModel::UserSettingsModel(QObject *p) connect(s.get(), &UserSettings::animateImagesOnHoverChanged, this, [this]() { emit dataChanged(index(AnimateImagesOnHover), index(AnimateImagesOnHover), {Value}); }); + connect(s.get(), &UserSettings::showImageChanged, this, [this]() { + emit dataChanged(index(ShowImage), index(ShowImage), {Value}); + }); connect(s.get(), &UserSettings::typingNotificationsChanged, this, [this]() { emit dataChanged(index(TypingNotifications), index(TypingNotifications), {Value}); }); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 44cf7c84..63a4d616 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -75,6 +75,7 @@ class UserSettings final : public QObject Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) Q_PROPERTY(QString emojiFont READ emojiFont WRITE setEmojiFontFamily NOTIFY emojiFontChanged) Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged) + Q_PROPERTY(ShowImage showImage READ showImage WRITE setShowImage NOTIFY showImageChanged) Q_PROPERTY(QString ringtone READ ringtone WRITE setRingtone NOTIFY ringtoneChanged) Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged) Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged) @@ -157,6 +158,14 @@ public: }; Q_ENUM(Presence) + enum class ShowImage + { + Always, + OnlyPrivate, + Never, + }; + Q_ENUM(ShowImage) + void save(); void load(std::optional profile); void applyTheme(); @@ -196,6 +205,7 @@ public: void setPrivacyScreen(bool state); void setPrivacyScreenTimeout(int state); void setPresence(Presence state); + void setShowImage(ShowImage state); void setRingtone(QString ringtone); void setMicrophone(QString microphone); void setCamera(QString camera); @@ -273,6 +283,7 @@ public: return emojiFont_; } Presence presence() const { return presence_; } + ShowImage showImage() const { return showImage_; } QString ringtone() const { return ringtone_; } QString microphone() const { return microphone_; } QString camera() const { return camera_; } @@ -343,6 +354,7 @@ signals: void fontChanged(QString state); void emojiFontChanged(QString state); void presenceChanged(Presence state); + void showImageChanged(ShowImage state); void ringtoneChanged(QString ringtone); void microphoneChanged(QString microphone); void cameraChanged(QString camera); @@ -418,6 +430,7 @@ private: QString font_; QString emojiFont_; Presence presence_; + ShowImage showImage_; QString ringtone_; QString microphone_; QString camera_; @@ -490,6 +503,7 @@ class UserSettingsModel : public QAbstractListModel TimelineSection, TimelineMaxWidth, EnlargeEmojiOnlyMessages, + ShowImage, OpenImageExternal, OpenVideoExternal, ButtonsInTimeline, diff --git a/src/notifications/Manager.cpp b/src/notifications/Manager.cpp index d15eea51..f67e01ff 100644 --- a/src/notifications/Manager.cpp +++ b/src/notifications/Manager.cpp @@ -5,9 +5,33 @@ #include "notifications/Manager.h" #include "Cache.h" +#include "Cache_p.h" #include "EventAccessors.h" +#include "UserSettingsPage.h" #include "Utils.h" +bool +NotificationsManager::allowShowingImages(const mtx::responses::Notification ¬ification) +{ + auto show = UserSettings::instance()->showImage(); + + switch (show) { + case UserSettings::ShowImage::Always: + return true; + case UserSettings::ShowImage::OnlyPrivate: { + auto accessRules = cache::client() + ->getStateEvent(notification.room_id) + .value_or(mtx::events::StateEvent{}) + .content; + + return accessRules.join_rule != mtx::events::state::JoinRule::Public; + } + case UserSettings::ShowImage::Never: + default: + return false; + } +} + QString NotificationsManager::getMessageTemplate(const mtx::responses::Notification ¬ification) { diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h index 707a4fb3..b748b510 100644 --- a/src/notifications/Manager.h +++ b/src/notifications/Manager.h @@ -66,7 +66,6 @@ private: const QString &text, const QImage &icon); void closeNotification(uint id); - const bool hasMarkup_; const bool hasImages_; #endif @@ -111,6 +110,7 @@ private slots: private: QString getMessageTemplate(const mtx::responses::Notification ¬ification); + bool allowShowingImages(const mtx::responses::Notification ¬ification); // notification ID to (room ID, event ID) // Only populated on Linux atm diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index 75ba1886..1704d3a3 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -114,7 +114,7 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if } if (hasMarkup_) { - if (hasImages_ && + if (hasImages_ && allowShowingImages(notification) && (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image || mtx::accessors::event_type(notification.event) == mtx::events::EventType::Sticker)) { MxcImageProvider::download( diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp index ee5639e4..03b2480d 100644 --- a/src/notifications/ManagerMac.cpp +++ b/src/notifications/ManagerMac.cpp @@ -93,7 +93,9 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if } else { const QString messageInfo = (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) + if (allowShowingImages(notification) && + (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image || + mtx::accessors::event_type(notification.event) == mtx::events::EventType::Sticker)) MxcImageProvider::download( QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"), QSize(200, 80), diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp index a69afa4e..03fd4782 100644 --- a/src/notifications/ManagerWin.cpp +++ b/src/notifications/ManagerWin.cpp @@ -83,8 +83,9 @@ NotificationsManager::postNotification(const mtx::responses::Notification ¬if if (!icon.save(iconPath)) iconPath.clear(); - if (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image || - mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image) { + if (allowShowingImages(notification) && + (mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image || + mtx::accessors::msg_type(notification.event) == mtx::events::MessageType::Image)) { MxcImageProvider::download( QString::fromStdString(mtx::accessors::url(notification.event)) .remove(QStringLiteral("mxc://")), diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 48b58ee5..eea345b5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -3387,4 +3387,26 @@ TimelineModel::parentSpace() return parentSummary.get(); } +bool +TimelineModel::showImage() const +{ + auto show = UserSettings::instance()->showImage(); + + switch (show) { + case UserSettings::ShowImage::Always: + return true; + case UserSettings::ShowImage::OnlyPrivate: { + auto accessRules = cache::client() + ->getStateEvent(room_id_.toStdString()) + .value_or(mtx::events::StateEvent{}) + .content; + + return accessRules.join_rule != mtx::events::state::JoinRule::Public; + } + case UserSettings::ShowImage::Never: + default: + return false; + } +} + #include "moc_TimelineModel.cpp" diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 3a189e39..9f96c62d 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -323,6 +323,7 @@ public: const mtx::events::StateEvent &event) const; Q_INVOKABLE QString formatPolicyRule(const QString &id) const; Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); + Q_INVOKABLE bool showImage() const; Q_INVOKABLE void viewRawMessage(const QString &id); Q_INVOKABLE void forwardMessage(const QString &eventId, QString roomId);