From 760f6757923c9aba2ecd7d89fc0ddfd43a741c6d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 31 Jul 2021 17:59:03 +0200 Subject: Ensure the encrypted rooms db is always created --- src/Cache_p.h | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/Cache_p.h') diff --git a/src/Cache_p.h b/src/Cache_p.h index 89c88925..18b9601f 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -689,6 +689,8 @@ private: lmdb::dbi outboundMegolmSessionDb_; lmdb::dbi megolmSessionDataDb_; + lmdb::dbi encryptedRooms_; + QString localUserId_; QString cacheDirectory_; -- cgit 1.5.1 From 25e7a985b82bbeff9c3c5b4d15b44f2539768260 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 1 Aug 2021 00:59:46 +0200 Subject: Add option to only send encrypted messages to verified devices fixes #636 --- src/Cache.cpp | 49 ++++++++++++++++++-- src/Cache_p.h | 3 +- src/Olm.cpp | 3 +- src/UserSettingsPage.cpp | 113 +++++++++++++++++++++++++++++------------------ src/UserSettingsPage.h | 7 +++ 5 files changed, 127 insertions(+), 48 deletions(-) (limited to 'src/Cache_p.h') diff --git a/src/Cache.cpp b/src/Cache.cpp index 7d0b1a89..291df053 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3542,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id) } std::map> -Cache::getMembersWithKeys(const std::string &room_id) +Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) { std::string_view keys; @@ -3559,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id) auto res = keysDb.get(txn, user_id, keys); if (res) { - members[std::string(user_id)] = - json::parse(keys).get(); + auto k = json::parse(keys).get(); + if (verified_only) { + auto verif = verificationStatus(std::string(user_id)); + if (verif.user_verified == crypto::Trust::Verified || + !verif.verified_devices.empty()) { + auto keyCopy = k; + keyCopy.device_keys.clear(); + + std::copy_if( + k.device_keys.begin(), + k.device_keys.end(), + std::inserter(keyCopy.device_keys, + keyCopy.device_keys.end()), + [&verif](const auto &key) { + auto curve25519 = key.second.keys.find( + "curve25519:" + key.first); + if (curve25519 == key.second.keys.end()) + return false; + if (auto t = + verif.verified_device_keys.find( + curve25519->second); + t == + verif.verified_device_keys.end() || + t->second != crypto::Trust::Verified) + return false; + + return key.first == + key.second.device_id && + std::find( + verif.verified_devices.begin(), + verif.verified_devices.end(), + key.first) != + verif.verified_devices.end(); + }); + + if (!keyCopy.device_keys.empty()) + members[std::string(user_id)] = + std::move(keyCopy); + } + } else { + members[std::string(user_id)] = std::move(k); + } } else { - members[std::string(user_id)] = {}; + if (!verified_only) + members[std::string(user_id)] = {}; } } cursor.close(); diff --git a/src/Cache_p.h b/src/Cache_p.h index 18b9601f..5d700658 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -48,7 +48,8 @@ public: // user cache stores user keys std::optional userKeys(const std::string &user_id); std::map> getMembersWithKeys( - const std::string &room_id); + const std::string &room_id, + bool verified_only); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, diff --git a/src/Olm.cpp b/src/Olm.cpp index e3ca1c34..048a6c0f 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -524,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, auto own_user_id = http::client()->user_id().to_string(); - auto members = cache::client()->getMembersWithKeys(room_id); + auto members = cache::client()->getMembersWithKeys( + room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers()); std::map> sendSessionTo; mtx::crypto::OutboundGroupSessionPtr session = nullptr; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index a062780a..ab6ac492 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -90,13 +90,11 @@ UserSettings::load(std::optional profile) decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); - shareKeysWithTrustedUsers_ = - settings.value("user/automatically_share_keys_with_trusted_users", false).toBool(); - mobileMode_ = settings.value("user/mobile_mode", false).toBool(); - 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()); + mobileMode_ = settings.value("user/mobile_mode", false).toBool(); + 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()); if (presenceValue < 0) presenceValue = 0; presence_ = static_cast(presenceValue); @@ -123,6 +121,12 @@ UserSettings::load(std::optional profile) userId_ = settings.value(prefix + "auth/user_id", "").toString(); deviceId_ = settings.value(prefix + "auth/device_id", "").toString(); + shareKeysWithTrustedUsers_ = + settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false) + .toBool(); + onlyShareKeysWithVerifiedUsers_ = + settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool(); + disableCertificateValidation_ = settings.value("disable_certificate_validation", false).toBool(); @@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer) save(); } +void +UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys) +{ + if (shareKeys == onlyShareKeysWithVerifiedUsers_) + return; + + onlyShareKeysWithVerifiedUsers_ = shareKeys; + emit onlyShareKeysWithVerifiedUsersChanged(shareKeys); + save(); +} + void UserSettings::setShareKeysWithTrustedUsers(bool shareKeys) { @@ -610,8 +625,6 @@ UserSettings::save() settings.setValue("decrypt_sidebar", decryptSidebar_); settings.setValue("privacy_screen", privacyScreen_); settings.setValue("privacy_screen_timeout", privacyScreenTimeout_); - settings.setValue("automatically_share_keys_with_trusted_users", - shareKeysWithTrustedUsers_); settings.setValue("mobile_mode", mobileMode_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", typingNotifications_); @@ -650,6 +663,11 @@ UserSettings::save() settings.setValue(prefix + "auth/user_id", userId_); settings.setValue(prefix + "auth/device_id", deviceId_); + settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users", + shareKeysWithTrustedUsers_); + settings.setValue(prefix + "user/only_share_keys_with_verified_users", + onlyShareKeysWithVerifiedUsers_); + settings.setValue("disable_certificate_validation", disableCertificateValidation_); settings.sync(); @@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - shareKeysWithTrustedUsers_ = new Toggle(this); - groupViewToggle_ = new Toggle{this}; - timelineButtonsToggle_ = new Toggle{this}; - typingNotifications_ = new Toggle{this}; - messageHoverHighlight_ = new Toggle{this}; - enlargeEmojiOnlyMessages_ = new Toggle{this}; - sortByImportance_ = new Toggle{this}; - readReceipts_ = new Toggle{this}; - markdown_ = new Toggle{this}; - desktopNotifications_ = new Toggle{this}; - alertOnNotification_ = new Toggle{this}; - useStunServer_ = new Toggle{this}; - mobileMode_ = new Toggle{this}; - scaleFactorCombo_ = new QComboBox{this}; - fontSizeCombo_ = new QComboBox{this}; - fontSelectionCombo_ = new QFontComboBox{this}; - emojiFontSelectionCombo_ = new QComboBox{this}; - ringtoneCombo_ = new QComboBox{this}; - microphoneCombo_ = new QComboBox{this}; - cameraCombo_ = new QComboBox{this}; - cameraResolutionCombo_ = new QComboBox{this}; - cameraFrameRateCombo_ = new QComboBox{this}; - timelineMaxWidthSpin_ = new QSpinBox{this}; - privacyScreenTimeout_ = new QSpinBox{this}; + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + decryptSidebar_ = new Toggle(this); + privacyScreen_ = new Toggle{this}; + onlyShareKeysWithVerifiedUsers_ = new Toggle(this); + shareKeysWithTrustedUsers_ = new Toggle(this); + groupViewToggle_ = new Toggle{this}; + timelineButtonsToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + messageHoverHighlight_ = new Toggle{this}; + enlargeEmojiOnlyMessages_ = new Toggle{this}; + sortByImportance_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdown_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + alertOnNotification_ = new Toggle{this}; + useStunServer_ = new Toggle{this}; + mobileMode_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QFontComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; + ringtoneCombo_ = new QComboBox{this}; + microphoneCombo_ = new QComboBox{this}; + cameraCombo_ = new QComboBox{this}; + cameraResolutionCombo_ = new QComboBox{this}; + cameraFrameRateCombo_ = new QComboBox{this}; + timelineMaxWidthSpin_ = new QSpinBox{this}; + privacyScreenTimeout_ = new QSpinBox{this}; trayToggle_->setChecked(settings_->tray()); startInTrayToggle_->setChecked(settings_->startInTray()); avatarCircles_->setChecked(settings_->avatarCircles()); decryptSidebar_->setChecked(settings_->decryptSidebar()); privacyScreen_->setChecked(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); groupViewToggle_->setChecked(settings_->groupView()); timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); @@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge formLayout_->addRow(new HorizontalLine{this}); boxWrap(tr("Device ID"), deviceIdValue_); boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - boxWrap( - tr("Share keys with verified users and devices"), - shareKeysWithTrustedUsers_, - tr("Automatically replies to key requests from other users, if they are verified.")); + boxWrap(tr("Send encrypted messages to verified users only"), + onlyShareKeysWithVerifiedUsers_, + tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious.")); + boxWrap(tr("Share keys with verified users and devices"), + shareKeysWithTrustedUsers_, + tr("Automatically replies to key requests from other users, if they are verified, " + "even if that device shouldn't have access to those keys otherwise.")); formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); @@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge } }); + connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setOnlyShareKeysWithVerifiedUsers(enabled); + }); + connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { settings_->setShareKeysWithTrustedUsers(enabled); }); @@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *) groupViewToggle_->setState(settings_->groupView()); decryptSidebar_->setState(settings_->decryptSidebar()); privacyScreen_->setState(settings_->privacyScreen()); + onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); avatarCircles_->setState(settings_->avatarCircles()); typingNotifications_->setState(settings_->typingNotifications()); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index acb08569..096aab81 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -88,6 +88,8 @@ class UserSettings : public QObject setScreenShareHideCursor NOTIFY screenShareHideCursorChanged) Q_PROPERTY( bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) + Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE + setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged) Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged) Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged) @@ -152,6 +154,7 @@ public: void setScreenShareRemoteVideo(bool state); void setScreenShareHideCursor(bool state); void setUseStunServer(bool state); + void setOnlyShareKeysWithVerifiedUsers(bool state); void setShareKeysWithTrustedUsers(bool state); void setProfile(QString profile); void setUserId(QString userId); @@ -208,6 +211,7 @@ public: bool screenShareHideCursor() const { return screenShareHideCursor_; } bool useStunServer() const { return useStunServer_; } bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; } + bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; } QString profile() const { return profile_; } QString userId() const { return userId_; } QString accessToken() const { return accessToken_; } @@ -252,6 +256,7 @@ signals: void screenShareRemoteVideoChanged(bool state); void screenShareHideCursorChanged(bool state); void useStunServerChanged(bool state); + void onlyShareKeysWithVerifiedUsersChanged(bool state); void shareKeysWithTrustedUsersChanged(bool state); void profileChanged(QString profile); void userIdChanged(QString userId); @@ -284,6 +289,7 @@ private: bool privacyScreen_; int privacyScreenTimeout_; bool shareKeysWithTrustedUsers_; + bool onlyShareKeysWithVerifiedUsers_; bool mobileMode_; int timelineMaxWidth_; int roomListWidth_; @@ -372,6 +378,7 @@ private: Toggle *privacyScreen_; QSpinBox *privacyScreenTimeout_; Toggle *shareKeysWithTrustedUsers_; + Toggle *onlyShareKeysWithVerifiedUsers_; Toggle *mobileMode_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; -- cgit 1.5.1 From a57a15a2e07da8cc07bc12e828b7c636efe36cbc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Aug 2021 01:45:47 +0200 Subject: Basic sticker pack editor --- CMakeLists.txt | 2 +- io.github.NhekoReborn.Nheko.yaml | 2 +- resources/qml/Avatar.qml | 6 +- resources/qml/RoomSettings.qml | 4 +- resources/qml/ScrollHelper.qml | 7 +- resources/qml/components/AvatarListTile.qml | 133 ++++++++++ resources/qml/dialogs/ImagePackEditorDialog.qml | 283 ++++++++++++++++++++++ resources/qml/dialogs/ImagePackSettingsDialog.qml | 174 ++++--------- resources/res.qrc | 2 + src/Cache.cpp | 2 +- src/Cache_p.h | 34 ++- src/MxcImageProvider.cpp | 26 +- src/MxcImageProvider.h | 7 +- src/SingleImagePackModel.cpp | 181 ++++++++++++++ src/SingleImagePackModel.h | 38 ++- src/timeline/TimelineModel.cpp | 9 + src/timeline/TimelineModel.h | 8 +- 17 files changed, 751 insertions(+), 167 deletions(-) create mode 100644 resources/qml/components/AvatarListTile.qml create mode 100644 resources/qml/dialogs/ImagePackEditorDialog.qml (limited to 'src/Cache_p.h') diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f824048..e8bc855d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb + GIT_TAG e5688a2c5987a614b5055595f991f18568127bd2 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 0fa450b3..2c0c5ebf 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -161,7 +161,7 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 316a4040785ee2eabac7ef5ce7b4acb71c48f6eb + - commit: e5688a2c5987a614b5055595f991f18568127bd2 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 6c12952a..9685dde1 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -11,10 +11,11 @@ import im.nheko 1.0 Rectangle { id: avatar - property alias url: img.source + property string url property string userid property string displayName property alias textColor: label.color + property bool crop: true signal clicked(var mouse) @@ -44,12 +45,13 @@ Rectangle { anchors.fill: parent asynchronous: true - fillMode: Image.PreserveAspectCrop + fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit mipmap: true smooth: true sourceSize.width: avatar.width sourceSize.height: avatar.height layer.enabled: true + source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale") MouseArea { id: mouseArea diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index 6ba080c4..69cf427c 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -154,7 +154,7 @@ ApplicationWindow { GridLayout { columns: 2 - rowSpacing: 10 + rowSpacing: Nheko.paddingLarge MatrixText { text: qsTr("SETTINGS") @@ -180,7 +180,7 @@ ApplicationWindow { } MatrixText { - text: "Room access" + text: qsTr("Room access") Layout.fillWidth: true } diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index 2dd56f27..e584ae3d 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -30,6 +30,10 @@ MouseArea { property alias enabled: root.enabled function calculateNewPosition(flickableItem, wheel) { + // breaks ListView's with headers... + //if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + // minYExtent += flickableItem.headerItem.height; + //Nothing to scroll if (flickableItem.contentHeight < flickableItem.height) return flickableItem.contentY; @@ -55,9 +59,6 @@ MouseArea { var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) - minYExtent += flickableItem.headerItem.height; - //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml new file mode 100644 index 00000000..36c26a97 --- /dev/null +++ b/resources/qml/components/AvatarListTile.qml @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +Rectangle { + id: tile + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + required property string avatarUrl + required property string title + required property string subtitle + required property int index + required property int selectedIndex + property bool crop: true + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered && !(index == selectedIndex) + + PropertyChanges { + target: tile + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: index == selectedIndex + + PropertyChanges { + target: tile + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + HoverHandler { + id: hovered + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: tile.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: title + crop: tile.crop + } + + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + width: parent.width - avatar.width + Layout.preferredWidth: parent.width - avatar.width + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: tile.importantText + elideWidth: textContent.width - Nheko.paddingMedium + fullText: title + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + color: tile.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - Nheko.paddingSmall + fullText: subtitle + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + +} diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml new file mode 100644 index 00000000..0049d3b4 --- /dev/null +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../components" +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +ApplicationWindow { + //Component.onCompleted: Nheko.reparent(win) + + id: win + + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property SingleImagePackModel imagePack + property int currentImageIndex: -1 + readonly property int stickerDim: 128 + readonly property int stickerDimPad: 128 + Nheko.paddingSmall + + title: qsTr("Editing image pack") + height: 600 + width: 600 + palette: Nheko.colors + color: Nheko.colors.base + modality: Qt.WindowModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: false + pageIndex: 0 + + AdaptiveLayoutElement { + id: packlistC + + visible: Settings.groupView + minimumWidth: 200 + collapsedWidth: 200 + preferredWidth: 300 + maximumWidth: 300 + clip: true + + ListView { + //required property bool isEmote + //required property bool isSticker + + model: imagePack + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + header: AvatarListTile { + title: imagePack.packname + avatarUrl: imagePack.avatarUrl + subtitle: imagePack.statekey + index: -1 + selectedIndex: currentImageIndex + + TapHandler { + onSingleTapped: currentImageIndex = -1 + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: parent.height - Nheko.paddingSmall * 2 + width: 3 + color: Nheko.colors.highlight + } + + } + + delegate: AvatarListTile { + id: packItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + required property string shortCode + required property string url + required property string body + + title: shortCode + subtitle: body + avatarUrl: url + selectedIndex: currentImageIndex + crop: false + + TapHandler { + onSingleTapped: currentImageIndex = index + } + + } + + } + + } + + AdaptiveLayoutElement { + id: packinfoC + + Rectangle { + color: Nheko.colors.window + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex == -1 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: imagePack.packname + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + visible: imagePack.roomid + text: qsTr("State key") + } + + MatrixTextField { + visible: imagePack.roomid + Layout.fillWidth: true + text: imagePack.statekey + onTextEdited: imagePack.statekey = text + } + + MatrixText { + text: qsTr("Packname") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.packname + onTextEdited: imagePack.packname = text + } + + MatrixText { + text: qsTr("Attrbution") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.attribution + onTextEdited: imagePack.attribution = text + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.isEmotePack + onToggled: imagePack.isEmotePack = checked + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.isStickerPack + onToggled: imagePack.isStickerPack = checked + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + GridLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + visible: currentImageIndex >= 0 + enabled: visible + columns: 2 + rowSpacing: Nheko.paddingLarge + + Avatar { + Layout.columnSpan: 2 + url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/") + displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + height: 130 + width: 130 + crop: false + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + text: qsTr("Shortcode") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.ShortCode) + } + + MatrixText { + text: qsTr("Body") + } + + MatrixTextField { + Layout.fillWidth: true + text: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Body) + onTextEdited: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.Body) + } + + MatrixText { + text: qsTr("Use as Emoji") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote) + onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsEmote) + Layout.alignment: Qt.AlignRight + } + + MatrixText { + text: qsTr("Use as Sticker") + } + + ToggleButton { + checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker) + onToggled: imagePack.setData(imagePack.index(currentImageIndex, 0), text, SingleImagePackModel.IsSticker) + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.columnSpan: 2 + Layout.fillHeight: true + } + + } + + } + + } + + } + + footer: DialogButtonBox { + id: buttons + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole + onClicked: win.close() + } + + Button { + text: qsTr("Save") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + onClicked: { + imagePack.save(); + win.close(); + } + } + + } + +} diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index 3d830bf7..c57867fd 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -20,14 +20,22 @@ ApplicationWindow { readonly property int stickerDimPad: 128 + Nheko.paddingSmall title: qsTr("Image pack settings") - height: 400 - width: 600 + height: 600 + width: 800 palette: Nheko.colors color: Nheko.colors.base modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint Component.onCompleted: Nheko.reparent(win) + Component { + id: packEditor + + ImagePackEditorDialog { + } + + } + AdaptiveLayout { id: adaptiveView @@ -54,7 +62,7 @@ ApplicationWindow { enabled: !Settings.mobileMode } - delegate: Rectangle { + delegate: AvatarListTile { id: packItem property color background: Nheko.colors.window @@ -63,131 +71,24 @@ ApplicationWindow { property color bubbleBackground: Nheko.colors.highlight property color bubbleText: Nheko.colors.highlightedText required property string displayName - required property string avatarUrl required property bool fromAccountData required property bool fromCurrentRoom - required property int index - - color: background - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - state: "normal" - states: [ - State { - name: "highlight" - when: hovered.hovered && !(index == currentPackIndex) - - PropertyChanges { - target: packItem - background: Nheko.colors.dark - importantText: Nheko.colors.brightText - unimportantText: Nheko.colors.brightText - bubbleBackground: Nheko.colors.highlight - bubbleText: Nheko.colors.highlightedText - } - - }, - State { - name: "selected" - when: index == currentPackIndex - - PropertyChanges { - target: packItem - background: Nheko.colors.highlight - importantText: Nheko.colors.highlightedText - unimportantText: Nheko.colors.highlightedText - bubbleBackground: Nheko.colors.highlightedText - bubbleText: Nheko.colors.highlight - } - } - ] + title: displayName + subtitle: { + if (fromAccountData) + return qsTr("Private pack"); + else if (fromCurrentRoom) + return qsTr("Pack from this room"); + else + return qsTr("Globally enabled pack"); + } + selectedIndex: currentPackIndex TapHandler { - margin: -Nheko.paddingSmall onSingleTapped: currentPackIndex = index } - HoverHandler { - id: hovered - } - - RowLayout { - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - - Avatar { - // In the future we could show an online indicator by setting the userid for the avatar - //userid: Nheko.currentUser.userid - - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - displayName: packItem.displayName - } - - ColumnLayout { - id: textContent - - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 100 - width: parent.width - avatar.width - Layout.preferredWidth: parent.width - avatar.width - spacing: Nheko.paddingSmall - - RowLayout { - Layout.fillWidth: true - spacing: 0 - - ElidedLabel { - Layout.alignment: Qt.AlignBottom - color: packItem.importantText - elideWidth: textContent.width - Nheko.paddingMedium - fullText: displayName - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - } - - RowLayout { - Layout.fillWidth: true - spacing: 0 - - ElidedLabel { - color: packItem.unimportantText - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: textContent.width - Nheko.paddingSmall - fullText: { - if (fromAccountData) - return qsTr("Private pack"); - else if (fromCurrentRoom) - return qsTr("Pack from this room"); - else - return qsTr("Globally enabled pack"); - } - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - } - - } - - } - } } @@ -201,15 +102,10 @@ ApplicationWindow { color: Nheko.colors.window ColumnLayout { - //Button { - // Layout.alignment: Qt.AlignHCenter - // text: qsTr("Edit") - // enabled: currentPack.canEdit - //} - id: packinfo property string packName: currentPack ? currentPack.packname : "" + property string attribution: currentPack ? currentPack.attribution : "" property string avatarUrl: currentPack ? currentPack.avatarUrl : "" anchors.fill: parent @@ -227,8 +123,18 @@ ApplicationWindow { MatrixText { text: packinfo.packName - font.pixelSize: 24 + font.pixelSize: Math.ceil(fontMetrics.pixelSize * 1.1) + horizontalAlignment: TextEdit.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 + } + + MatrixText { + text: packinfo.attribution + wrapMode: TextEdit.Wrap + horizontalAlignment: TextEdit.AlignHCenter Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: packinfoC.width - Nheko.paddingLarge * 2 } GridLayout { @@ -250,6 +156,18 @@ ApplicationWindow { } + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Edit") + enabled: currentPack.canEdit + onClicked: { + var dialog = packEditor.createObject(timelineRoot, { + "imagePack": currentPack + }); + dialog.show(); + } + } + GridView { Layout.fillHeight: true Layout.fillWidth: true @@ -272,7 +190,7 @@ ApplicationWindow { width: stickerDim height: stickerDim hoverEnabled: true - ToolTip.text: ":" + model.shortcode + ": - " + model.body + ToolTip.text: ":" + model.shortCode + ": - " + model.body ToolTip.visible: hovered contentItem: Image { diff --git a/resources/res.qrc b/resources/res.qrc index c911653c..d7187f42 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -160,6 +160,7 @@ qml/device-verification/Success.qml qml/dialogs/InputDialog.qml qml/dialogs/ImagePackSettingsDialog.qml + qml/dialogs/ImagePackEditorDialog.qml qml/ui/Ripple.qml qml/ui/Spinner.qml qml/ui/animations/BlinkAnimation.qml @@ -173,6 +174,7 @@ qml/voip/VideoCall.qml qml/components/AdaptiveLayout.qml qml/components/AdaptiveLayoutElement.qml + qml/components/AvatarListTile.qml qml/components/FlatButton.qml qml/RoomMembers.qml qml/InviteDialog.qml diff --git a/src/Cache.cpp b/src/Cache.cpp index 291df053..f3f3dbb6 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -125,7 +125,7 @@ template bool containsStateUpdates(const T &e) { - return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e); + return std::visit([](const auto &ev) { return Cache::isStateEvent_; }, e); } bool diff --git a/src/Cache_p.h b/src/Cache_p.h index 5d700658..30c365a6 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -291,15 +291,9 @@ public: std::optional secret(const std::string name); template - static constexpr bool isStateEvent(const mtx::events::StateEvent &) - { - return true; - } - template - static constexpr bool isStateEvent(const mtx::events::Event &) - { - return false; - } + constexpr static bool isStateEvent_ = + std::is_same_v>, + mtx::events::StateEvent().content)>>; static int compare_state_key(const MDB_val *a, const MDB_val *b) { @@ -416,11 +410,27 @@ private: } std::visit( - [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) { - if constexpr (isStateEvent(e)) { + [&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) { + if constexpr (isStateEvent_) { eventsDb.put(txn, e.event_id, json(e).dump()); - if (e.type != EventType::Unsupported) { + if (std::is_same_v< + std::remove_cv_t>, + StateEvent>) { + if (e.type == EventType::RoomMember) + membersdb.del(txn, e.state_key, ""); + else if (e.state_key.empty()) + statesdb.del(txn, to_string(e.type)); + else + stateskeydb.del( + txn, + to_string(e.type), + json::object({ + {"key", e.state_key}, + {"id", e.event_id}, + }) + .dump()); + } else if (e.type != EventType::Unsupported) { if (e.state_key.empty()) statesdb.put( txn, to_string(e.type), json(e).dump()); diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index ab0f8152..b8648269 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -22,7 +22,14 @@ QHash infos; QQuickImageResponse * MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + auto id_ = id; + bool crop = true; + if (id.endsWith("?scale")) { + crop = false; + id_.remove("?scale"); + } + + MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize); pool.start(response); return response; } @@ -36,20 +43,24 @@ void MxcImageResponse::run() { MxcImageProvider::download( - m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) { + m_id, + m_requestedSize, + [this](QString, QSize, QImage image, QString) { if (image.isNull()) { m_error = "Failed to download image."; } else { m_image = image; } emit finished(); - }); + }, + m_crop); } void MxcImageProvider::download(const QString &id, const QSize &requestedSize, - std::function then) + std::function then, + bool crop) { std::optional encryptionInfo; auto temp = infos.find("mxc://" + id); @@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id, if (requestedSize.isValid() && !encryptionInfo) { QString fileName = - QString("%1_%2x%3_crop") + QString("%1_%2x%3_%4") .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals))) .arg(requestedSize.width()) - .arg(requestedSize.height()); + .arg(requestedSize.height()) + .arg(crop ? "crop" : "scale"); QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/media_cache", fileName); @@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id, opts.mxc_url = "mxc://" + id.toStdString(); opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1; opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1; - opts.method = "crop"; + opts.method = crop ? "crop" : "scale"; http::client()->get_thumbnail( opts, [fileInfo, requestedSize, then, id](const std::string &res, diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 7b960836..61d82852 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -19,9 +19,10 @@ class MxcImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize) : m_id(id) , m_requestedSize(requestedSize) + , m_crop(crop) { setAutoDelete(false); } @@ -37,6 +38,7 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + bool m_crop; }; class MxcImageProvider @@ -51,7 +53,8 @@ public slots: static void addEncryptionInfo(mtx::crypto::EncryptedFile info); static void download(const QString &id, const QSize &requestedSize, - std::function then); + std::function then, + bool crop = true); private: QThreadPool pool; diff --git a/src/SingleImagePackModel.cpp b/src/SingleImagePackModel.cpp index 6c508da0..d3cc8014 100644 --- a/src/SingleImagePackModel.cpp +++ b/src/SingleImagePackModel.cpp @@ -5,12 +5,18 @@ #include "SingleImagePackModel.h" #include "Cache_p.h" +#include "ChatPage.h" #include "MatrixClient.h" +#include "timeline/Permissions.h" +#include "timeline/TimelineModel.h" + +#include "Logging.h" SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent) : QAbstractListModel(parent) , roomid_(std::move(pack_.source_room)) , statekey_(std::move(pack_.state_key)) + , old_statekey_(statekey_) , pack(std::move(pack_.pack)) { if (!pack.pack) @@ -61,6 +67,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const return {}; } +bool +SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + using mtx::events::msc2545::PackUsage; + + if (hasIndex(index.row(), index.column(), index.parent())) { + auto &img = pack.images.at(shortcodes.at(index.row())); + switch (role) { + case ShortCode: { + auto newCode = value.toString().toStdString(); + + // otherwise we delete this by accident + if (pack.images.count(newCode)) + return false; + + auto tmp = img; + auto oldCode = shortcodes.at(index.row()); + pack.images.erase(oldCode); + shortcodes[index.row()] = newCode; + pack.images.insert({newCode, tmp}); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::ShortCode}); + return true; + } + case Body: + img.body = value.toString().toStdString(); + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::Body}); + return true; + case IsEmote: { + bool isEmote = value.toBool(); + bool isSticker = + img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsEmote}); + + return true; + } + case IsSticker: { + bool isEmote = + img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji(); + bool isSticker = value.toBool(); + + img.usage.set(PackUsage::Emoji, isEmote); + img.usage.set(PackUsage::Sticker, isSticker); + + if (img.usage == pack.pack->usage) + img.usage.reset(); + + emit dataChanged( + this->index(index.row()), this->index(index.row()), {Roles::IsSticker}); + + return true; + } + } + } + return false; +} + bool SingleImagePackModel::isGloballyEnabled() const { @@ -98,3 +171,111 @@ SingleImagePackModel::setGloballyEnabled(bool enabled) // emit this->globallyEnabledChanged(); }); } + +bool +SingleImagePackModel::canEdit() const +{ + if (roomid_.empty()) + return true; + else + return Permissions(QString::fromStdString(roomid_)) + .canChange(qml_mtx_events::ImagePackInRoom); +} + +void +SingleImagePackModel::setPackname(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->display_name) { + this->pack.pack->display_name = val_; + emit packnameChanged(); + } +} + +void +SingleImagePackModel::setAttribution(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->attribution) { + this->pack.pack->attribution = val_; + emit attributionChanged(); + } +} + +void +SingleImagePackModel::setAvatarUrl(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != this->pack.pack->avatar_url) { + this->pack.pack->avatar_url = val_; + emit avatarUrlChanged(); + } +} + +void +SingleImagePackModel::setStatekey(QString val) +{ + auto val_ = val.toStdString(); + if (val_ != statekey_) { + statekey_ = val_; + emit statekeyChanged(); + } +} + +void +SingleImagePackModel::setIsStickerPack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_sticker()) { + pack.pack->usage.set(PackUsage::Sticker, val); + emit isStickerPackChanged(); + } +} + +void +SingleImagePackModel::setIsEmotePack(bool val) +{ + using mtx::events::msc2545::PackUsage; + if (val != pack.pack->is_emoji()) { + pack.pack->usage.set(PackUsage::Emoji, val); + emit isEmotePackChanged(); + } +} + +void +SingleImagePackModel::save() +{ + if (roomid_.empty()) { + http::client()->put_account_data(pack, [this](mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } else { + if (old_statekey_ != statekey_) { + http::client()->send_state_event( + roomid_, + to_string(mtx::events::EventType::ImagePackInRoom), + old_statekey_, + nlohmann::json::object(), + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to delete old image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } + + http::client()->send_state_event( + roomid_, + statekey_, + pack, + [this](const mtx::responses::EventId &, mtx::http::RequestErr e) { + if (e) + ChatPage::instance()->showNotification( + tr("Failed to update image pack: {}") + .arg(QString::fromStdString(e->matrix_error.error))); + }); + } +} diff --git a/src/SingleImagePackModel.h b/src/SingleImagePackModel.h index e0c791ba..44f413c6 100644 --- a/src/SingleImagePackModel.h +++ b/src/SingleImagePackModel.h @@ -15,14 +15,18 @@ class SingleImagePackModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(QString roomid READ roomid CONSTANT) - Q_PROPERTY(QString statekey READ statekey CONSTANT) - Q_PROPERTY(QString attribution READ statekey CONSTANT) - Q_PROPERTY(QString packname READ packname CONSTANT) - Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT) - Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT) - Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT) + Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged) + Q_PROPERTY( + QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged) + Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged) + Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged) + Q_PROPERTY( + bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged) + Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged) Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY globallyEnabledChanged) + Q_PROPERTY(bool canEdit READ canEdit CONSTANT) + public: enum Roles { @@ -32,11 +36,15 @@ public: IsEmote, IsSticker, }; + Q_ENUM(Roles); SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr); QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; QString roomid() const { return QString::fromStdString(roomid_); } QString statekey() const { return QString::fromStdString(statekey_); } @@ -47,14 +55,30 @@ public: bool isEmotePack() const { return pack.pack->is_emoji(); } bool isGloballyEnabled() const; + bool canEdit() const; void setGloballyEnabled(bool enabled); + void setPackname(QString val); + void setAttribution(QString val); + void setAvatarUrl(QString val); + void setStatekey(QString val); + void setIsStickerPack(bool val); + void setIsEmotePack(bool val); + + Q_INVOKABLE void save(); + signals: void globallyEnabledChanged(); + void statekeyChanged(); + void attributionChanged(); + void packnameChanged(); + void avatarUrlChanged(); + void isEmotePackChanged(); + void isStickerPackChanged(); private: std::string roomid_; - std::string statekey_; + std::string statekey_, old_statekey_; mtx::events::msc2545::ImagePack pack; std::vector shortcodes; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index a8adf05b..10d9788d 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t) case qml_mtx_events::KeyVerificationDone: case qml_mtx_events::KeyVerificationReady: return mtx::events::EventType::RoomMessage; + //! m.image_pack, currently im.ponies.room_emotes + case qml_mtx_events::ImagePackInRoom: + return mtx::events::EventType::ImagePackRooms; + //! m.image_pack, currently im.ponies.user_emotes + case qml_mtx_events::ImagePackInAccountData: + return mtx::events::EventType::ImagePackInAccountData; + //! m.image_pack.rooms, currently im.ponies.emote_rooms + case qml_mtx_events::ImagePackRooms: + return mtx::events::EventType::ImagePackRooms; default: return mtx::events::EventType::Unsupported; }; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f62c5360..b5c8ca37 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -107,7 +107,13 @@ enum EventType KeyVerificationCancel, KeyVerificationKey, KeyVerificationDone, - KeyVerificationReady + KeyVerificationReady, + //! m.image_pack, currently im.ponies.room_emotes + ImagePackInRoom, + //! m.image_pack, currently im.ponies.user_emotes + ImagePackInAccountData, + //! m.image_pack.rooms, currently im.ponies.emote_rooms + ImagePackRooms, }; Q_ENUM_NS(EventType) mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType); -- cgit 1.5.1