From f1a23355bdd7d2fe08c5efa73ee21ece14979a7f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 9 Jan 2022 00:28:03 +0100 Subject: [PATCH] Port usersettings to qml --- resources/qml/ImageButton.qml | 2 + resources/qml/MessageInput.qml | 2 +- resources/qml/RoomList.qml | 3 +- resources/qml/Root.qml | 15 +- resources/qml/ToggleButton.qml | 2 +- resources/qml/UserSettingsPage.qml | 218 +++ .../qml/dialogs/ImagePackEditorDialog.qml | 8 +- .../qml/dialogs/ImagePackSettingsDialog.qml | 2 +- resources/qml/dialogs/RoomSettings.qml | 2 +- resources/res.qrc | 1 + src/MainWindow.cpp | 36 +- src/MainWindow.h | 5 +- src/UserSettingsPage.cpp | 1698 ++++++++++------- src/UserSettingsPage.h | 189 +- src/Utils.cpp | 2 +- src/timeline/TimelineViewManager.cpp | 4 + 16 files changed, 1330 insertions(+), 859 deletions(-) create mode 100644 resources/qml/UserSettingsPage.qml diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index c68b6cde..ffd3e920 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -16,6 +16,7 @@ AbstractButton { property color highlightColor: Nheko.colors.highlight property color buttonTextColor: Nheko.colors.buttonText property bool changeColorOnHover: true + property bool ripple: true focusPolicy: Qt.NoFocus width: 16 @@ -38,6 +39,7 @@ AbstractButton { } Ripple { + enabled: button.ripple color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 87fad633..ceb2e1ea 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -194,7 +194,7 @@ Rectangle { } room.input.send(); event.accepted = true; - } else if (event.key == Qt.Key_Tab) { + } else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) { event.accepted = true; if (popup.opened) { if (event.modifiers & Qt.ShiftModifier) diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 2f8a2d43..6e7b683f 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -678,6 +678,7 @@ Page { visible: !collapsed Layout.fillWidth: true hoverEnabled: true + ripple: false width: 22 height: 22 image: ":/icons/icons/ui/settings.svg" @@ -685,7 +686,7 @@ Page { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("User settings") Layout.margins: Nheko.paddingMedium - onClicked: Nheko.showUserSettingsPage() + onClicked: mainWindow.push(userSettingsPage); } } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index c85b641a..82025057 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -144,6 +144,14 @@ Page { } + Component { + id: userSettingsPage + + UserSettingsPage { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -353,8 +361,13 @@ Page { target: UIA } - ChatPage { + StackView { + id: mainWindow + anchors.fill: parent + initialItem: ChatPage { + //anchors.fill: parent + } } } diff --git a/resources/qml/ToggleButton.qml b/resources/qml/ToggleButton.qml index 3859891a..98950b8a 100644 --- a/resources/qml/ToggleButton.qml +++ b/resources/qml/ToggleButton.qml @@ -36,7 +36,7 @@ Switch { width: parent.height height: width radius: width / 2 - color: toggleButton.down ? "whitesmoke" : "whitesmoke" + color: toggleButton.enabled ? "whitesmoke" : "#cccccc" border.color: "#ebebeb" } diff --git a/resources/qml/UserSettingsPage.qml b/resources/qml/UserSettingsPage.qml new file mode 100644 index 00000000..025f44fa --- /dev/null +++ b/resources/qml/UserSettingsPage.qml @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "ui" +import Qt.labs.platform 1.1 as Platform +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.13 +import im.nheko 1.0 + +Rectangle { + id: userSettingsDialog + + property bool collapsed: width < 800 + color: Nheko.colors.window + + Shortcut { + sequence: StandardKey.Cancel + onActivated: userSettingsDialog.close() + } + + ScrollView { + id: scroll + + palette: Nheko.colors + ScrollBar.horizontal.visible: false + anchors.fill: parent + anchors.margins: Nheko.paddingLarge + + contentWidth: availableWidth + + Timer { + id: deadTimer + interval: 500 + } + + Connections { + target: scroll.contentItem + function onContentYChanged() { deadTimer.restart(); } + } + + + GridLayout { + id: grid + + columns: userSettingsDialog.collapsed ? 1 : 2 + rowSpacing: Nheko.paddingMedium + columnSpacing: Nheko.paddingMedium + + anchors.fill: parent + anchors.leftMargin: userSettingsDialog.collapsed ? Nheko.paddingLarge : (userSettingsDialog.width-600) * 0.4 + anchors.rightMargin: userSettingsDialog.collapsed ? Nheko.paddingLarge : (userSettingsDialog.width-600) * 0.4 + + Repeater { + model: UserSettingsModel + + delegate: Item { + required property var model + id: r + + Component.onCompleted: { + while (children.length) { + console.log("Reparenting: " + children[0]); + children[0].parent = grid; + } + } + + Label { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + color: Nheko.colors.text + text: model.name + //Layout.column: 0 + Layout.columnSpan: (model.type == UserSettingsModel.SectionTitle && !userSettingsDialog.collapsed) ? 2 : 1 + //Layout.row: model.index + Layout.minimumWidth: implicitWidth + Layout.leftMargin: model.type == UserSettingsModel.SectionTitle ? 0 : Nheko.paddingMedium + Layout.topMargin: model.type == UserSettingsModel.SectionTitle ? Nheko.paddingLarge : 0 + font.pointSize: 1.1 * fontInfo.pointSize + + HoverHandler { + id: hovered + enabled: model.description ?? false + } + ToolTip.visible: hovered.hovered && model.description + ToolTip.text: model.description ?? "" + ToolTip.delay: Nheko.tooltipDelay + } + + DelegateChooser { + id: chooser + + roleValue: model.type + Layout.alignment: Qt.AlignRight + + //Layout.column: model.type == UserSettingsModel.SectionTitle ? 0 : 1 + Layout.columnSpan: (model.type == UserSettingsModel.SectionTitle && !userSettingsDialog.collapsed) ? 2 : 1 + //Layout.row: model.index + Layout.preferredHeight: child.height + Layout.preferredWidth: Math.min(child.implicitWidth, child.width || 1000) + Layout.fillWidth: model.type == UserSettingsModel.SectionTitle + Layout.rightMargin: model.type == UserSettingsModel.SectionTitle ? 0 : Nheko.paddingMedium + + DelegateChoice { + roleValue: UserSettingsModel.Toggle + ToggleButton { + checked: model.value + onCheckedChanged: model.value = checked + enabled: model.enabled + } + } + DelegateChoice { + roleValue: UserSettingsModel.Options + ComboBox { + Layout.preferredWidth: Math.min(200, implicitWidth) + width: Math.min(200, implicitWidth) + model: r.model.values + currentIndex: r.model.value + enabled: !deadTimer.running + onCurrentIndexChanged: r.model.value = currentIndex + } + } + DelegateChoice { + roleValue: UserSettingsModel.Number + + SpinBox { + //implicitWidth: 100 + enabled: !deadTimer.running && model.enabled + from: model.valueLowerBound + to: model.valueUpperBound + stepSize: model.valueStep + value: model.value + onValueChanged: model.value = value + } + } + DelegateChoice { + roleValue: UserSettingsModel.ReadOnlyText + Text { + color: Nheko.colors.text + text: model.value + } + } + DelegateChoice { + roleValue: UserSettingsModel.SectionTitle + Item { + width: grid.width + height: fontMetrics.lineSpacing + Rectangle { + anchors.topMargin: Nheko.paddingSmall + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + color: Nheko.colors.buttonText + height: 1 + } + } + } + DelegateChoice { + roleValue: UserSettingsModel.KeyStatus + Text { + color: model.good ? "green" : Nheko.theme.error + text: model.value ? qsTr("CACHED") : qsTr("NOT CACHED") + } + } + DelegateChoice { + roleValue: UserSettingsModel.SessionKeyImportExport + RowLayout { + Button { + text: qsTr("IMPORT") + onClicked: UserSettingsModel.importSessionKeys() + } + Button { + text: qsTr("EXPORT") + onClicked: UserSettingsModel.exportSessionKeys() + } + } + } + DelegateChoice { + roleValue: UserSettingsModel.XSignKeysRequestDownload + RowLayout { + Button { + text: qsTr("DOWNLOAD") + onClicked: UserSettingsModel.downloadCrossSigningSecrets() + } + Button { + text: qsTr("REQUEST") + onClicked: UserSettingsModel.requestCrossSigningSecrets() + } + } + } + DelegateChoice { + Text { + text: model.value + } + } + } + } + } + } + } + + ImageButton { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Nheko.paddingMedium + width: Nheko.avatarSize + height: Nheko.avatarSize + image: ":/icons/icons/ui/angle-arrow-left.svg" + ToolTip.visible: hovered + ToolTip.text: qsTr("Back") + onClicked: mainWindow.pop() + } + +} + diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml index 852290f2..3ba04d94 100644 --- a/resources/qml/dialogs/ImagePackEditorDialog.qml +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -213,7 +213,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.isEmotePack - onClicked: imagePack.isEmotePack = checked + onCheckedChanged: imagePack.isEmotePack = checked Layout.alignment: Qt.AlignRight } @@ -223,7 +223,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.isStickerPack - onClicked: imagePack.isStickerPack = checked + onCheckedChanged: imagePack.isStickerPack = checked Layout.alignment: Qt.AlignRight } @@ -279,7 +279,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsEmote) - onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote) + onCheckedChanged: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsEmote) Layout.alignment: Qt.AlignRight } @@ -289,7 +289,7 @@ ApplicationWindow { ToggleButton { checked: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.IsSticker) - onClicked: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker) + onCheckedChanged: imagePack.setData(imagePack.index(currentImageIndex, 0), checked, SingleImagePackModel.IsSticker) Layout.alignment: Qt.AlignRight } diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index d6025f80..fa079855 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -185,7 +185,7 @@ ApplicationWindow { ToggleButton { ToolTip.text: qsTr("Enables this pack to be used in all rooms") checked: currentPack ? currentPack.isGloballyEnabled : false - onClicked: currentPack.isGloballyEnabled = !currentPack.isGloballyEnabled + onCheckedChanged: currentPack.isGloballyEnabled = checked Layout.alignment: Qt.AlignRight } diff --git a/resources/qml/dialogs/RoomSettings.qml b/resources/qml/dialogs/RoomSettings.qml index 0320beb7..c9f2b1a1 100644 --- a/resources/qml/dialogs/RoomSettings.qml +++ b/resources/qml/dialogs/RoomSettings.qml @@ -214,7 +214,7 @@ ApplicationWindow { id: encryptionToggle checked: roomSettings.isEncryptionEnabled - onClicked: { + onCheckedChanged: { if (roomSettings.isEncryptionEnabled) { checked = true; return ; diff --git a/resources/res.qrc b/resources/res.qrc index d825699a..660a48c7 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -86,6 +86,7 @@ qml/CommunitiesList.qml qml/RoomList.qml qml/TimelineView.qml + qml/UserSettingsPage.qml qml/Avatar.qml qml/Completer.qml qml/EncryptionIndicator.qml diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6a62e353..5bfce89e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -56,11 +56,10 @@ MainWindow::MainWindow(QWidget *parent) trayIcon_ = new TrayIcon(QStringLiteral(":/logos/nheko.svg"), this); - welcome_page_ = new WelcomePage(this); - login_page_ = new LoginPage(this); - register_page_ = new RegisterPage(this); - chat_page_ = new ChatPage(userSettings_, this); - userSettingsPage_ = new UserSettingsPage(userSettings_, this); + welcome_page_ = new WelcomePage(this); + login_page_ = new LoginPage(this); + register_page_ = new RegisterPage(this); + chat_page_ = new ChatPage(userSettings_, this); // Initialize sliding widget manager. pageStack_ = new QStackedWidget(this); @@ -68,7 +67,6 @@ MainWindow::MainWindow(QWidget *parent) pageStack_->addWidget(login_page_); pageStack_->addWidget(register_page_); pageStack_->addWidget(chat_page_); - pageStack_->addWidget(userSettingsPage_); setCentralWidget(pageStack_); @@ -93,13 +91,7 @@ MainWindow::MainWindow(QWidget *parent) showLoginPage(); }); - connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { - pageStack_->setCurrentWidget(chat_page_); - }); - - connect(userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - connect( - userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); + connect(userSettings_.get(), &UserSettings::trayChanged, trayIcon_, &TrayIcon::setVisible); connect(trayIcon_, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, @@ -109,8 +101,6 @@ MainWindow::MainWindow(QWidget *parent) connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged); - connect(chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); - connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { http::client()->set_user(res.user_id); showChatPage(); @@ -247,14 +237,8 @@ MainWindow::showChatPage() login_page_->reset(); chat_page_->bootstrap(userid, homeserver, token); - connect(cache::client(), - &Cache::databaseReady, - userSettingsPage_, - &UserSettingsPage::updateSecretStatus); - connect(cache::client(), - &Cache::secretChanged, - userSettingsPage_, - &UserSettingsPage::updateSecretStatus); + connect(cache::client(), &Cache::databaseReady, this, &MainWindow::secretsChanged); + connect(cache::client(), &Cache::secretChanged, this, &MainWindow::secretsChanged); emit reload(); } @@ -403,9 +387,3 @@ MainWindow::showRegisterPage() pageStack_->addWidget(register_page_); pageStack_->setCurrentWidget(register_page_); } - -void -MainWindow::showUserSettingsPage() -{ - pageStack_->setCurrentWidget(userSettingsPage_); -} diff --git a/src/MainWindow.h b/src/MainWindow.h index 82579937..458eb054 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -84,9 +84,6 @@ private slots: //! Show the register page in the main window. void showRegisterPage(); - //! Show user settings page. - void showUserSettingsPage(); - //! Show the chat page and start communicating with the given access token. void showChatPage(); @@ -98,6 +95,7 @@ private slots: signals: void focusChanged(const bool focused); void reload(); + void secretsChanged(); private: void showDialog(QWidget *dialog); @@ -120,7 +118,6 @@ private: QStackedWidget *pageStack_; //! The main chat area. ChatPage *chat_page_; - UserSettingsPage *userSettingsPage_; QSharedPointer userSettings_; //! Tray icon that shows the unread message count. TrayIcon *trayIcon_; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 169a6907..b6d05fb5 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -24,9 +24,11 @@ #include #include #include +#include #include "Cache.h" #include "Config.h" +#include "MainWindow.h" #include "MatrixClient.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -759,737 +761,850 @@ UserSettings::save() settings.sync(); } -HorizontalLine::HorizontalLine(QWidget *parent) - : QFrame{parent} +QHash +UserSettingsModel::roleNames() const { - setFrameShape(QFrame::HLine); - setFrameShadow(QFrame::Sunken); + static QHash roles{ + {Name, "name"}, + {Description, "description"}, + {Value, "value"}, + {Type, "type"}, + {ValueLowerBound, "valueLowerBound"}, + {ValueUpperBound, "valueUpperBound"}, + {ValueStep, "valueStep"}, + {Values, "values"}, + {Good, "good"}, + {Enabled, "enabled"}, + }; + + return roles; } -UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidget *parent) - : QWidget{parent} - , settings_{settings} +QVariant +UserSettingsModel::data(const QModelIndex &index, int role) const { - topLayout_ = new QVBoxLayout{this}; + if (index.row() >= COUNT) + return {}; - QIcon icon; - icon.addFile(QStringLiteral(":/icons/icons/ui/angle-arrow-left.svg")); + auto i = UserSettings::instance(); + if (!i) + return {}; - auto backBtn_ = new FlatButton{this}; - backBtn_->setMinimumSize(QSize(24, 24)); - backBtn_->setIcon(icon); - backBtn_->setIconSize(QSize(24, 24)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.1); - - auto versionInfo = new QLabel(QStringLiteral("%1 | %2").arg(nheko::version, nheko::build_os)); - if (QCoreApplication::applicationName() != QLatin1String("nheko")) - versionInfo->setText(versionInfo->text() + " | " + - tr("profile: %1").arg(QCoreApplication::applicationName())); - versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); - - topBarLayout_ = new QHBoxLayout; - topBarLayout_->setSpacing(0); - topBarLayout_->setContentsMargins(0, 0, 0, 0); - topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); - topBarLayout_->addStretch(1); - - formLayout_ = new QFormLayout; - - formLayout_->setLabelAlignment(Qt::AlignLeft); - formLayout_->setFormAlignment(Qt::AlignRight); - formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); - formLayout_->setHorizontalSpacing(0); - - auto general_ = new QLabel{tr("GENERAL"), this}; - general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - general_->setFont(font); - - trayToggle_ = new Toggle{this}; - startInTrayToggle_ = new Toggle{this}; - avatarCircles_ = new Toggle{this}; - useIdenticon_ = new Toggle{this}; - decryptSidebar_ = new Toggle(this); - privacyScreen_ = new Toggle{this}; - onlyShareKeysWithVerifiedUsers_ = new Toggle(this); - shareKeysWithTrustedUsers_ = new Toggle(this); - useOnlineKeyBackup_ = 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}; - animateImagesOnHover_ = 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()); - useIdenticon_->setChecked(settings_->useIdenticon()); - decryptSidebar_->setChecked(settings_->decryptSidebar()); - privacyScreen_->setChecked(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers()); - useOnlineKeyBackup_->setChecked(settings_->useOnlineKeyBackup()); - groupViewToggle_->setChecked(settings_->groupView()); - timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline()); - typingNotifications_->setChecked(settings_->typingNotifications()); - messageHoverHighlight_->setChecked(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setChecked(settings_->enlargeEmojiOnlyMessages()); - sortByImportance_->setChecked(settings_->sortByImportance()); - readReceipts_->setChecked(settings_->readReceipts()); - markdown_->setChecked(settings_->markdown()); - animateImagesOnHover_->setChecked(settings_->animateImagesOnHover()); - desktopNotifications_->setChecked(settings_->hasDesktopNotifications()); - alertOnNotification_->setChecked(settings_->hasAlertOnNotification()); - useStunServer_->setChecked(settings_->useStunServer()); - mobileMode_->setChecked(settings_->mobileMode()); - - if (!settings_->tray()) { - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); - } - - if (!settings_->privacyScreen()) { - privacyScreenTimeout_->setDisabled(true); - } - - avatarCircles_->setFixedSize(64, 48); - - auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; - uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); - uiLabel_->setAlignment(Qt::AlignBottom); - uiLabel_->setFont(font); - - for (double option = 1; option <= 3; option += 0.25) - scaleFactorCombo_->addItem(QString::number(option)); - for (double option = 6; option <= 24; option += 0.5) - fontSizeCombo_->addItem(QStringLiteral("%1 ").arg(QString::number(option))); - - QFontDatabase fontDb; - - // TODO: Is there a way to limit to just emojis, rather than - // all emoji fonts? - auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); - emojiFontSelectionCombo_->addItem(tr("Default")); - for (const auto &family : emojiFamilies) { - emojiFontSelectionCombo_->addItem(family); - } - - QString currentFont = settings_->font(); - if (currentFont != QLatin1String("default") || currentFont != QLatin1String("")) { - fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(currentFont)); - } - - emojiFontSelectionCombo_->setCurrentIndex( - emojiFontSelectionCombo_->findText(settings_->emojiFont())); - - themeCombo_ = new QComboBox{this}; - themeCombo_->addItem(QStringLiteral("Light")); - themeCombo_->addItem(QStringLiteral("Dark")); - themeCombo_->addItem(QStringLiteral("System")); - - QString themeStr = settings_->theme(); - themeStr.replace(0, 1, themeStr[0].toUpper()); - int themeIndex = themeCombo_->findText(themeStr); - themeCombo_->setCurrentIndex(themeIndex); - - timelineMaxWidthSpin_->setMinimum(0); - timelineMaxWidthSpin_->setMaximum(100'000'000); - timelineMaxWidthSpin_->setSingleStep(10); - - privacyScreenTimeout_->setMinimum(0); - privacyScreenTimeout_->setMaximum(3600); - privacyScreenTimeout_->setSingleStep(10); - - auto callsLabel = new QLabel{tr("CALLS"), this}; - callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); - callsLabel->setAlignment(Qt::AlignBottom); - callsLabel->setFont(font); - - auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; - encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); - encryptionLabel_->setAlignment(Qt::AlignBottom); - encryptionLabel_->setFont(font); - - QFont monospaceFont; - monospaceFont.setFamily(QStringLiteral("Monospace")); - monospaceFont.setStyleHint(QFont::Monospace); - monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); - - deviceIdValue_ = new QLabel{this}; - deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceIdValue_->setFont(monospaceFont); - - deviceFingerprintValue_ = new QLabel{this}; - deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); - deviceFingerprintValue_->setFont(monospaceFont); - - deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); - - backupSecretCached = new QLabel{this}; - masterSecretCached = new QLabel{this}; - selfSigningSecretCached = new QLabel{this}; - userSigningSecretCached = new QLabel{this}; - backupSecretCached->setFont(monospaceFont); - masterSecretCached->setFont(monospaceFont); - selfSigningSecretCached->setFont(monospaceFont); - userSigningSecretCached->setFont(monospaceFont); - - auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; - sessionKeysLabel->setFont(font); - sessionKeysLabel->setMargin(OptionMargin); - - auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; - auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; - - auto sessionKeysLayout = new QHBoxLayout; - sessionKeysLayout->addWidget(new QLabel{QLatin1String(""), this}, 1, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); - sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); - - auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this}; - crossSigningKeysLabel->setFont(font); - crossSigningKeysLabel->setMargin(OptionMargin); - - auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this}; - auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this}; - - auto crossSigningKeysLayout = new QHBoxLayout; - crossSigningKeysLayout->addWidget(new QLabel{QLatin1String(""), this}, 1, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight); - crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight); - - auto boxWrap = - [this, &font](QString labelText, QWidget *field, QString tooltipText = QLatin1String("")) { - auto label = new QLabel{labelText, this}; - label->setFont(font); - label->setMargin(OptionMargin); - - if (!tooltipText.isEmpty()) { - label->setToolTip(tooltipText); - } - - auto layout = new QHBoxLayout; - layout->addWidget(field, 0, Qt::AlignRight); - - formLayout_->addRow(label, layout); - }; - - formLayout_->addRow(general_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Minimize to tray"), - trayToggle_, - tr("Keep the application running in the background after closing the client window.")); - boxWrap(tr("Start in tray"), - startInTrayToggle_, - tr("Start the application in the background without showing the client window.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Circular Avatars"), - avatarCircles_, - tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle.")); - boxWrap(tr("Use identicons"), - useIdenticon_, - tr("Display an identicon instead of a letter when no avatar is set.")); - boxWrap(tr("Group's sidebar"), - groupViewToggle_, - tr("Show a column containing groups and tags next to the room list.")); - boxWrap(tr("Decrypt messages in sidebar"), - decryptSidebar_, - tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " - "encrypted chats.")); - boxWrap(tr("Privacy Screen"), - privacyScreen_, - tr("When the window loses focus, the timeline will\nbe blurred.")); - boxWrap(tr("Privacy screen timeout (in seconds [0 - 3600])"), - privacyScreenTimeout_, - tr("Set timeout (in seconds) for how long after window loses\nfocus before the screen" - " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " - "hour (3600 seconds)")); - boxWrap(tr("Show buttons in timeline"), - timelineButtonsToggle_, - tr("Show buttons to quickly reply, react or access additional options next to each " - "message.")); - boxWrap(tr("Limit width of timeline"), - timelineMaxWidthSpin_, - tr("Set the max width of messages in the timeline (in pixels). This can help " - "readability on wide screen, when Nheko is maximised")); - boxWrap(tr("Typing notifications"), - typingNotifications_, - tr("Show who is typing in a room.\nThis will also enable or disable sending typing " - "notifications to others.")); - boxWrap( - tr("Sort rooms by unreads"), - sortByImportance_, - tr("Display rooms with new messages first.\nIf this is off, the list of rooms will only " - "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms which " - "have active notifications (the small circle with a number in it) will be sorted on " - "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " - "seem to consider them as important as the other rooms.")); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Read receipts"), - readReceipts_, - tr("Show if your message was read.\nStatus is displayed next to timestamps.")); - boxWrap(tr("Send messages as Markdown"), - markdown_, - tr("Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " - "text.")); - boxWrap(tr("Play animated images only on hover"), - animateImagesOnHover_, - tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.")); - boxWrap(tr("Desktop notifications"), - desktopNotifications_, - tr("Notify about received message when the client is not currently focused.")); - boxWrap(tr("Alert on notification"), - alertOnNotification_, - tr("Show an alert when a message is received.\nThis usually causes the application " - "icon in the task bar to animate in some fashion.")); - boxWrap(tr("Highlight message on hover"), - messageHoverHighlight_, - tr("Change the background color of messages when you hover over them.")); - boxWrap(tr("Large Emoji in timeline"), - enlargeEmojiOnlyMessages_, - tr("Make font size larger if messages with only a few emojis are displayed.")); - formLayout_->addRow(uiLabel_); - formLayout_->addRow(new HorizontalLine{this}); - - boxWrap(tr("Touchscreen mode"), - mobileMode_, - tr("Will prevent text selection in the timeline to make touch scrolling easier.")); -#if !defined(Q_OS_MAC) - boxWrap(tr("Scale factor"), - scaleFactorCombo_, - tr("Change the scale factor of the whole user interface.")); -#else - scaleFactorCombo_->hide(); -#endif - boxWrap(tr("Font size"), fontSizeCombo_); - boxWrap(tr("Font Family"), fontSelectionCombo_); - -#if !defined(Q_OS_MAC) - boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); -#else - emojiFontSelectionCombo_->hide(); -#endif - - boxWrap(tr("Theme"), themeCombo_); - - formLayout_->addRow(callsLabel); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Ringtone"), - ringtoneCombo_, - tr("Set the notification sound to play when a call invite arrives")); - boxWrap(tr("Microphone"), microphoneCombo_); - boxWrap(tr("Camera"), cameraCombo_); - boxWrap(tr("Camera resolution"), cameraResolutionCombo_); - boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_); - - ringtoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - ringtoneCombo_->addItem(QStringLiteral("Mute")); - ringtoneCombo_->addItem(QStringLiteral("Default")); - ringtoneCombo_->addItem(QStringLiteral("Other...")); - const QString &ringtone = settings_->ringtone(); - if (!ringtone.isEmpty() && ringtone != QLatin1String("Mute") && - ringtone != QLatin1String("Default")) - ringtoneCombo_->addItem(ringtone); - microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents); - - boxWrap(tr("Allow fallback call assist server"), - useStunServer_, - tr("Will use turn.matrix.org as assist when your home server does not offer one.")); - - formLayout_->addRow(encryptionLabel_); - formLayout_->addRow(new HorizontalLine{this}); - boxWrap(tr("Device ID"), deviceIdValue_); - boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); - 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.")); - boxWrap(tr("Online Key Backup"), - useOnlineKeyBackup_, - tr("Download message encryption keys from and upload to the encrypted online key " - "backup.")); - formLayout_->addRow(new HorizontalLine{this}); - formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); - formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout); - - boxWrap(tr("Master signing key"), - masterSecretCached, - tr("Your most important key. You don't need to have it cached, since not caching " - "it makes it less likely it can be stolen and it is only needed to rotate your " - "other signing keys.")); - boxWrap(tr("User signing key"), - userSigningSecretCached, - tr("The key to verify other users. If it is cached, verifying a user will verify " - "all their devices.")); - boxWrap(tr("Self signing key"), - selfSigningSecretCached, - tr("The key to verify your own devices. If it is cached, verifying one of your devices " - "will mark it verified for all your other devices and for users, that have verified " - "you.")); - boxWrap(tr("Backup key"), - backupSecretCached, - tr("The key to decrypt online key backups. If it is cached, you can enable online " - "key backup to store encryption keys securely encrypted on the server.")); - // updateSecretStatus(); - - auto scrollArea_ = new QScrollArea{this}; - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - - QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); - - auto spacingAroundForm = new QHBoxLayout; - spacingAroundForm->addStretch(1); - spacingAroundForm->addLayout(formLayout_, 0); - spacingAroundForm->addStretch(1); - - auto scrollAreaContents_ = new QWidget{this}; - scrollAreaContents_->setObjectName(QStringLiteral("UserSettingScrollWidget")); - scrollAreaContents_->setLayout(spacingAroundForm); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addLayout(topBarLayout_); - topLayout_->addWidget(scrollArea_, Qt::AlignTop); - topLayout_->addStretch(1); - topLayout_->addWidget(versionInfo); - - connect(themeCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &text) { - settings_->setTheme(text.toLower()); - emit themeChanged(); - }); - connect(scaleFactorCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); - connect(fontSizeCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &size) { settings_->setFontSize(size.trimmed().toDouble()); }); - connect(fontSelectionCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); - connect(emojiFontSelectionCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); - - connect( - ringtoneCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &ringtone) { - if (ringtone == QLatin1String("Other...")) { - QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - auto filepath = QFileDialog::getOpenFileName( - this, tr("Select a file"), homeFolder, tr("All Files (*)")); - if (!filepath.isEmpty()) { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != QLatin1String("Mute") && oldSetting != QLatin1String("Default")) - ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(filepath); - ringtoneCombo_->addItem(filepath); - ringtoneCombo_->setCurrentText(filepath); - } else { - ringtoneCombo_->setCurrentText(settings_->ringtone()); - } - } else if (ringtone == QLatin1String("Mute") || ringtone == QLatin1String("Default")) { - const auto &oldSetting = settings_->ringtone(); - if (oldSetting != QLatin1String("Mute") && oldSetting != QLatin1String("Default")) - ringtoneCombo_->removeItem(ringtoneCombo_->findText(oldSetting)); - settings_->setRingtone(ringtone); - } - }); - - connect(microphoneCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString µphone) { settings_->setMicrophone(microphone); }); - - connect(cameraCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &camera) { - settings_->setCamera(camera); - std::vector resolutions = - CallDevices::instance().resolutions(camera.toStdString()); - cameraResolutionCombo_->clear(); - for (const auto &resolution : resolutions) - cameraResolutionCombo_->addItem(QString::fromStdString(resolution)); - }); - - connect(cameraResolutionCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &resolution) { - settings_->setCameraResolution(resolution); - std::vector frameRates = CallDevices::instance().frameRates( - settings_->camera().toStdString(), resolution.toStdString()); - cameraFrameRateCombo_->clear(); - for (const auto &frameRate : frameRates) - cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate)); - }); - - connect(cameraFrameRateCombo_, - static_cast(&QComboBox::currentTextChanged), - this, - [this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); }); - - connect(trayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTray(enabled); - if (enabled) { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setEnabled(true); - startInTrayToggle_->setState(false); - settings_->setStartInTray(false); - } else { - startInTrayToggle_->setChecked(false); - startInTrayToggle_->setState(false); - startInTrayToggle_->setDisabled(true); - settings_->setStartInTray(false); + if (role == Name) { + switch (index.row()) { + case Theme: + return tr("Theme"); + case ScaleFactor: + return tr("Scale factor"); + case MessageHoverHighlight: + return tr("Highlight message on hover"); + case EnlargeEmojiOnlyMessages: + return tr("Large Emoji in timeline"); + case Tray: + return tr("Minimize to tray"); + case StartInTray: + return tr("Start in tray"); + case GroupView: + return tr("Group's sidebar"); + case Markdown: + return tr("Send messages as Markdown"); + case AnimateImagesOnHover: + return tr("Play animated images only on hover"); + case TypingNotifications: + return tr("Typing notifications"); + case SortByImportance: + return tr("Sort rooms by unreads"); + case ButtonsInTimeline: + return tr("Show buttons in timeline"); + case TimelineMaxWidth: + return tr("Limit width of timeline"); + case ReadReceipts: + return tr("Read receipts"); + case DesktopNotifications: + return tr("Desktop notifications"); + case AlertOnNotification: + return tr("Alert on notification"); + case AvatarCircles: + return tr("Circular Avatars"); + case UseIdenticon: + return tr("Use identicons"); + case DecryptSidebar: + return tr("Decrypt messages in sidebar"); + case PrivacyScreen: + return tr("Privacy Screen"); + case PrivacyScreenTimeout: + return tr("Privacy screen timeout (in seconds [0 - 3600])"); + case MobileMode: + return tr("Touchscreen mode"); + case FontSize: + return tr("Font size"); + case Font: + return tr("Font Family"); + case EmojiFont: + return tr("Emoji Font Family"); + case Ringtone: + return tr("Ringtone"); + case Microphone: + return tr("Microphone"); + case Camera: + return tr("Camera"); + case CameraResolution: + return tr("Camera resolution"); + case CameraFrameRate: + return tr("Camera frame rate"); + case UseStunServer: + return tr("Allow fallback call assist server"); + case OnlyShareKeysWithVerifiedUsers: + return tr("Send encrypted messages to verified users only"); + case ShareKeysWithTrustedUsers: + return tr("Share keys with verified users and devices"); + case UseOnlineKeyBackup: + return tr("Online Key Backup"); + case Profile: + return tr("Profile"); + case UserId: + return tr("User ID"); + case AccessToken: + return tr("Accesstoken"); + case DeviceId: + return tr("Device ID"); + case DeviceFingerprint: + return tr("Device Fingerprint"); + case Homeserver: + return tr("Homeserver"); + case Version: + return tr("Version"); + case Platform: + return tr("Platform"); + case GeneralSection: + return tr("GENERAL"); + case TimelineSection: + return tr("TIMELINE"); + case SidebarSection: + return tr("SIDEBAR"); + case TraySection: + return tr("TRAY"); + case NotificationsSection: + return tr("NOTIFICATIONS"); + case VoipSection: + return tr("CALLS"); + case EncryptionSection: + return tr("ENCRYPTION"); + case LoginInfoSection: + return tr("INFO"); + case SessionKeys: + return tr("Session Keys"); + case CrossSigningSecrets: + return tr("Cross Signing Secrets"); + case OnlineBackupKey: + return tr("Online backup key"); + case SelfSigningKey: + return tr("Self signing key"); + case UserSigningKey: + return tr("User signing key"); + case MasterKey: + return tr("Master signing key"); } - emit trayOptionChanged(enabled); - }); - - connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setStartInTray(enabled); - }); - - connect(mobileMode_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMobileMode(enabled); - }); - - connect(groupViewToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setGroupView(enabled); - }); - - connect(decryptSidebar_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDecryptSidebar(enabled); - }); - - connect(privacyScreen_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setPrivacyScreen(enabled); - if (enabled) { - privacyScreenTimeout_->setEnabled(true); - } else { - privacyScreenTimeout_->setDisabled(true); - } - }); - - connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setOnlyShareKeysWithVerifiedUsers(enabled); - }); - - connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setShareKeysWithTrustedUsers(enabled); - }); - - connect(useOnlineKeyBackup_, &Toggle::toggled, this, [this](bool enabled) { - if (enabled) { - if (QMessageBox::question( - this, - tr("Enable online key backup"), - tr("The Nheko authors recommend not enabling online key backup until " - "symmetric online key backup is available. Enable anyway?")) != - QMessageBox::StandardButton::Yes) { - useOnlineKeyBackup_->setState(false); - return; + } else if (role == Value) { + switch (index.row()) { + case Theme: + return QStringList{ + QStringLiteral("light"), + QStringLiteral("dark"), + QStringLiteral("system"), } + .indexOf(i->theme()); + case ScaleFactor: + return utils::scaleFactor(); + case MessageHoverHighlight: + return i->messageHoverHighlight(); + case EnlargeEmojiOnlyMessages: + return i->enlargeEmojiOnlyMessages(); + case Tray: + return i->tray(); + case StartInTray: + return i->startInTray(); + case GroupView: + return i->groupView(); + case Markdown: + return i->markdown(); + case AnimateImagesOnHover: + return i->animateImagesOnHover(); + case TypingNotifications: + return i->typingNotifications(); + case SortByImportance: + return i->sortByImportance(); + case ButtonsInTimeline: + return i->buttonsInTimeline(); + case TimelineMaxWidth: + return i->timelineMaxWidth(); + case ReadReceipts: + return i->readReceipts(); + case DesktopNotifications: + return i->hasDesktopNotifications(); + case AlertOnNotification: + return i->hasAlertOnNotification(); + case AvatarCircles: + return i->avatarCircles(); + case UseIdenticon: + return i->useIdenticon(); + case DecryptSidebar: + return i->decryptSidebar(); + case PrivacyScreen: + return i->privacyScreen(); + case PrivacyScreenTimeout: + return i->privacyScreenTimeout(); + case MobileMode: + return i->mobileMode(); + case FontSize: + return i->fontSize(); + case Font: + return data(index, Values).toStringList().indexOf(i->font()); + case EmojiFont: + return data(index, Values).toStringList().indexOf(i->emojiFont()); + case Ringtone: { + auto v = i->ringtone(); + nhlog::ui()->critical("ringtone: {}", v.toStdString()); + if (v == QStringView(u"Mute")) + return 0; + else if (v == QStringView(u"Default")) + return 1; + else if (v == QStringView(u"Other")) + return 2; + else + return 3; } - settings_->setUseOnlineKeyBackup(enabled); - }); + case Microphone: + return data(index, Values).toStringList().indexOf(i->microphone()); + case Camera: + return data(index, Values).toStringList().indexOf(i->camera()); + case CameraResolution: + return data(index, Values).toStringList().indexOf(i->cameraResolution()); + case CameraFrameRate: + return data(index, Values).toStringList().indexOf(i->cameraFrameRate()); + case UseStunServer: + return i->useStunServer(); + case OnlyShareKeysWithVerifiedUsers: + return i->onlyShareKeysWithVerifiedUsers(); + case ShareKeysWithTrustedUsers: + return i->shareKeysWithTrustedUsers(); + case UseOnlineKeyBackup: + return i->useOnlineKeyBackup(); + case Profile: + return i->profile().isEmpty() ? tr("Default") : i->profile(); + case UserId: + return i->userId(); + case AccessToken: + return i->accessToken(); + case DeviceId: + return i->deviceId(); + case DeviceFingerprint: + return utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519); + case Homeserver: + return i->homeserver(); + case Version: + return QString::fromStdString(nheko::version); + case Platform: + return QString::fromStdString(nheko::build_os); + case OnlineBackupKey: + return cache::secret(mtx::secret_storage::secrets::megolm_backup_v1).has_value(); + case SelfSigningKey: + return cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) + .has_value(); + case UserSigningKey: + return cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing) + .has_value(); + case MasterKey: + return cache::secret(mtx::secret_storage::secrets::cross_signing_master).has_value(); + } + } else if (role == Description) { + switch (index.row()) { + case Theme: + case Font: + case EmojiFont: + return {}; + case Microphone: + return tr("Set the notification sound to play when a call invite arrives"); + case Camera: + case CameraResolution: + case CameraFrameRate: + case Ringtone: + return {}; + case TimelineMaxWidth: + return tr("Set the max width of messages in the timeline (in pixels). This can help " + "readability on wide screen, when Nheko is maximised"); + case PrivacyScreenTimeout: + return tr( + "Set timeout (in seconds) for how long after window loses\nfocus before the screen" + " will be blurred.\nSet to 0 to blur immediately after focus loss. Max value of 1 " + "hour (3600 seconds)"); + case FontSize: + return {}; + case MessageHoverHighlight: + return tr("Change the background color of messages when you hover over them."); + case EnlargeEmojiOnlyMessages: + return tr("Make font size larger if messages with only a few emojis are displayed."); + case Tray: + return tr( + "Keep the application running in the background after closing the client window."); + case StartInTray: + return tr("Start the application in the background without showing the client window."); + case GroupView: + return tr("Show a column containing groups and tags next to the room list."); + case Markdown: + return tr( + "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " + "text."); + case AnimateImagesOnHover: + return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them."); + case TypingNotifications: + return tr( + "Show who is typing in a room.\nThis will also enable or disable sending typing " + "notifications to others."); + case SortByImportance: + return tr( + "Display rooms with new messages first.\nIf this is off, the list of rooms will only " + "be sorted by the timestamp of the last message in a room.\nIf this is on, rooms " + "which " + "have active notifications (the small circle with a number in it) will be sorted on " + "top. Rooms, that you have muted, will still be sorted by timestamp, since you don't " + "seem to consider them as important as the other rooms."); + case ButtonsInTimeline: + return tr( + "Show buttons to quickly reply, react or access additional options next to each " + "message."); + case ReadReceipts: + return tr("Show if your message was read.\nStatus is displayed next to timestamps."); + case DesktopNotifications: + return tr("Notify about received message when the client is not currently focused."); + case AlertOnNotification: + return tr( + "Show an alert when a message is received.\nThis usually causes the application " + "icon in the task bar to animate in some fashion."); + case AvatarCircles: + return tr( + "Change the appearance of user avatars in chats.\nOFF - square, ON - Circle."); + case UseIdenticon: + return tr("Display an identicon instead of a letter when no avatar is set."); + case DecryptSidebar: + return tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in " + "encrypted chats."); + case PrivacyScreen: + return tr("When the window loses focus, the timeline will\nbe blurred."); + case MobileMode: + return tr( + "Will prevent text selection in the timeline to make touch scrolling easier."); + case ScaleFactor: + return tr("Change the scale factor of the whole user interface."); + case UseStunServer: + return tr( + "Will use turn.matrix.org as assist when your home server does not offer one."); + case OnlyShareKeysWithVerifiedUsers: + return tr("Requires a user to be verified to send encrypted messages to them. This " + "improves safety but makes E2EE more tedious."); + case ShareKeysWithTrustedUsers: + return 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."); + case UseOnlineKeyBackup: + return tr( + "Download message encryption keys from and upload to the encrypted online key " + "backup."); + case Profile: + case UserId: + case AccessToken: + case DeviceId: + case DeviceFingerprint: + case Homeserver: + case Version: + case Platform: + case GeneralSection: + case TimelineSection: + case SidebarSection: + case TraySection: + case NotificationsSection: + case VoipSection: + case EncryptionSection: + case LoginInfoSection: + case SessionKeys: + case CrossSigningSecrets: + return {}; + case OnlineBackupKey: + return tr( + "The key to decrypt online key backups. If it is cached, you can enable online " + "key backup to store encryption keys securely encrypted on the server."); + case SelfSigningKey: + return tr( + "The key to verify your own devices. If it is cached, verifying one of your devices " + "will mark it verified for all your other devices and for users, that have verified " + "you."); + case UserSigningKey: + return tr( + "The key to verify other users. If it is cached, verifying a user will verify " + "all their devices."); + case MasterKey: + return tr( + "Your most important key. You don't need to have it cached, since not caching " + "it makes it less likely it can be stolen and it is only needed to rotate your " + "other signing keys."); + } + } else if (role == Type) { + switch (index.row()) { + case Theme: + case Font: + case EmojiFont: + case Microphone: + case Camera: + case CameraResolution: + case CameraFrameRate: + case Ringtone: + return Options; + case TimelineMaxWidth: + case PrivacyScreenTimeout: + case FontSize: + case ScaleFactor: + return Number; + case MessageHoverHighlight: + case EnlargeEmojiOnlyMessages: + case Tray: + case StartInTray: + case GroupView: + case Markdown: + case AnimateImagesOnHover: + case TypingNotifications: + case SortByImportance: + case ButtonsInTimeline: + case ReadReceipts: + case DesktopNotifications: + case AlertOnNotification: + case AvatarCircles: + case UseIdenticon: + case DecryptSidebar: + case PrivacyScreen: + case MobileMode: + case UseStunServer: + case OnlyShareKeysWithVerifiedUsers: + case ShareKeysWithTrustedUsers: + case UseOnlineKeyBackup: + return Toggle; + case Profile: + case UserId: + case AccessToken: + case DeviceId: + case DeviceFingerprint: + case Homeserver: + case Version: + case Platform: + return ReadOnlyText; + case GeneralSection: + case TimelineSection: + case SidebarSection: + case TraySection: + case NotificationsSection: + case VoipSection: + case EncryptionSection: + case LoginInfoSection: + return SectionTitle; + case SessionKeys: + return SessionKeyImportExport; + case CrossSigningSecrets: + return XSignKeysRequestDownload; + case OnlineBackupKey: + case SelfSigningKey: + case UserSigningKey: + case MasterKey: + return KeyStatus; + } + } else if (role == ValueLowerBound) { + switch (index.row()) { + case TimelineMaxWidth: + return 0; + case PrivacyScreenTimeout: + return 0; + case FontSize: + return 8.0; + case ScaleFactor: + return 1.0; + } + } else if (role == ValueUpperBound) { + switch (index.row()) { + case TimelineMaxWidth: + return 20000; + case PrivacyScreenTimeout: + return 3600; + case FontSize: + return 24.0; + case ScaleFactor: + return 3.0; + } + } else if (role == ValueStep) { + switch (index.row()) { + case TimelineMaxWidth: + return 20; + case PrivacyScreenTimeout: + return 10; + case FontSize: + return 0.5; + case ScaleFactor: + return .25; + } + } else if (role == Values) { + auto vecToList = [](const std::vector &vec) { + QStringList l; + for (const auto &d : vec) + l.push_back(QString::fromStdString(d)); + return l; + }; + static QFontDatabase fontDb; - connect(avatarCircles_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAvatarCircles(enabled); - }); + switch (index.row()) { + case Theme: + return QStringList{ + QStringLiteral("Light"), + QStringLiteral("Dark"), + QStringLiteral("System"), + }; + case Microphone: + return vecToList(CallDevices::instance().names(false, i->microphone().toStdString())); + case Camera: + return vecToList(CallDevices::instance().names(true, i->camera().toStdString())); + case CameraResolution: + return vecToList(CallDevices::instance().resolutions(i->camera().toStdString())); + case CameraFrameRate: + return vecToList(CallDevices::instance().frameRates( + i->camera().toStdString(), i->cameraResolution().toStdString())); - if (JdenticonProvider::isAvailable()) - connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setUseIdenticon(enabled); - }); - else - useIdenticon_->setDisabled(true); + case Font: + return fontDb.families(); + case EmojiFont: + return fontDb.families(QFontDatabase::WritingSystem::Symbol); + case Ringtone: + QStringList l{ + QStringLiteral("Mute"), + QStringLiteral("Default"), + QStringLiteral("Other"), + }; + if (!l.contains(i->ringtone())) + l.push_back(i->ringtone()); + return l; + } + } else if (role == Good) { + switch (index.row()) { + case OnlineBackupKey: + return cache::secret(mtx::secret_storage::secrets::megolm_backup_v1).has_value(); + case SelfSigningKey: + return cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) + .has_value(); + case UserSigningKey: + return cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing) + .has_value(); + case MasterKey: + return true; + } + } else if (role == Enabled) { + switch (index.row()) { + case StartInTray: + return i->tray(); + case PrivacyScreenTimeout: + return i->privacyScreen(); + case UseIdenticon: + return JdenticonProvider::isAvailable(); + default: + return true; + } + } - connect( - markdown_, &Toggle::toggled, this, [this](bool enabled) { settings_->setMarkdown(enabled); }); - - connect(animateImagesOnHover_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAnimateImagesOnHover(enabled); - }); - - connect(typingNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setTypingNotifications(enabled); - }); - - connect(sortByImportance_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setSortByImportance(enabled); - }); - - connect(timelineButtonsToggle_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setButtonsInTimeline(enabled); - }); - - connect(readReceipts_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setReadReceipts(enabled); - }); - - connect(desktopNotifications_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setDesktopNotifications(enabled); - }); - - connect(alertOnNotification_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setAlertOnNotification(enabled); - }); - - connect(messageHoverHighlight_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setMessageHoverHighlight(enabled); - }); - - connect(enlargeEmojiOnlyMessages_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setEnlargeEmojiOnlyMessages(enabled); - }); - - connect(useStunServer_, &Toggle::toggled, this, [this](bool enabled) { - settings_->setUseStunServer(enabled); - }); - - connect(timelineMaxWidthSpin_, - qOverload(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setTimelineMaxWidth(newValue); }); - - connect(privacyScreenTimeout_, - qOverload(&QSpinBox::valueChanged), - this, - [this](int newValue) { settings_->setPrivacyScreenTimeout(newValue); }); - - connect( - sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); - - connect( - sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); - - connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() { - olm::request_cross_signing_keys(); - }); - - connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() { - olm::download_cross_signing_keys(); - }); - - connect(backBtn_, &QPushButton::clicked, this, [this]() { - settings_->save(); - emit moveBack(); - }); + return {}; } -void -UserSettingsPage::showEvent(QShowEvent *) +bool +UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role) { - // FIXME macOS doesn't show the full option unless a space is added. - utils::restoreCombobox(fontSizeCombo_, QString::number(settings_->fontSize()) + " "); - utils::restoreCombobox(scaleFactorCombo_, QString::number(utils::scaleFactor())); - utils::restoreCombobox(themeCombo_, settings_->theme()); - utils::restoreCombobox(ringtoneCombo_, settings_->ringtone()); + static QFontDatabase fontDb; - trayToggle_->setState(settings_->tray()); - startInTrayToggle_->setState(settings_->startInTray()); - groupViewToggle_->setState(settings_->groupView()); - decryptSidebar_->setState(settings_->decryptSidebar()); - privacyScreen_->setState(settings_->privacyScreen()); - onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers()); - shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers()); - useOnlineKeyBackup_->setState(settings_->useOnlineKeyBackup()); - avatarCircles_->setState(settings_->avatarCircles()); - typingNotifications_->setState(settings_->typingNotifications()); - sortByImportance_->setState(settings_->sortByImportance()); - timelineButtonsToggle_->setState(settings_->buttonsInTimeline()); - mobileMode_->setState(settings_->mobileMode()); - readReceipts_->setState(settings_->readReceipts()); - markdown_->setState(settings_->markdown()); - desktopNotifications_->setState(settings_->hasDesktopNotifications()); - alertOnNotification_->setState(settings_->hasAlertOnNotification()); - messageHoverHighlight_->setState(settings_->messageHoverHighlight()); - enlargeEmojiOnlyMessages_->setState(settings_->enlargeEmojiOnlyMessages()); - deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); - timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth()); - privacyScreenTimeout_->setValue(settings_->privacyScreenTimeout()); + auto i = UserSettings::instance(); + if (role == Value) { + switch (index.row()) { + case Theme: { + if (value == 0) { + i->setTheme("light"); + return true; + } else if (value == 1) { + i->setTheme("dark"); + return true; + } else if (value == 2) { + i->setTheme("system"); + return true; + } else + return false; + } + case MessageHoverHighlight: { + if (value.userType() == QMetaType::Bool) { + i->setMessageHoverHighlight(value.toBool()); + return true; + } else + return false; + } + case ScaleFactor: { + if (value.canConvert()) { + utils::setScaleFactor(static_cast(value.toDouble())); + return true; + } else + return false; + } + case EnlargeEmojiOnlyMessages: { + if (value.userType() == QMetaType::Bool) { + i->setEnlargeEmojiOnlyMessages(value.toBool()); + return true; + } else + return false; + } + case Tray: { + if (value.userType() == QMetaType::Bool) { + i->setTray(value.toBool()); + return true; + } else + return false; + } + case StartInTray: { + if (value.userType() == QMetaType::Bool) { + i->setStartInTray(value.toBool()); + return true; + } else + return false; + } + case GroupView: { + if (value.userType() == QMetaType::Bool) { + i->setGroupView(value.toBool()); + return true; + } else + return false; + } + case Markdown: { + if (value.userType() == QMetaType::Bool) { + i->setMarkdown(value.toBool()); + return true; + } else + return false; + } + case AnimateImagesOnHover: { + if (value.userType() == QMetaType::Bool) { + i->setAnimateImagesOnHover(value.toBool()); + return true; + } else + return false; + } + case TypingNotifications: { + if (value.userType() == QMetaType::Bool) { + i->setTypingNotifications(value.toBool()); + return true; + } else + return false; + } + case SortByImportance: { + if (value.userType() == QMetaType::Bool) { + i->setSortByImportance(value.toBool()); + return true; + } else + return false; + } + case ButtonsInTimeline: { + if (value.userType() == QMetaType::Bool) { + i->setButtonsInTimeline(value.toBool()); + return true; + } else + return false; + } + case TimelineMaxWidth: { + if (value.userType() == QMetaType::Int) { + i->setTimelineMaxWidth(value.toInt()); + return true; + } else + return false; + } + case ReadReceipts: { + if (value.userType() == QMetaType::Bool) { + i->setReadReceipts(value.toBool()); + return true; + } else + return false; + } + case DesktopNotifications: { + if (value.userType() == QMetaType::Bool) { + i->setDesktopNotifications(value.toBool()); + return true; + } else + return false; + } + case AlertOnNotification: { + if (value.userType() == QMetaType::Bool) { + i->setAlertOnNotification(value.toBool()); + return true; + } else + return false; + } + case AvatarCircles: { + if (value.userType() == QMetaType::Bool) { + i->setAvatarCircles(value.toBool()); + return true; + } else + return false; + } + case UseIdenticon: { + if (value.userType() == QMetaType::Bool) { + i->setUseIdenticon(value.toBool()); + return true; + } else + return false; + } + case DecryptSidebar: { + if (value.userType() == QMetaType::Bool) { + i->setDecryptSidebar(value.toBool()); + return true; + } else + return false; + } + return i->decryptSidebar(); + case PrivacyScreen: { + if (value.userType() == QMetaType::Bool) { + i->setPrivacyScreen(value.toBool()); + return true; + } else + return false; + } + case PrivacyScreenTimeout: { + if (value.userType() == QMetaType::Int) { + i->setPrivacyScreenTimeout(value.toInt()); + return true; + } else + return false; + } + case MobileMode: { + if (value.userType() == QMetaType::Bool) { + i->setMobileMode(value.toBool()); + return true; + } else + return false; + } + case FontSize: { + if (value.userType() == QMetaType::Double) { + i->setFontSize(value.toBool()); + return true; + } else + return false; + } + case Font: { + if (value.userType() == QMetaType::Int) { + i->setFontFamily(fontDb.families().at(value.toInt())); + return true; + } else + return false; + } + case EmojiFont: { + if (value.userType() == QMetaType::QString) { + i->setFontFamily( + fontDb.families(QFontDatabase::WritingSystem::Symbol).at(value.toInt())); + return true; + } else + return false; + } + case Ringtone: { + if (value.userType() == QMetaType::Int) { + int ringtone = value.toInt(); - auto mics = CallDevices::instance().names(false, settings_->microphone().toStdString()); - microphoneCombo_->clear(); - for (const auto &m : mics) - microphoneCombo_->addItem(QString::fromStdString(m)); - - auto cameraResolution = settings_->cameraResolution(); - auto cameraFrameRate = settings_->cameraFrameRate(); - - auto cameras = CallDevices::instance().names(true, settings_->camera().toStdString()); - cameraCombo_->clear(); - for (const auto &c : cameras) - cameraCombo_->addItem(QString::fromStdString(c)); - - utils::restoreCombobox(cameraResolutionCombo_, cameraResolution); - utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate); - - useStunServer_->setState(settings_->useStunServer()); - - deviceFingerprintValue_->setText( - utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); + // setRingtone is called twice, because updating the list breaks the set value, + // because it does not exist yet! + if (ringtone == 2) { + QString homeFolder = + QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + auto filepath = QFileDialog::getOpenFileName( + MainWindow::instance(), tr("Select a file"), homeFolder, tr("All Files (*)")); + if (!filepath.isEmpty()) { + i->setRingtone(filepath); + i->setRingtone(filepath); + } + } else if (ringtone == 0) { + i->setRingtone(QStringLiteral("Mute")); + i->setRingtone(QStringLiteral("Mute")); + } else if (ringtone == 1) { + i->setRingtone(QStringLiteral("Default")); + i->setRingtone(QStringLiteral("Default")); + } + return true; + } + return false; + } + case Microphone: { + if (value.userType() == QMetaType::Int) { + i->setMicrophone(data(index, Values).toStringList().at(value.toInt())); + return true; + } else + return false; + } + case Camera: { + if (value.userType() == QMetaType::Int) { + i->setCamera(data(index, Values).toStringList().at(value.toInt())); + return true; + } else + return false; + } + case CameraResolution: { + if (value.userType() == QMetaType::Int) { + i->setCameraResolution(data(index, Values).toStringList().at(value.toInt())); + return true; + } else + return false; + } + case CameraFrameRate: { + if (value.userType() == QMetaType::Int) { + i->setCameraFrameRate(data(index, Values).toStringList().at(value.toInt())); + return true; + } else + return false; + } + case UseStunServer: { + if (value.userType() == QMetaType::Bool) { + i->setUseStunServer(value.toBool()); + return true; + } else + return false; + } + case OnlyShareKeysWithVerifiedUsers: { + if (value.userType() == QMetaType::Bool) { + i->setOnlyShareKeysWithVerifiedUsers(value.toBool()); + return true; + } else + return false; + } + case ShareKeysWithTrustedUsers: { + if (value.userType() == QMetaType::Bool) { + i->setShareKeysWithTrustedUsers(value.toBool()); + return true; + } else + return false; + } + case UseOnlineKeyBackup: { + if (value.userType() == QMetaType::Bool) { + i->setUseOnlineKeyBackup(value.toBool()); + return true; + } else + return false; + } + } + } + return false; } void -UserSettingsPage::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.initFrom(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -UserSettingsPage::importSessionKeys() +UserSettingsModel::importSessionKeys() { const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, QLatin1String("")); + const QString fileName = QFileDialog::getOpenFileName( + MainWindow::instance(), tr("Open Sessions File"), homeFolder, QLatin1String("")); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); + QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString()); return; } @@ -1497,7 +1612,7 @@ UserSettingsPage::importSessionKeys() auto payload = std::string(bin.data(), bin.size()); bool ok; - auto password = QInputDialog::getText(this, + auto password = QInputDialog::getText(MainWindow::instance(), tr("File Password"), tr("Enter the passphrase to decrypt the file:"), QLineEdit::Password, @@ -1507,7 +1622,8 @@ UserSettingsPage::importSessionKeys() return; if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + QMessageBox::warning( + MainWindow::instance(), tr("Error"), tr("The password cannot be empty")); return; } @@ -1515,16 +1631,15 @@ UserSettingsPage::importSessionKeys() auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); cache::importSessionKeys(std::move(sessions)); } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); + QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what()); } } - void -UserSettingsPage::exportSessionKeys() +UserSettingsModel::exportSessionKeys() { // Open password dialog. bool ok; - auto password = QInputDialog::getText(this, + auto password = QInputDialog::getText(MainWindow::instance(), tr("File Password"), tr("Enter passphrase to encrypt your session keys:"), QLineEdit::Password, @@ -1534,18 +1649,19 @@ UserSettingsPage::exportSessionKeys() return; if (password.isEmpty()) { - QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); + QMessageBox::warning( + MainWindow::instance(), tr("Error"), tr("The password cannot be empty")); return; } // Open file dialog to save the file. const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); - const QString fileName = - QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), homeFolder); + const QString fileName = QFileDialog::getSaveFileName( + MainWindow::instance(), tr("File to save the exported session keys"), homeFolder); QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::warning(this, tr("Error"), file.errorString()); + QMessageBox::warning(MainWindow::instance(), tr("Error"), file.errorString()); return; } @@ -1563,31 +1679,135 @@ UserSettingsPage::exportSessionKeys() out << prefix << newline << b64 << newline << suffix << newline; file.close(); } catch (const std::exception &e) { - QMessageBox::warning(this, tr("Error"), e.what()); + QMessageBox::warning(MainWindow::instance(), tr("Error"), e.what()); } } - void -UserSettingsPage::updateSecretStatus() +UserSettingsModel::requestCrossSigningSecrets() { - QString ok = QStringLiteral("QLabel { color : #00cc66; }"); - QString notSoOk = QStringLiteral("QLabel { color : #ff9933; }"); - - auto updateLabel = [&ok, ¬SoOk](QLabel *label, const std::string &secretName) { - if (cache::secret(secretName)) { - label->setStyleSheet(ok); - label->setText(tr("CACHED")); - } else { - if (secretName == mtx::secret_storage::secrets::cross_signing_master) - label->setStyleSheet(ok); - else - label->setStyleSheet(notSoOk); - label->setText(tr("NOT CACHED")); - } - }; - - updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master); - updateLabel(userSigningSecretCached, mtx::secret_storage::secrets::cross_signing_user_signing); - updateLabel(selfSigningSecretCached, mtx::secret_storage::secrets::cross_signing_self_signing); - updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1); + olm::request_cross_signing_keys(); +} +void +UserSettingsModel::downloadCrossSigningSecrets() +{ + olm::download_cross_signing_keys(); +} + +UserSettingsModel::UserSettingsModel(QObject *p) + : QAbstractListModel(p) +{ + auto s = UserSettings::instance(); + connect(s.get(), &UserSettings::themeChanged, this, [this]() { + emit dataChanged(index(Theme), index(Theme), {Value}); + }); + connect(s.get(), &UserSettings::mobileModeChanged, this, [this]() { + emit dataChanged(index(MobileMode), index(MobileMode), {Value}); + }); + + connect(s.get(), &UserSettings::fontChanged, this, [this]() { + emit dataChanged(index(Font), index(Font), {Value}); + }); + connect(s.get(), &UserSettings::fontSizeChanged, this, [this]() { + emit dataChanged(index(FontSize), index(FontSize), {Value}); + }); + connect(s.get(), &UserSettings::emojiFontChanged, this, [this]() { + emit dataChanged(index(EmojiFont), index(EmojiFont), {Value}); + }); + connect(s.get(), &UserSettings::avatarCirclesChanged, this, [this]() { + emit dataChanged(index(AvatarCircles), index(AvatarCircles), {Value}); + }); + connect(s.get(), &UserSettings::useIdenticonChanged, this, [this]() { + emit dataChanged(index(UseIdenticon), index(UseIdenticon), {Value}); + }); + connect(s.get(), &UserSettings::privacyScreenChanged, this, [this]() { + emit dataChanged(index(PrivacyScreen), index(PrivacyScreen), {Value}); + emit dataChanged(index(PrivacyScreenTimeout), index(PrivacyScreenTimeout), {Enabled}); + }); + connect(s.get(), &UserSettings::privacyScreenTimeoutChanged, this, [this]() { + emit dataChanged(index(PrivacyScreenTimeout), index(PrivacyScreenTimeout), {Value}); + }); + + connect(s.get(), &UserSettings::timelineMaxWidthChanged, this, [this]() { + emit dataChanged(index(TimelineMaxWidth), index(TimelineMaxWidth), {Value}); + }); + connect(s.get(), &UserSettings::messageHoverHighlightChanged, this, [this]() { + emit dataChanged(index(MessageHoverHighlight), index(MessageHoverHighlight), {Value}); + }); + connect(s.get(), &UserSettings::enlargeEmojiOnlyMessagesChanged, this, [this]() { + emit dataChanged(index(EnlargeEmojiOnlyMessages), index(EnlargeEmojiOnlyMessages), {Value}); + }); + connect(s.get(), &UserSettings::animateImagesOnHoverChanged, this, [this]() { + emit dataChanged(index(AnimateImagesOnHover), index(AnimateImagesOnHover), {Value}); + }); + connect(s.get(), &UserSettings::typingNotificationsChanged, this, [this]() { + emit dataChanged(index(TypingNotifications), index(TypingNotifications), {Value}); + }); + connect(s.get(), &UserSettings::readReceiptsChanged, this, [this]() { + emit dataChanged(index(ReadReceipts), index(ReadReceipts), {Value}); + }); + connect(s.get(), &UserSettings::buttonInTimelineChanged, this, [this]() { + emit dataChanged(index(ButtonsInTimeline), index(ButtonsInTimeline), {Value}); + }); + connect(s.get(), &UserSettings::markdownChanged, this, [this]() { + emit dataChanged(index(Markdown), index(Markdown), {Value}); + }); + + connect(s.get(), &UserSettings::groupViewStateChanged, this, [this]() { + emit dataChanged(index(GroupView), index(GroupView), {Value}); + }); + connect(s.get(), &UserSettings::roomSortingChanged, this, [this]() { + emit dataChanged(index(SortByImportance), index(SortByImportance), {Value}); + }); + connect(s.get(), &UserSettings::decryptSidebarChanged, this, [this]() { + emit dataChanged(index(DecryptSidebar), index(DecryptSidebar), {Value}); + }); + + connect(s.get(), &UserSettings::trayChanged, this, [this]() { + emit dataChanged(index(Tray), index(Tray), {Value}); + emit dataChanged(index(StartInTray), index(StartInTray), {Enabled}); + }); + connect(s.get(), &UserSettings::startInTrayChanged, this, [this]() { + emit dataChanged(index(StartInTray), index(StartInTray), {Value}); + }); + + connect(s.get(), &UserSettings::desktopNotificationsChanged, this, [this]() { + emit dataChanged(index(DesktopNotifications), index(DesktopNotifications), {Value}); + }); + connect(s.get(), &UserSettings::alertOnNotificationChanged, this, [this]() { + emit dataChanged(index(AlertOnNotification), index(AlertOnNotification), {Value}); + }); + + connect(s.get(), &UserSettings::useStunServerChanged, this, [this]() { + emit dataChanged(index(UseStunServer), index(UseStunServer), {Value}); + }); + connect(s.get(), &UserSettings::microphoneChanged, this, [this]() { + emit dataChanged(index(Microphone), index(Microphone), {Value, Values}); + }); + connect(s.get(), &UserSettings::cameraChanged, this, [this]() { + emit dataChanged(index(Camera), index(Camera), {Value, Values}); + }); + connect(s.get(), &UserSettings::cameraResolutionChanged, this, [this]() { + emit dataChanged(index(CameraResolution), index(CameraResolution), {Value, Values}); + }); + connect(s.get(), &UserSettings::cameraFrameRateChanged, this, [this]() { + emit dataChanged(index(CameraFrameRate), index(CameraFrameRate), {Value, Values}); + }); + connect(s.get(), &UserSettings::ringtoneChanged, this, [this]() { + emit dataChanged(index(Ringtone), index(Ringtone), {Values, Value}); + }); + + connect(s.get(), &UserSettings::onlyShareKeysWithVerifiedUsersChanged, this, [this]() { + emit dataChanged( + index(OnlyShareKeysWithVerifiedUsers), index(OnlyShareKeysWithVerifiedUsers), {Value}); + }); + connect(s.get(), &UserSettings::shareKeysWithTrustedUsersChanged, this, [this]() { + emit dataChanged( + index(ShareKeysWithTrustedUsers), index(ShareKeysWithTrustedUsers), {Value}); + }); + connect(s.get(), &UserSettings::useOnlineKeyBackupChanged, this, [this]() { + emit dataChanged(index(UseOnlineKeyBackup), index(UseOnlineKeyBackup), {Value}); + }); + connect(MainWindow::instance(), &MainWindow::secretsChanged, this, [this]() { + emit dataChanged(index(OnlineBackupKey), index(MasterKey), {Value, Good}); + }); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index e9e41d8c..bcf34655 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -353,89 +354,125 @@ private: static QSharedPointer instance_; }; -class HorizontalLine : public QFrame +class UserSettingsModel : public QAbstractListModel { Q_OBJECT -public: - HorizontalLine(QWidget *parent = nullptr); -}; + enum Indices + { + GeneralSection, + Theme, + MobileMode, +#ifndef Q_OS_MAC + ScaleFactor, +#endif + Font, + FontSize, + EmojiFont, + AvatarCircles, + UseIdenticon, + PrivacyScreen, + PrivacyScreenTimeout, -class UserSettingsPage : public QWidget -{ - Q_OBJECT + TimelineSection, + TimelineMaxWidth, + MessageHoverHighlight, + EnlargeEmojiOnlyMessages, + AnimateImagesOnHover, + TypingNotifications, + ReadReceipts, + ButtonsInTimeline, + Markdown, + + SidebarSection, + GroupView, + SortByImportance, + DecryptSidebar, + + TraySection, + Tray, + StartInTray, + + NotificationsSection, + DesktopNotifications, + AlertOnNotification, + + VoipSection, + UseStunServer, + Microphone, + Camera, + CameraResolution, + CameraFrameRate, + Ringtone, + + EncryptionSection, + OnlyShareKeysWithVerifiedUsers, + ShareKeysWithTrustedUsers, + SessionKeys, + UseOnlineKeyBackup, + OnlineBackupKey, + SelfSigningKey, + UserSigningKey, + MasterKey, + CrossSigningSecrets, + DeviceId, + DeviceFingerprint, + + LoginInfoSection, + UserId, + Homeserver, + Profile, + Version, + Platform, + COUNT, + // hidden for now + AccessToken, +#ifdef Q_OS_MAC + ScaleFactor, +#endif + }; public: - UserSettingsPage(QSharedPointer settings, QWidget *parent = nullptr); + enum Types + { + Toggle, + ReadOnlyText, + Options, + Number, + SectionTitle, + SectionBar, + KeyStatus, + SessionKeyImportExport, + XSignKeysRequestDownload, + }; + Q_ENUM(Types); -protected: - void showEvent(QShowEvent *event) override; - void paintEvent(QPaintEvent *event) override; + enum Roles + { + Name, + Description, + Value, + Type, + ValueLowerBound, + ValueUpperBound, + ValueStep, + Values, + Good, + Enabled, + }; -signals: - void moveBack(); - void trayOptionChanged(bool value); - void themeChanged(); - void decryptSidebarChanged(); + UserSettingsModel(QObject *parent = nullptr); + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)COUNT; + } + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; -public slots: - void updateSecretStatus(); - -private slots: - void importSessionKeys(); - void exportSessionKeys(); - -private: - // Layouts - QVBoxLayout *topLayout_; - QHBoxLayout *topBarLayout_; - QFormLayout *formLayout_; - - // Shared settings object. - QSharedPointer settings_; - - Toggle *trayToggle_; - Toggle *startInTrayToggle_; - Toggle *groupViewToggle_; - Toggle *timelineButtonsToggle_; - Toggle *typingNotifications_; - Toggle *messageHoverHighlight_; - Toggle *enlargeEmojiOnlyMessages_; - Toggle *sortByImportance_; - Toggle *readReceipts_; - Toggle *markdown_; - Toggle *animateImagesOnHover_; - Toggle *desktopNotifications_; - Toggle *alertOnNotification_; - Toggle *avatarCircles_; - Toggle *useIdenticon_; - Toggle *useStunServer_; - Toggle *decryptSidebar_; - Toggle *privacyScreen_; - QSpinBox *privacyScreenTimeout_; - Toggle *shareKeysWithTrustedUsers_; - Toggle *onlyShareKeysWithVerifiedUsers_; - Toggle *useOnlineKeyBackup_; - Toggle *mobileMode_; - QLabel *deviceFingerprintValue_; - QLabel *deviceIdValue_; - QLabel *backupSecretCached; - QLabel *masterSecretCached; - QLabel *selfSigningSecretCached; - QLabel *userSigningSecretCached; - - QComboBox *themeCombo_; - QComboBox *scaleFactorCombo_; - QComboBox *fontSizeCombo_; - QFontComboBox *fontSelectionCombo_; - QComboBox *emojiFontSelectionCombo_; - QComboBox *ringtoneCombo_; - QComboBox *microphoneCombo_; - QComboBox *cameraCombo_; - QComboBox *cameraResolutionCombo_; - QComboBox *cameraFrameRateCombo_; - - QSpinBox *timelineMaxWidthSpin_; - - int sideMargin_ = 0; + Q_INVOKABLE void importSessionKeys(); + Q_INVOKABLE void exportSessionKeys(); + Q_INVOKABLE void requestCrossSigningSecrets(); + Q_INVOKABLE void downloadCrossSigningSecrets(); }; diff --git a/src/Utils.cpp b/src/Utils.cpp index 75b3c8f2..a9cfde22 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -394,7 +394,7 @@ utils::humanReadableFingerprint(const QString &ed25519) QString fingerprint; for (int i = 0; i < ed25519.length(); i = i + 4) { fingerprint.append(QStringView(ed25519).mid(i, 4)); - if (i > 0 && i % 16 == 12) + if (i > 0 && i == 20) fingerprint.append('\n'); else if (i < ed25519.length()) fingerprint.append(' '); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 7234caa9..e689e2fa 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -259,6 +259,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Nheko(); }); + qmlRegisterSingletonType( + "im.nheko", 1, 0, "UserSettingsModel", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new UserSettingsModel(); + }); qmlRegisterSingletonInstance("im.nheko", 1, 0, "VerificationManager", verificationManager_); qmlRegisterSingletonInstance("im.nheko", 1, 0, "Presence", presenceEmitter); qmlRegisterSingletonType(