diff --git a/TODO.md b/TODO.md
index d91767c7..1bec8b87 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,19 +1,27 @@
# TODO
+- sessions page size
+- menu click-through padding to close it easily
+- clear listview checked on message clear
+- unregister popup/menu when destroyed without being closed
+- flickshortcuts
+- Account: wait until accountInfo available
+- avatar upload/change component
+- show scrollbars for a few secs if there's content to scroll on beginning
+- can leave room with a reason?
+- field/area focus line in popups weird
+- use new nio `restore_login()`
+
## Refactoring
-- Rewrite account settings using `HTabbedContainer`
- - Use new default/reset controls system
- - Display name field text should be colored
-
-- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach,
- be more declarative
-
- Reorder QML object declarations,
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
## Issues
+- Bottom focus line for an `HTextArea` inside a `ScrollView` is invisible,
+ put the background on `ScrollView` instead?
+
- Don't send typing notification when switching to a room where the composer
has preloaded text
@@ -21,9 +29,6 @@
the marker will only be updated for accounts that have already received
it (server lag)
-- Popups can't be scrolled when not enough height to show all
-- `TextArea`s in Popups grow past window height instead of being scrollable
-
- Jumping between accounts (clicking in account bar or alt+(Shift+)N) is
laggy with hundreds of rooms in between
- On startup, if a room's last event is a membership change,
diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py
index 7bdb54cf..45481d9a 100644
--- a/src/backend/matrix_client.py
+++ b/src/backend/matrix_client.py
@@ -12,7 +12,6 @@ import sys
import traceback
from contextlib import suppress
from copy import deepcopy
-from dataclasses import asdict
from datetime import datetime, timedelta
from functools import partial
from pathlib import Path
@@ -1261,8 +1260,11 @@ class MatrixClient(nio.AsyncClient):
async def devices_info(self) -> List[Dict[str, Any]]:
"""Get list of devices and their info for our user."""
- def get_trust(device_id: str) -> str:
- # Returns "verified", "blacklisted", "ignored" or "unset"
+ def get_type(device_id: str) -> str:
+ # Return "current", "verified", "blacklisted", "ignored" or "unset"
+
+ if device_id == self.device_id:
+ return "current"
if device_id not in self.olm.device_store[self.user_id]:
return "unset"
@@ -1274,19 +1276,27 @@ class MatrixClient(nio.AsyncClient):
{
"id": device.id,
"display_name": device.display_name or "",
- "last_seen_ip": device.last_seen_ip or "",
+ "last_seen_ip": (device.last_seen_ip or "").strip(" -"),
"last_seen_date": device.last_seen_date or ZeroDate,
"last_seen_country": "",
- "trusted": get_trust(device.id) == "verified",
- "blacklisted": get_trust(device.id) == "blacklisted",
+ "type": get_type(device.id),
}
for device in (await self.devices()).devices
]
+ # Reversed due to sorted(reverse=True) call below
+ types_order = {
+ "current": 4,
+ "unset": 3,
+ "verified": 2,
+ "ignored": 1,
+ "blacklisted": 0,
+ }
+
+ # Sort by type, then by descending date
return sorted(
devices,
- # The current device will always be first
- key = lambda d: (d["id"] == self.device_id, d["last_seen_date"]),
+ key = lambda d: (types_order[d["type"]], d["last_seen_date"]),
reverse = True,
)
diff --git a/src/gui/Base/ButtonLayout/ApplyButton.qml b/src/gui/Base/ButtonLayout/ApplyButton.qml
index 89c8bc05..148c8d4e 100644
--- a/src/gui/Base/ButtonLayout/ApplyButton.qml
+++ b/src/gui/Base/ButtonLayout/ApplyButton.qml
@@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
import ".."
HButton {
- implicitHeight: theme.baseElementsHeight
text: qsTr("Apply")
icon.name: "apply"
icon.color: theme.colors.positiveBackground
+ Layout.preferredHeight: theme.baseElementsHeight
Layout.fillWidth: true
}
diff --git a/src/gui/Base/ButtonLayout/CancelButton.qml b/src/gui/Base/ButtonLayout/CancelButton.qml
index fa53c69e..0728ced9 100644
--- a/src/gui/Base/ButtonLayout/CancelButton.qml
+++ b/src/gui/Base/ButtonLayout/CancelButton.qml
@@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
import ".."
HButton {
- implicitHeight: theme.baseElementsHeight
text: qsTr("Cancel")
icon.name: "cancel"
icon.color: theme.colors.negativeBackground
+ Layout.preferredHeight: theme.baseElementsHeight
Layout.fillWidth: true
}
diff --git a/src/gui/Base/ButtonLayout/OtherButton.qml b/src/gui/Base/ButtonLayout/OtherButton.qml
new file mode 100644
index 00000000..525477a9
--- /dev/null
+++ b/src/gui/Base/ButtonLayout/OtherButton.qml
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import ".."
+
+HButton {
+ Layout.preferredHeight: theme.baseElementsHeight
+ Layout.fillWidth: true
+}
diff --git a/src/gui/Base/HBox.qml b/src/gui/Base/HBox.qml
index 4d5846c9..861caf09 100644
--- a/src/gui/Base/HBox.qml
+++ b/src/gui/Base/HBox.qml
@@ -3,34 +3,17 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
-Rectangle {
- id: box
- color: theme.controls.box.background
- radius: theme.controls.box.radius
- implicitWidth: theme.controls.box.defaultWidth
- implicitHeight: childrenRect.height
+HFlickableColumnPage {
+ implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
+ implicitHeight: Math.min(parent.height, flickable.contentHeight)
- Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
- Keys.onEnterPressed: Keys.onReturnPressed(event)
+ // XXX
+ // Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
+ // Keys.onEnterPressed: Keys.onReturnPressed(event)
-
- property alias buttonModel: buttonRepeater.model
- property var buttonCallbacks: []
- property string focusButton: ""
- property string clickButtonOnEnter: ""
-
- property bool fillAvailableHeight: false
-
- property HButton firstButton: null
-
- default property alias body: interfaceBody.data
-
-
- function enterClickButton() {
- for (let i = 0; i < buttonModel.length; i++) {
- const btn = buttonRepeater.itemAt(i)
- if (btn.enabled && btn.name === clickButtonOnEnter) btn.clicked()
- }
+ background: Rectangle {
+ color: theme.controls.box.background
+ radius: theme.controls.box.radius
}
@@ -41,86 +24,6 @@ Rectangle {
overshoot: 3
}
- HColumnLayout {
- id: mainColumn
- width: parent.width
-
- Binding on height {
- value: box.height
- when: box.fillAvailableHeight
- }
-
- HColumnLayout {
- id: interfaceBody
- spacing: theme.spacing * 1.5
-
- Layout.margins: spacing
- }
-
- HGridLayout {
- id: buttonGrid
- visible: buttonModel.length > 0
- flow: width >= buttonRepeater.summedImplicitWidth ?
- GridLayout.LeftToRight : GridLayout.TopToBottom
-
- HRepeater {
- id: buttonRepeater
- model: []
-
- onItemAdded: if (index === 0 && box)
- box.firstButton = buttonRepeater.itemAt(0)
-
- onItemRemoved: if (index === 0 && box)
- box.firstButton = null
-
- HButton {
- id: button
- text: modelData.text
- icon.name: modelData.iconName || ""
- icon.color: modelData.iconColor || (
- name === "ok" || name === "apply" || name === "retry" ?
- theme.colors.positiveBackground :
-
- name === "cancel" ?
- theme.colors.negativeBackground :
-
- theme.icons.colorize
- )
-
- enabled:
- modelData.enabled === undefined ?
- true : modelData.enabled
-
- loading: modelData.loading || false
-
- disableWhileLoading:
- modelData.disableWhileLoading === undefined ?
- true : modelData.disableWhileLoading
-
- onClicked: buttonCallbacks[name](button)
-
- Keys.onLeftPressed: previous.forceActiveFocus()
- Keys.onUpPressed: previous.forceActiveFocus()
- Keys.onRightPressed: next.forceActiveFocus()
- Keys.onDownPressed: next.forceActiveFocus()
-
- Component.onCompleted:
- if (name === focusButton) forceActiveFocus()
-
- Layout.fillWidth: true
- Layout.preferredHeight: theme.baseElementsHeight
-
-
- property string name: modelData.name
-
- property Item next: buttonRepeater.itemAt(
- utils.numberWrapAt(index + 1, buttonRepeater.count),
- )
- property Item previous: buttonRepeater.itemAt(
- utils.numberWrapAt(index - 1, buttonRepeater.count),
- )
- }
- }
- }
- }
+ Behavior on implicitWidth { HNumberAnimation {} }
+ Behavior on implicitHeight { HNumberAnimation {} }
}
diff --git a/src/gui/Base/HButtonContent.qml b/src/gui/Base/HButtonContent.qml
index 3dc7a831..0da9fb1f 100644
--- a/src/gui/Base/HButtonContent.qml
+++ b/src/gui/Base/HButtonContent.qml
@@ -6,7 +6,6 @@ import QtQuick.Layouts 1.12
HRowLayout {
id: buttonContent
- implicitHeight: theme.baseElementsHeight
spacing: button.spacing
opacity: loading ? theme.loadingElementsOpacity :
enabled ? 1 : theme.disabledElementsOpacity
diff --git a/src/gui/Base/HCheckBox.qml b/src/gui/Base/HCheckBox.qml
index a892822d..1b307c45 100644
--- a/src/gui/Base/HCheckBox.qml
+++ b/src/gui/Base/HCheckBox.qml
@@ -7,7 +7,7 @@ import QtQuick.Layouts 1.12
CheckBox {
id: box
checked: defaultChecked
- spacing: theme.spacing
+ spacing: contentItem.visible ? theme.spacing : 0
padding: 0
indicator: Rectangle {
@@ -33,21 +33,26 @@ CheckBox {
HIcon {
anchors.centerIn: parent
dimension: parent.width - 2
- svgName: "check-mark"
colorize: theme.controls.checkBox.checkIconColorize
+ svgName:
+ box.checkState === Qt.PartiallyChecked ?
+ "check-mark-partial" :
+ "check-mark"
- scale: box.checked ? 1 : 0
+ scale: box.checkState === Qt.Unchecked ? 0 : 1
Behavior on scale {
HNumberAnimation {
overshoot: 4
easing.type: Easing.InOutBack
+ factor: 0.5
}
}
}
}
contentItem: HColumnLayout {
+ visible: mainText.text || subtitleText.text
opacity: box.enabled ? 1 : theme.disabledElementsOpacity
HLabel {
diff --git a/src/gui/Base/HColumnPage.qml b/src/gui/Base/HColumnPage.qml
index f640b7d2..9903acb8 100644
--- a/src/gui/Base/HColumnPage.qml
+++ b/src/gui/Base/HColumnPage.qml
@@ -7,10 +7,16 @@ HPage {
default property alias columnData: column.data
+ property alias column: column
+
+
+ implicitWidth: theme.controls.box.defaultWidth
+ contentHeight: column.childrenRect.height
HColumnLayout {
id: column
anchors.fill: parent
+ spacing: theme.spacing * 1.5
}
}
diff --git a/src/gui/Base/HFlickableColumnPage.qml b/src/gui/Base/HFlickableColumnPage.qml
index 6ae05a21..de497161 100644
--- a/src/gui/Base/HFlickableColumnPage.qml
+++ b/src/gui/Base/HFlickableColumnPage.qml
@@ -5,6 +5,9 @@ import "../ShortcutBundles"
HPage {
id: page
+ implicitWidth: theme.controls.box.defaultWidth
+ contentHeight:
+ flickable.contentHeight + flickable.topMargin + flickable.bottomMargin
default property alias columnData: column.data
@@ -19,9 +22,14 @@ HPage {
HFlickable {
id: flickable
anchors.fill: parent
- clip: true
contentWidth: parent.width
- contentHeight: column.childrenRect.height + column.padding * 2
+ contentHeight: column.implicitHeight
+ clip: true
+
+ topMargin: theme.spacing
+ bottomMargin: topMargin
+ leftMargin: topMargin
+ rightMargin: topMargin
FlickShortcuts {
id: flickShortcuts
@@ -31,13 +39,9 @@ HPage {
HColumnLayout {
id: column
- x: padding
- y: padding
- width: flickable.width - padding * 2
- height: flickable.height - padding * 2
-
- property int padding:
- page.currentSpacing < theme.spacing ? 0 : page.currentSpacing
+ width:
+ flickable.width - flickable.leftMargin - flickable.rightMargin
+ spacing: theme.spacing * 1.5
}
}
diff --git a/src/gui/Base/HLabeledItem.qml b/src/gui/Base/HLabeledItem.qml
index 70a39a62..c81ef800 100644
--- a/src/gui/Base/HLabeledItem.qml
+++ b/src/gui/Base/HLabeledItem.qml
@@ -9,7 +9,7 @@ HColumnLayout {
default property alias insideData: itemHolder.data
- readonly property Item item: itemHolder.visibleChildren[0]
+ readonly property Item item: itemHolder.children[0]
readonly property alias label: label
readonly property alias errorLabel: errorLabel
readonly property alias toolTip: toolTip
diff --git a/src/gui/Base/HMenuSeparator.qml b/src/gui/Base/HMenuSeparator.qml
new file mode 100644
index 00000000..3853d97a
--- /dev/null
+++ b/src/gui/Base/HMenuSeparator.qml
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+
+MenuSeparator {
+ id: separator
+ padding: 0
+ contentItem: Item {
+ implicitHeight: theme.spacing
+ }
+}
diff --git a/src/gui/Base/HPage.qml b/src/gui/Base/HPage.qml
index 419319f1..379861f4 100644
--- a/src/gui/Base/HPage.qml
+++ b/src/gui/Base/HPage.qml
@@ -12,7 +12,11 @@ Page {
property int currentSpacing:
useVariableSpacing ?
- Math.min(theme.spacing * width / 400, theme.spacing) :
+ Math.min(
+ theme.spacing * width / 400,
+ theme.spacing * height / 400,
+ theme.spacing,
+ ) :
theme.spacing
diff --git a/src/gui/Base/HPageHeader.qml b/src/gui/Base/HPageHeader.qml
deleted file mode 100644
index 36041609..00000000
--- a/src/gui/Base/HPageHeader.qml
+++ /dev/null
@@ -1,50 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-import QtQuick 2.12
-import QtQuick.Layouts 1.12
-
-Rectangle {
- width: page.width
- height: show ? theme.baseElementsHeight : 0
- visible: height > 0
- color: theme.controls.header.background
-
-
- property HPage page: parent
- property bool show: mainUI.mainPane.collapse
-
-
- Behavior on height { HNumberAnimation {} }
-
- HRowLayout {
- anchors.fill: parent
-
- HButton {
- id: goToMainPaneButton
- padded: false
- backgroundColor: "transparent"
- icon.name: "go-back-to-main-pane"
- toolTip.text: qsTr("Go back to main pane")
-
- onClicked: mainUI.mainPane.toggleFocus()
-
- Layout.preferredWidth: theme.baseElementsHeight
- Layout.fillHeight: true
- }
-
- HLabel {
- text: page.title
- elide: Text.ElideRight
- horizontalAlignment: Qt.AlignHCenter
- verticalAlignment: Qt.AlignVCenter
-
- Layout.fillWidth: true
- Layout.fillHeight: true
- }
-
- Item {
- Layout.preferredWidth: goToMainPaneButton.width
- Layout.fillHeight: true
- }
- }
-}
diff --git a/src/gui/Base/HPopup.qml b/src/gui/Base/HPopup.qml
index 2f05b5fe..711eced4 100644
--- a/src/gui/Base/HPopup.qml
+++ b/src/gui/Base/HPopup.qml
@@ -6,12 +6,15 @@ import CppUtils 0.1
Popup {
id: popup
- anchors.centerIn: Overlay.overlay
modal: true
focus: true
padding: 0
margins: theme.spacing
+ // FIXME: Qt 5.15: `anchors.centerIn: Overlay.overlay` + transition broken
+ x: (parent.width - width) / 2
+ y: (parent.height - height) / 2
+
enter: Transition {
HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 }
}
diff --git a/src/gui/Base/HTabContainer.qml b/src/gui/Base/HTabContainer.qml
deleted file mode 100644
index 3fd51bec..00000000
--- a/src/gui/Base/HTabContainer.qml
+++ /dev/null
@@ -1,39 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-import QtQuick 2.12
-import QtQuick.Controls 2.12
-import QtQuick.Layouts 1.12
-
-HColumnLayout {
- Layout.alignment: Qt.AlignCenter
- Layout.fillWidth: false
- Layout.fillHeight: false
- Layout.maximumWidth: parent.width
-
- property alias tabIndex: tabBar.currentIndex
- property alias tabModel: tabRepeater.model
- default property alias data: swipeView.contentData
-
- HTabBar {
- id: tabBar
-
- Layout.fillWidth: true
-
- Repeater {
- id: tabRepeater
- HTabButton { text: modelData }
- }
- }
-
- SwipeView {
- id: swipeView
- clip: true
- currentIndex: tabBar.currentIndex
- interactive: false
-
- Layout.fillWidth: true
-
- Behavior on implicitWidth { HNumberAnimation {} }
- Behavior on implicitHeight { HNumberAnimation {} }
- }
-}
diff --git a/src/gui/Base/HTabbedBox.qml b/src/gui/Base/HTabbedBox.qml
new file mode 100644
index 00000000..932512e4
--- /dev/null
+++ b/src/gui/Base/HTabbedBox.qml
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+
+HPage {
+ default property alias swipeViewData: swipeView.contentData
+
+
+ contentWidth:
+ Math.max(swipeView.contentWidth, theme.controls.box.defaultWidth)
+
+ header: HTabBar {}
+
+ background: Rectangle {
+ color: theme.controls.box.background
+ radius: theme.controls.box.radius
+ }
+
+ HNumberAnimation on scale {
+ running: true
+ from: 0
+ to: 1
+ overshoot: 3
+ }
+
+ Behavior on implicitWidth { HNumberAnimation {} }
+ Behavior on implicitHeight { HNumberAnimation {} }
+
+ Binding {
+ target: header
+ property: "currentIndex"
+ value: swipeView.currentIndex
+ }
+
+ SwipeView {
+ id: swipeView
+ anchors.fill: parent
+ clip: true
+ currentIndex: header.currentIndex
+
+ onCurrentItemChanged: currentItem.takeFocus()
+ }
+}
diff --git a/src/gui/Dialogs/ExportKeys.qml b/src/gui/Dialogs/ExportKeys.qml
index 8a6bac35..784825ff 100644
--- a/src/gui/Dialogs/ExportKeys.qml
+++ b/src/gui/Dialogs/ExportKeys.qml
@@ -37,8 +37,8 @@ HFileDialogOpener {
PasswordPopup {
id: exportPasswordPopup
- details.text: qsTr("Passphrase to protect this file:")
- okText: qsTr("Export")
+ summary.text: qsTr("Passphrase to protect this file:")
+ validateButton.text: qsTr("Export")
onAcceptedPasswordChanged: exportKeys(file, acceptedPassword)
diff --git a/src/gui/Dialogs/ImportKeys.qml b/src/gui/Dialogs/ImportKeys.qml
index afc8a812..42c2a46d 100644
--- a/src/gui/Dialogs/ImportKeys.qml
+++ b/src/gui/Dialogs/ImportKeys.qml
@@ -16,17 +16,16 @@ HFileDialogOpener {
property string userId: ""
- property bool importing: false
property Future importFuture: null
PasswordPopup {
id: importPasswordPopup
- details.text:
- importing ?
+ summary.text:
+ importFuture ?
qsTr("This might take a while...") :
qsTr("Passphrase used to protect this file:")
- okText: qsTr("Import")
+ validateButton.text: qsTr("Import")
onClosed: if (importFuture) importFuture.cancel()
@@ -35,13 +34,10 @@ HFileDialogOpener {
function verifyPassword(pass, callback) {
- importing = true
-
const call = py.callClientCoro
const path = file.toString().replace(/^file:\/\//, "")
importFuture = call(userId, "import_keys", [path, pass], () => {
- importing = false
importFuture = null
callback(true)
@@ -78,7 +74,7 @@ HFileDialogOpener {
Binding on closePolicy {
value: Popup.CloseOnEscape
- when: importing
+ when: importFuture
}
}
}
diff --git a/src/gui/Pages/AccountSettings/Account.qml b/src/gui/Pages/AccountSettings/Account.qml
new file mode 100644
index 00000000..30681a9d
--- /dev/null
+++ b/src/gui/Pages/AccountSettings/Account.qml
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import "../.."
+import "../../Base"
+import "../../Base/ButtonLayout"
+import "../../Dialogs"
+
+HFlickableColumnPage {
+ id: page
+
+
+ property string userId
+ readonly property QtObject account: ModelStore.get("accounts").find(userId)
+
+
+ function takeFocus() {
+ nameField.item.forceActiveFocus()
+ }
+
+ function applyChanges() {
+ if (nameField.item.changed) {
+ saveButton.nameChangeRunning = true
+
+ py.callClientCoro(
+ userId, "set_displayname", [nameField.item.text], () => {
+ py.callClientCoro(userId, "update_own_profile", [], () => {
+ saveButton.nameChangeRunning = false
+ })
+ }
+ )
+ }
+
+ if (aliasField.item.changed) {
+ window.settings.writeAliases[userId] = aliasField.item.text
+ window.settingsChanged()
+ }
+
+ if (avatar.changed) {
+ saveButton.avatarChangeRunning = true
+
+ const path =
+ Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "")
+
+ py.callClientCoro(userId, "set_avatar_from_file", [path], () => {
+ py.callClientCoro(userId, "update_own_profile", [], () => {
+ saveButton.avatarChangeRunning = false
+ })
+ }, (errType, [httpCode]) => {
+ console.error("Avatar upload failed:", httpCode, errType)
+ saveButton.avatarChangeRunning = false
+ })
+ }
+ }
+
+ function cancel() {
+ nameField.item.reset()
+ aliasField.item.reset()
+ fileDialog.selectedFile = ""
+ fileDialog.file = ""
+ }
+
+
+ footer: ButtonLayout {
+ ApplyButton {
+ id: saveButton
+
+ property bool nameChangeRunning: false
+ property bool avatarChangeRunning: false
+
+ disableWhileLoading: false
+ loading: nameChangeRunning || avatarChangeRunning
+ enabled:
+ avatar.changed ||
+ nameField.item.changed ||
+ (aliasField.item.changed && ! aliasField.alreadyTakenBy)
+
+ onClicked: applyChanges()
+ }
+
+ CancelButton {
+ enabled: saveButton.enabled && ! saveButton.loading
+ onClicked: cancel()
+ }
+ }
+
+ Keys.onEscapePressed: cancel()
+
+
+ HUserAvatar {
+ property bool changed: Boolean(sourceOverride)
+
+ id: avatar
+ userId: page.userId
+ displayName: nameField.item.text
+ mxc: account.avatar_url
+ toolTipMxc: ""
+ sourceOverride: fileDialog.selectedFile || fileDialog.file
+
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ // Layout.preferredWidth: 256 * theme.uiScale
+ Layout.preferredHeight: width
+
+ Rectangle {
+ z: 10
+ visible: opacity > 0
+ opacity: ! fileDialog.dialog.visible &&
+ ((! avatar.mxc && ! avatar.changed) || avatar.hovered) ?
+ 1 : 0
+
+ anchors.fill: parent
+ color: utils.hsluv(
+ 0, 0, 0, (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7,
+ )
+
+ Behavior on opacity { HNumberAnimation {} }
+ Behavior on color { HColorAnimation {} }
+
+ HoverHandler { id: overlayHover }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton
+ cursorShape:
+ overlayHover.hovered ?
+ Qt.PointingHandCursor : Qt.ArrowCursor
+ }
+
+ HColumnLayout {
+ anchors.centerIn: parent
+ spacing: currentSpacing
+ width: parent.width
+
+ HIcon {
+ svgName: "upload-avatar"
+ colorize: (! avatar.mxc && overlayHover.hovered) ?
+ theme.colors.accentText : theme.icons.colorize
+ dimension: avatar.width / 3
+
+ Layout.alignment: Qt.AlignCenter
+ }
+
+ Item { Layout.preferredHeight: theme.spacing }
+
+ HLabel {
+ text: avatar.mxc ?
+ qsTr("Change profile picture") :
+ qsTr("Upload profile picture")
+
+ color: (! avatar.mxc && overlayHover.hovered) ?
+ theme.colors.accentText : theme.colors.brightText
+ Behavior on color { HColorAnimation {} }
+
+ font.pixelSize: theme.fontSize.small
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Qt.AlignHCenter
+
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ HFileDialogOpener {
+ id: fileDialog
+ fileType: HFileDialogOpener.FileType.Images
+ dialog.title: qsTr("Select profile picture for %1")
+ .arg(account.display_name)
+ }
+ }
+
+ HLabel {
+ text: qsTr("User ID:
%1")
+ .arg(utils.coloredNameHtml(userId, userId, userId))
+ textFormat: Text.StyledText
+ wrapMode: Text.Wrap
+ lineHeight: 1.1
+
+ Layout.fillWidth: true
+ }
+
+ HLabeledItem {
+ id: nameField
+ label.text: qsTr("Display name:")
+
+ Layout.fillWidth: true
+
+ HTextField {
+ width: parent.width
+ defaultText: account.display_name
+ maximumLength: 255
+
+ // TODO: Qt 5.14+: use a Binding enabled when text not empty
+ color: utils.nameColor(text)
+
+ onAccepted: applyChanges()
+ }
+ }
+
+ HLabeledItem {
+ readonly property var aliases: window.settings.writeAliases
+ readonly property string currentAlias: aliases[userId] || ""
+
+ readonly property string alreadyTakenBy: {
+ if (! item.text) return ""
+
+ for (const [id, idAlias] of Object.entries(aliases))
+ if (id !== userId && idAlias === item.text) return id
+
+ return ""
+ }
+
+
+ id: aliasField
+
+ label.text: qsTr("Composer alias:")
+
+ errorLabel.text:
+ alreadyTakenBy ?
+ qsTr("Taken by %1").arg(alreadyTakenBy) :
+ ""
+
+ toolTip.text: qsTr(
+ "From any chat, start a message with specified alias " +
+ "followed by a space to type and send as this " +
+ "account.\n" +
+ "The account must have permission to talk in the room.\n"+
+ "To ignore the alias when typing, prepend it with a space."
+ )
+
+ Layout.fillWidth: true
+
+ HTextField {
+ width: parent.width
+ error: aliasField.alreadyTakenBy !== ""
+ onAccepted: applyChanges()
+ defaultText: aliasField.currentAlias
+ placeholderText: qsTr("e.g. %1").arg((
+ nameField.item.text ||
+ account.display_name ||
+ userId.substring(1)
+ )[0])
+ }
+ }
+}
diff --git a/src/gui/Pages/AccountSettings/AccountSettings.qml b/src/gui/Pages/AccountSettings/AccountSettings.qml
index cda105ce..ce7a0cea 100644
--- a/src/gui/Pages/AccountSettings/AccountSettings.qml
+++ b/src/gui/Pages/AccountSettings/AccountSettings.qml
@@ -6,55 +6,26 @@ import QtQuick.Layouts 1.12
import "../.."
import "../../Base"
-HFlickableColumnPage {
- id: accountSettings
- title: qsTr("Account settings")
- header: HPageHeader {}
+HPage {
+ id: page
- property int avatarPreferredSize: 256 * theme.uiScale
-
- property string userId: ""
-
- readonly property bool ready:
- accountInfo !== null && accountInfo.profile_updated > new Date(1)
-
- readonly property QtObject accountInfo:
- ModelStore.get("accounts").find(userId)
-
- property string headerName: ready ? accountInfo.display_name : userId
+ property string userId
- HSpacer {}
+ HTabbedBox {
+ anchors.centerIn: parent
+ width: Math.min(implicitWidth, page.availableWidth)
+ height: Math.min(implicitHeight, page.availableHeight)
- Repeater {
- id: repeater
- model: ["Profile.qml", "ImportExportKeys.qml"]
-
- Rectangle {
- color: ready ? theme.controls.box.background : "transparent"
- Behavior on color { HColorAnimation {} }
-
- Layout.alignment: Qt.AlignCenter
- Layout.topMargin: index > 0 ? theme.spacing : 0
- Layout.bottomMargin: index < repeater.count - 1 ? theme.spacing : 0
-
- Layout.maximumWidth: Math.min(parent.width, 640)
- Layout.preferredWidth:
- pageLoader.isWide ? parent.width : avatarPreferredSize
-
- Layout.preferredHeight: childrenRect.height
-
- HLoader {
- anchors.centerIn: parent
- width: ready ? parent.width : 96
- source: ready ?
- modelData :
- (modelData === "Profile.qml" ?
- "../../Base/HBusyIndicator.qml" : "")
- }
+ header: HTabBar {
+ HTabButton { text: qsTr("Account") }
+ HTabButton { text: qsTr("Encryption") }
+ HTabButton { text: qsTr("Sessions") }
}
- }
- HSpacer {}
+ Account { userId: page.userId }
+ Encryption { userId: page.userId }
+ Sessions { userId: page.userId }
+ }
}
diff --git a/src/gui/Pages/AccountSettings/DeviceDelegate.qml b/src/gui/Pages/AccountSettings/DeviceDelegate.qml
new file mode 100644
index 00000000..1aa4732c
--- /dev/null
+++ b/src/gui/Pages/AccountSettings/DeviceDelegate.qml
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import "../../Base"
+import "../../Base/ButtonLayout"
+import "../../Base/HTile"
+
+HTile {
+ id: device
+
+
+ property HListView view
+
+
+ backgroundColor: "transparent"
+ compact: false
+
+ leftPadding: theme.spacing * 2
+ rightPadding: 0
+
+ contentItem: ContentRow {
+ tile: device
+ spacing: 0
+
+ HCheckBox {
+ id: checkBox
+ checked: view.checked[model.id] || false
+ onClicked: view.toggleCheck(model.index)
+ }
+
+ HColumnLayout {
+ Layout.leftMargin: theme.spacing
+
+ HRowLayout {
+ spacing: theme.spacing
+
+ TitleLabel {
+ text: model.display_name || qsTr("Unnamed")
+ }
+
+ TitleRightInfoLabel {
+ tile: device
+ text: utils.smartFormatDate(model.last_seen_date)
+ }
+ }
+
+ SubtitleLabel {
+ tile: device
+ font.family: theme.fontFamily.mono
+ text:
+ model.last_seen_ip ?
+ model.id + " " + model.last_seen_ip :
+ model.id
+ }
+ }
+
+ HButton {
+ icon.name: "device-action-menu"
+ toolTip.text: qsTr("Rename, verify or sign out")
+ backgroundColor: "transparent"
+ onClicked: contextMenuLoader.active = true
+
+ Layout.fillHeight: true
+ }
+ }
+
+ contextMenu: HMenu {
+ id: actionMenu
+ implicitWidth: Math.min(320 * theme.uiScale, window.width)
+ onOpened: nameField.forceActiveFocus()
+
+ HLabeledItem {
+ width: parent.width
+ label.topPadding: theme.spacing / 2
+ label.text: qsTr("Public display name:")
+ label.horizontalAlignment: Qt.AlignHCenter
+
+ HTextField {
+ id: nameField
+ width: parent.width
+ defaultText: model.display_name
+ horizontalAlignment: Qt.AlignHCenter
+ }
+ }
+
+ HMenuSeparator {}
+
+ HLabeledItem {
+ width: parent.width
+ label.text: qsTr("Actions:")
+ label.horizontalAlignment: Qt.AlignHCenter
+
+ ButtonLayout {
+ width: parent.width
+
+ ApplyButton {
+ enabled:
+ model.type !== "current" && model.type !== "verified"
+ text: qsTr("Verify")
+ icon.name: "device-verify"
+ }
+
+ CancelButton {
+ text: qsTr("Sign out")
+ icon.name: "device-delete"
+ }
+ }
+ }
+ }
+
+ onLeftClicked: checkBox.clicked()
+}
diff --git a/src/gui/Pages/AccountSettings/DeviceSection.qml b/src/gui/Pages/AccountSettings/DeviceSection.qml
new file mode 100644
index 00000000..8fb4fe3d
--- /dev/null
+++ b/src/gui/Pages/AccountSettings/DeviceSection.qml
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import "../../Base"
+
+HRowLayout {
+ property HListView view
+
+ readonly property int sectionCheckedCount:
+ Object.values(deviceList.checked).filter(
+ item => item.type === section
+ ).length
+
+ readonly property int sectionTotalCount:
+ deviceList.sectionItemCounts[section] || 0
+
+
+ HCheckBox {
+ padding: theme.spacing
+ topPadding: padding * (section === "current" ? 1 : 2)
+
+ text:
+ section === "current" ? qsTr("Current session") :
+ section === "verified" ? qsTr("Verified") :
+ section === "ignored" ? qsTr("Ignored") :
+ section === "blacklisted" ? qsTr("Blacklisted") :
+ qsTr("Unverified")
+
+ tristate: true
+
+ checkState:
+ sectionTotalCount === sectionCheckedCount ? Qt.Checked :
+ ! sectionCheckedCount ? Qt.Unchecked :
+ Qt.PartiallyChecked
+
+ nextCheckState:
+ checkState === Qt.Checked ? Qt.Unchecked : Qt.Checked
+
+ onClicked: {
+ const indice = []
+
+ for (let i = 0; i < deviceList.count; i++) {
+ if (deviceList.model.get(i).type === section)
+ indice.push(i)
+ }
+
+ const checkedItems = Object.values(deviceList.checked)
+
+ checkedItems.some(item => item.type === section) ?
+ deviceList.uncheck(...indice) :
+ deviceList.check(...indice)
+ }
+
+ Layout.fillWidth: true
+ }
+
+ HLabel {
+ text:
+ sectionCheckedCount ?
+ qsTr("%1 / %2")
+ .arg(sectionCheckedCount).arg(sectionTotalCount) :
+ sectionTotalCount
+
+ rightPadding: theme.spacing * 1.5
+ color:
+ section === "current" || section === "verified" ?
+ theme.colors.positiveText :
+
+ section === "unset" || section === "ignored" ?
+ theme.colors.warningText :
+
+ theme.colors.errorText
+ }
+}
diff --git a/src/gui/Pages/AccountSettings/ImportExportKeys.qml b/src/gui/Pages/AccountSettings/Encryption.qml
similarity index 50%
rename from src/gui/Pages/AccountSettings/ImportExportKeys.qml
rename to src/gui/Pages/AccountSettings/Encryption.qml
index 85845989..f6a9d5db 100644
--- a/src/gui/Pages/AccountSettings/ImportExportKeys.qml
+++ b/src/gui/Pages/AccountSettings/Encryption.qml
@@ -3,41 +3,53 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- buttonModel: [
- { name: "export", text: qsTr("Export"), iconName: "export-keys"},
- { name: "import", text: qsTr("Import"), iconName: "import-keys"},
- ]
+HFlickableColumnPage {
+ id: page
- buttonCallbacks: ({
- export: button => {
- utils.makeObject(
+
+ property string userId
+
+
+ function takeFocus() { exportButton.forceActiveFocus() }
+
+
+ footer: ButtonLayout {
+ OtherButton {
+ id: exportButton
+ text: qsTr("Export")
+ icon.name: "export-keys"
+
+ onClicked: utils.makeObject(
"Dialogs/ExportKeys.qml",
- accountSettings,
- { userId: accountSettings.userId },
+ page,
+ { userId: page.userId },
obj => {
- button.loading = Qt.binding(() => obj.exporting)
+ loading = Qt.binding(() => obj.exporting)
obj.dialog.open()
}
)
- },
- import: button => {
- utils.makeObject(
+ }
+
+ OtherButton {
+ text: qsTr("Import")
+ icon.name: "import-keys"
+
+ onClicked: utils.makeObject(
"Dialogs/ImportKeys.qml",
- accountSettings,
- { userId: accountSettings.userId },
+ page,
+ { userId: page.userId },
obj => { obj.dialog.open() }
)
- },
- })
-
+ }
+ }
HLabel {
wrapMode: Text.Wrap
text: qsTr(
"The decryption keys for messages received in encrypted rooms " +
- "until present time can be backed up " +
+ "until present time can be saved " +
"to a passphrase-protected file.
" +
"You can then import this file on any Matrix account or " +
diff --git a/src/gui/Pages/AccountSettings/Profile.qml b/src/gui/Pages/AccountSettings/Profile.qml
deleted file mode 100644
index 7d7527ea..00000000
--- a/src/gui/Pages/AccountSettings/Profile.qml
+++ /dev/null
@@ -1,271 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-import QtQuick 2.12
-import QtQuick.Controls 2.12
-import QtQuick.Layouts 1.12
-import "../../Base"
-import "../../Dialogs"
-
-HGridLayout {
- function applyChanges() {
- if (nameField.changed) {
- saveButton.nameChangeRunning = true
-
- py.callClientCoro(
- userId, "set_displayname", [nameField.item.text], () => {
- py.callClientCoro(userId, "update_own_profile", [], () => {
- saveButton.nameChangeRunning = false
- accountSettings.headerName =
- Qt.binding(() => accountInfo.display_name)
- })
- }
- )
- }
-
- if (aliasField.changed) {
- window.settings.writeAliases[userId] = aliasField.item.text
- window.settingsChanged()
- }
-
- if (avatar.changed) {
- saveButton.avatarChangeRunning = true
-
- const path =
- Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "")
-
- py.callClientCoro(userId, "set_avatar_from_file", [path], () => {
- py.callClientCoro(userId, "update_own_profile", [], () => {
- saveButton.avatarChangeRunning = false
- })
- }, (errType, [httpCode]) => {
- console.error("Avatar upload failed:", httpCode, errType)
- saveButton.avatarChangeRunning = false
- })
- }
- }
-
- function cancelChanges() {
- nameField.item.text = accountInfo.display_name
- aliasField.item.text = aliasField.currentAlias
- fileDialog.selectedFile = ""
- fileDialog.file = ""
-
- accountSettings.headerName = Qt.binding(() => accountInfo.display_name)
- }
-
- columns: 2
- flow: pageLoader.isWide ? GridLayout.LeftToRight : GridLayout.TopToBottom
- rowSpacing: currentSpacing
-
- Component.onCompleted: nameField.item.forceActiveFocus()
-
- HUserAvatar {
- property bool changed: Boolean(sourceOverride)
-
- id: avatar
- userId: accountSettings.userId
- displayName: nameField.item.text
- mxc: accountInfo.avatar_url
- toolTipMxc: ""
- sourceOverride: fileDialog.selectedFile || fileDialog.file
-
- Layout.alignment: Qt.AlignHCenter
-
- Layout.preferredWidth: Math.min(flickable.height, avatarPreferredSize)
- Layout.preferredHeight: Layout.preferredWidth
-
- Rectangle {
- z: 10
- visible: opacity > 0
- opacity: ! fileDialog.dialog.visible &&
- ((! avatar.mxc && ! avatar.changed) || avatar.hovered) ?
- 1 : 0
-
- anchors.fill: parent
- color: utils.hsluv(0, 0, 0,
- (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7
- )
-
- Behavior on opacity { HNumberAnimation {} }
- Behavior on color { HColorAnimation {} }
-
- HoverHandler { id: overlayHover }
-
- MouseArea {
- anchors.fill: parent
- acceptedButtons: Qt.NoButton
- cursorShape:
- overlayHover.hovered ?
- Qt.PointingHandCursor : Qt.ArrowCursor
- }
-
- HColumnLayout {
- anchors.centerIn: parent
- spacing: currentSpacing
- width: parent.width
-
- HIcon {
- svgName: "upload-avatar"
- colorize: (! avatar.mxc && overlayHover.hovered) ?
- theme.colors.accentText : theme.icons.colorize
- dimension: avatar.width / 3
-
- Layout.alignment: Qt.AlignCenter
- }
-
- Item { Layout.preferredHeight: theme.spacing }
-
- HLabel {
- text: avatar.mxc ?
- qsTr("Change profile picture") :
- qsTr("Upload profile picture")
-
- color: (! avatar.mxc && overlayHover.hovered) ?
- theme.colors.accentText : theme.colors.brightText
- Behavior on color { HColorAnimation {} }
-
- font.pixelSize: theme.fontSize.big *
- avatar.height / avatarPreferredSize
- wrapMode: Text.WordWrap
- horizontalAlignment: Qt.AlignHCenter
-
- Layout.fillWidth: true
- }
- }
- }
-
- HFileDialogOpener {
- id: fileDialog
- fileType: HFileDialogOpener.FileType.Images
- dialog.title: qsTr("Select profile picture for %1")
- .arg(accountInfo.display_name)
- }
- }
-
- HColumnLayout {
- id: profileInfo
- spacing: theme.spacing
-
- HColumnLayout {
- spacing: theme.spacing
- Layout.margins: currentSpacing
-
- HLabel {
- text: qsTr("User ID:
%1")
- .arg(utils.coloredNameHtml(userId, userId, userId))
- textFormat: Text.StyledText
- wrapMode: Text.Wrap
-
- Layout.fillWidth: true
- }
-
- HLabeledItem {
- property bool changed: item.text !== accountInfo.display_name
-
- id: nameField
- label.text: qsTr("Display name:")
-
- Layout.fillWidth: true
- Layout.maximumWidth: 480
-
- HTextField {
- width: parent.width
- maximumLength: 255
-
- onAccepted: applyChanges()
- onTextChanged: accountSettings.headerName = text
- Component.onCompleted: text = accountInfo.display_name
-
- Keys.onEscapePressed: cancelChanges()
- }
- }
-
- HLabeledItem {
- property string currentAlias: aliases[userId] || ""
- property bool changed: item.text !== currentAlias
-
- readonly property var aliases: window.settings.writeAliases
-
- readonly property string alreadyTakenBy: {
- if (! item.text) return ""
-
- for (const [id, idAlias] of Object.entries(aliases))
- if (id !== userId && idAlias === item.text) return id
-
- return ""
- }
-
-
- id: aliasField
-
- label.text: qsTr("Composer alias:")
-
- errorLabel.text:
- alreadyTakenBy ?
- qsTr("Taken by %1").arg(alreadyTakenBy) :
- ""
-
- toolTip.text: qsTr(
- "From any chat, start a message with specified alias " +
- "followed by a space to type and send as this " +
- "account.\n" +
- "The account must have permission to talk in the room.\n"+
- "To ignore the alias when typing, prepend it with a space."
- )
-
- Layout.fillWidth: true
- Layout.maximumWidth: 480
-
- HTextField {
- width: parent.width
- error: aliasField.alreadyTakenBy !== ""
- onAccepted: applyChanges()
- placeholderText: qsTr("e.g. %1").arg((
- nameField.item.text ||
- accountInfo.display_name ||
- userId.substring(1)
- )[0])
-
- Component.onCompleted: text = aliasField.currentAlias
-
- Keys.onEscapePressed: cancelChanges()
- }
- }
- }
-
- HRowLayout {
- Layout.alignment: Qt.AlignBottom
-
- HButton {
- property bool nameChangeRunning: false
- property bool avatarChangeRunning: false
-
- id: saveButton
- icon.name: "apply"
- icon.color: theme.colors.positiveBackground
- text: qsTr("Save")
- loading: nameChangeRunning || avatarChangeRunning
- enabled:
- avatar.changed ||
- nameField.changed ||
- (aliasField.changed && ! aliasField.alreadyTakenBy)
-
- onClicked: applyChanges()
-
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignBottom
- }
-
- HButton {
- icon.name: "cancel"
- icon.color: theme.colors.negativeBackground
- text: qsTr("Cancel")
- enabled: saveButton.enabled && ! saveButton.loading
- onClicked: cancelChanges()
-
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignBottom
- }
- }
- }
-}
diff --git a/src/gui/Pages/AccountSettings/Sessions.qml b/src/gui/Pages/AccountSettings/Sessions.qml
new file mode 100644
index 00000000..dd895dec
--- /dev/null
+++ b/src/gui/Pages/AccountSettings/Sessions.qml
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import "../../Base"
+import "../../Base/ButtonLayout"
+import "../../PythonBridge"
+
+HColumnPage {
+ id: page
+
+
+ property string userId
+
+ property Future loadFuture: null
+
+
+ function takeFocus() {} // XXX
+
+ function loadDevices() {
+ loadFuture = py.callClientCoro(userId, "devices_info", [], devices => {
+ deviceList.checked = {}
+ deviceList.model.clear()
+
+ for (const device of devices)
+ deviceList.model.append(device)
+
+ loadFuture = null
+ })
+ }
+
+
+ footer: ButtonLayout {
+ visible: height >= 0
+ height: deviceList.selectedCount ? implicitHeight : 0
+
+ Behavior on height { HNumberAnimation {} }
+
+ OtherButton {
+ text:
+ deviceList.selectedCount === 1 ?
+ qsTr("Sign out checked session") :
+ qsTr("Sign out %1 sessions").arg(deviceList.selectedCount)
+
+ icon.name: "device-delete-checked"
+ icon.color: theme.colors.negativeBackground
+ }
+ }
+
+
+ HListView {
+ id: deviceList
+
+ readonly property var sectionItemCounts: {
+ const counts = {}
+
+ for (let i = 0; i < count; i++) {
+ const section = model.get(i).type
+ section in counts ? counts[section] += 1 : counts[section] = 1
+ }
+
+ return counts
+ }
+
+ clip: true
+ model: ListModel {}
+ delegate: DeviceDelegate {
+ width: deviceList.width
+ view: deviceList
+ }
+
+ section.property: "type"
+ section.delegate: DeviceSection {
+ width: deviceList.width
+ view: deviceList
+ }
+
+ Component.onCompleted: page.loadDevices()
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ HLoader {
+ anchors.centerIn: parent
+ width: 96 * theme.uiScale
+ height: width
+
+ source: "../../Base/HBusyIndicator.qml"
+ active: page.loadFuture
+ opacity: active ? 1 : 0
+
+ Behavior on opacity { HNumberAnimation { factor: 2 } }
+ }
+ }
+}
diff --git a/src/gui/Pages/AddAccount/AddAccount.qml b/src/gui/Pages/AddAccount/AddAccount.qml
index 8809636b..f8c51d2c 100644
--- a/src/gui/Pages/AddAccount/AddAccount.qml
+++ b/src/gui/Pages/AddAccount/AddAccount.qml
@@ -4,16 +4,21 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
-HFlickableColumnPage {
- title: qsTr("Add an account")
- header: HPageHeader {}
+HPage {
+ id: page
- HTabContainer {
- tabModel: [
- qsTr("Sign in"), qsTr("Register"), qsTr("Reset"),
- ]
+ HTabbedBox {
+ anchors.centerIn: parent
+ width: Math.min(implicitWidth, page.availableWidth)
+ height: Math.min(implicitHeight, page.availableHeight)
- SignIn { Component.onCompleted: forceActiveFocus() }
+ header: HTabBar {
+ HTabButton { text: qsTr("Sign in") }
+ HTabButton { text: qsTr("Register") }
+ HTabButton { text: qsTr("Reset") }
+ }
+
+ SignIn {}
Register {}
Reset {}
}
diff --git a/src/gui/Pages/AddAccount/Register.qml b/src/gui/Pages/AddAccount/Register.qml
index d8fb6ba8..23b2293d 100644
--- a/src/gui/Pages/AddAccount/Register.qml
+++ b/src/gui/Pages/AddAccount/Register.qml
@@ -3,28 +3,30 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: signInBox
- clickButtonOnEnter: "ok"
+HFlickableColumnPage {
+ function takeFocus() { registerButton.forceActiveFocus() }
- buttonModel: [
- { name: "ok", text: qsTr("Register from Riot"), iconName: "register" },
- ]
- buttonCallbacks: ({
- ok: button => {
- Qt.openUrlExternally("https://riot.im/app/#/register")
+ footer: ButtonLayout {
+ ApplyButton {
+ id: registerButton
+ text: qsTr("Register from Riot")
+ icon.name: "register"
+ onClicked: Qt.openUrlExternally("https://riot.im/app/#/register")
+
+ Layout.fillWidth: true
}
- })
+ }
HLabel {
wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter
text: qsTr(
- "Not yet implemented\n\nYou can create a new " +
- "account from another client such as Riot."
+ "Not implemented yet\n\n" +
+ "You can create a new account from another client such as Riot."
)
Layout.fillWidth: true
diff --git a/src/gui/Pages/AddAccount/Reset.qml b/src/gui/Pages/AddAccount/Reset.qml
index 1cd015f6..21bec39d 100644
--- a/src/gui/Pages/AddAccount/Reset.qml
+++ b/src/gui/Pages/AddAccount/Reset.qml
@@ -3,32 +3,31 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: signInBox
- clickButtonOnEnter: "ok"
+HFlickableColumnPage {
+ function takeFocus() { resetButton.forceActiveFocus() }
- buttonModel: [
- {
- name: "ok",
- text: qsTr("Reset password from Riot"),
- iconName: "reset-password"
- },
- ]
- buttonCallbacks: ({
- ok: button => {
- Qt.openUrlExternally("https://riot.im/app/#/forgot_password")
+ footer: ButtonLayout {
+ ApplyButton {
+ id: resetButton
+ text: qsTr("Reset password from Riot")
+ icon.name: "reset-password"
+ onClicked:
+ Qt.openUrlExternally("https://riot.im/app/#/forgot_password")
+
+ Layout.fillWidth: true
}
- })
+ }
HLabel {
wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter
text: qsTr(
- "Not yet implemented\n\nYou can reset your " +
- "password using another client such as Riot."
+ "Not implemented yet\n\n" +
+ "You can reset your password from another client such as Riot."
)
Layout.fillWidth: true
diff --git a/src/gui/Pages/AddAccount/SignIn.qml b/src/gui/Pages/AddAccount/SignIn.qml
index 98b187ed..a443cb3c 100644
--- a/src/gui/Pages/AddAccount/SignIn.qml
+++ b/src/gui/Pages/AddAccount/SignIn.qml
@@ -3,82 +3,10 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: signInBox
- clickButtonOnEnter: "apply"
-
- onFocusChanged: idField.item.forceActiveFocus()
-
- buttonModel: [
- {
- name: "apply",
- text: qsTr("Sign in"),
- enabled: canSignIn,
- iconName: "sign-in",
- loading: loginFuture !== null,
- disableWhileLoading: false,
- },
- { name: "cancel", text: qsTr("Cancel"), iconName: "cancel"},
- ]
-
- buttonCallbacks: ({
- apply: button => {
- if (loginFuture) loginFuture.cancel()
-
- signInTimeout.restart()
-
- errorMessage.text = ""
-
- const args = [
- idField.item.text.trim(), passwordField.item.text,
- undefined, serverField.item.text.trim(),
- ]
-
- loginFuture = py.callCoro("login_client", args, userId => {
- signInTimeout.stop()
- errorMessage.text = ""
- loginFuture = null
-
- py.callCoro(
- rememberAccount.checked ?
- "saved_accounts.add": "saved_accounts.delete",
-
- [userId]
- )
-
- pageLoader.showPage(
- "AccountSettings/AccountSettings", {userId}
- )
-
- }, (type, args, error, traceback, uuid) => {
- loginFuture = null
- signInTimeout.stop()
-
- let txt = qsTr(
- "Invalid request, login type or unknown error: %1",
- ).arg(type)
-
- type === "MatrixForbidden" ?
- txt = qsTr("Invalid username or password") :
-
- type === "MatrixUserDeactivated" ?
- txt = qsTr("This account was deactivated") :
-
- utils.showError(type, traceback, uuid)
-
- errorMessage.text = txt
- })
- },
-
- cancel: button => {
- if (! loginFuture) return
-
- signInTimeout.stop()
- loginFuture.cancel()
- loginFuture = null
- }
- })
+HFlickableColumnPage {
+ id: page
property var loginFuture: null
@@ -90,6 +18,83 @@ HBox {
passwordField.item.text && ! serverField.item.error
+ function takeFocus() { idField.item.forceActiveFocus() }
+
+ function signIn() {
+ if (page.loginFuture) page.loginFuture.cancel()
+
+ signInTimeout.restart()
+
+ errorMessage.text = ""
+
+ const args = [
+ idField.item.text.trim(), passwordField.item.text,
+ undefined, serverField.item.text.trim(),
+ ]
+
+ page.loginFuture = py.callCoro("login_client", args, userId => {
+ signInTimeout.stop()
+ errorMessage.text = ""
+ page.loginFuture = null
+
+ py.callCoro(
+ rememberAccount.checked ?
+ "saved_accounts.add": "saved_accounts.delete",
+
+ [userId]
+ )
+
+ pageLoader.showPage(
+ "AccountSettings/AccountSettings", {userId}
+ )
+
+ }, (type, args, error, traceback, uuid) => {
+ page.loginFuture = null
+ signInTimeout.stop()
+
+ let txt = qsTr(
+ "Invalid request, login type or unknown error: %1",
+ ).arg(type)
+
+ type === "MatrixForbidden" ?
+ txt = qsTr("Invalid username or password") :
+
+ type === "MatrixUserDeactivated" ?
+ txt = qsTr("This account was deactivated") :
+
+ utils.showError(type, traceback, uuid)
+
+ errorMessage.text = txt
+ })
+ }
+
+ function cancel() {
+ if (! page.loginFuture) return
+
+ signInTimeout.stop()
+ page.loginFuture.cancel()
+ page.loginFuture = null
+ }
+
+
+ footer: ButtonLayout {
+ ApplyButton {
+ enabled: page.canSignIn
+ text: qsTr("Sign in")
+ icon.name: "sign-in"
+ loading: page.loginFuture !== null
+ disableWhileLoading: false
+ onClicked: page.signIn()
+ }
+
+ CancelButton {
+ onClicked: page.cancel()
+ }
+ }
+
+ Keys.onEscapePressed: page.cancel()
+
+
Timer {
id: signInTimeout
interval: 30 * 1000
@@ -120,10 +125,10 @@ HBox {
HButton {
icon.name: modelData
circle: true
- checked: signInWith === modelData
+ checked: page.signInWith === modelData
enabled: modelData === "username"
autoExclusive: true
- onClicked: signInWith = modelData
+ onClicked: page.signInWith = modelData
}
}
}
@@ -131,8 +136,8 @@ HBox {
HLabeledItem {
id: idField
label.text: qsTr(
- signInWith === "email" ? "Email:" :
- signInWith === "phone" ? "Phone:" :
+ page.signInWith === "email" ? "Email:" :
+ page.signInWith === "phone" ? "Phone:" :
"Username:"
)
@@ -157,9 +162,6 @@ HBox {
HLabeledItem {
id: serverField
- label.text: qsTr("Homeserver:")
-
- Layout.fillWidth: true
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
readonly property var knownServers: [
@@ -182,6 +184,10 @@ HBox {
readonly property bool knownServerChosen:
knownServers.includes(item.cleanText)
+ label.text: qsTr("Homeserver:")
+
+ Layout.fillWidth: true
+
HTextField {
width: parent.width
text: "https://matrix.org"
diff --git a/src/gui/Pages/AddChat/AddChat.qml b/src/gui/Pages/AddChat/AddChat.qml
index b8bebf93..4ad4a259 100644
--- a/src/gui/Pages/AddChat/AddChat.qml
+++ b/src/gui/Pages/AddChat/AddChat.qml
@@ -2,27 +2,28 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
-import "../.."
import "../../Base"
-HFlickableColumnPage {
- id: addChatPage
- title: qsTr("Add new chat")
- header: HPageHeader {}
+HPage {
+ id: page
property string userId
- readonly property QtObject account: ModelStore.get("accounts").find(userId)
+ HTabbedBox {
+ anchors.centerIn: parent
+ width: Math.min(implicitWidth, page.availableWidth)
+ height: Math.min(implicitHeight, page.availableHeight)
- HTabContainer {
- tabModel: [
- qsTr("Direct chat"), qsTr("Join room"), qsTr("Create room"),
- ]
+ header: HTabBar {
+ HTabButton { text: qsTr("Direct chat") }
+ HTabButton { text: qsTr("Join room") }
+ HTabButton { text: qsTr("Create room") }
+ }
- DirectChat { Component.onCompleted: forceActiveFocus() }
- JoinRoom {}
- CreateRoom {}
+ DirectChat { userId: page.userId }
+ JoinRoom { userId: page.userId }
+ CreateRoom { userId: page.userId }
}
}
diff --git a/src/gui/Pages/AddChat/CreateRoom.qml b/src/gui/Pages/AddChat/CreateRoom.qml
index 5b47ffee..279e4acd 100644
--- a/src/gui/Pages/AddChat/CreateRoom.qml
+++ b/src/gui/Pages/AddChat/CreateRoom.qml
@@ -2,57 +2,69 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
+import "../.."
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: addChatBox
- clickButtonOnEnter: "apply"
+HFlickableColumnPage {
+ id: page
- onFocusChanged: nameField.item.forceActiveFocus()
- buttonModel: [
- { name: "apply", text: qsTr("Create"), iconName: "room-create" },
- { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
- ]
+ property string userId
+ readonly property QtObject account: ModelStore.get("accounts").find(userId)
- buttonCallbacks: ({
- apply: button => {
- button.loading = true
- errorMessage.text = ""
- const args = [
- nameField.item.text,
- topicArea.item.text,
- publicCheckBox.checked,
- encryptCheckBox.checked,
- ! blockOtherServersCheckBox.checked,
- ]
+ function takeFocus() { nameField.item.forceActiveFocus() }
- py.callClientCoro(userId, "new_group_chat", args, roomId => {
- button.loading = false
- pageLoader.showRoom(userId, roomId)
- mainPane.roomList.startCorrectItemSearch()
+ function create() {
+ applyButton.loading = true
+ errorMessage.text = ""
- }, (type, args) => {
- button.loading = false
- errorMessage.text =
- qsTr("Unknown error - %1: %2").arg(type).arg(args)
- })
- },
+ const args = [
+ nameField.item.text,
+ topicArea.item.text,
+ publicCheckBox.checked,
+ encryptCheckBox.checked,
+ ! blockOtherServersCheckBox.checked,
+ ]
- cancel: button => {
- nameField.item.text = ""
- topicArea.item.text = ""
- publicCheckBox.checked = false
- encryptCheckBox.checked = false
- blockOtherServersCheckBox.checked = false
+ py.callClientCoro(userId, "new_group_chat", args, roomId => {
+ applyButton.loading = false
+ pageLoader.showRoom(userId, roomId)
+ mainPane.roomList.startCorrectItemSearch()
- pageLoader.showPrevious()
+ }, (type, args) => {
+ applyButton.loading = false
+ errorMessage.text =
+ qsTr("Unknown error - %1: %2").arg(type).arg(args)
+ })
+ }
+
+ function cancel() {
+ nameField.item.reset()
+ topicArea.item.reset()
+ publicCheckBox.reset()
+ encryptCheckBox.reset()
+ blockOtherServersCheckBox.reset()
+
+ pageLoader.showPrevious()
+ }
+
+
+ footer: ButtonLayout {
+ ApplyButton {
+ id: applyButton
+ text: qsTr("Create")
+ icon.name: "room-create"
+ onClicked: create()
}
- })
+ CancelButton {
+ onClicked: cancel()
+ }
+ }
- readonly property string userId: addChatPage.userId
+ Keys.onEscapePressed: cancel()
HRoomAvatar {
@@ -70,6 +82,9 @@ HBox {
opacity: nameField.item.text ? 0 : 1
visible: opacity > 0
+ userId: page.userId
+ account: page.account
+
Behavior on opacity { HNumberAnimation {} }
}
}
diff --git a/src/gui/Pages/AddChat/CurrentUserAvatar.qml b/src/gui/Pages/AddChat/CurrentUserAvatar.qml
index 3c99b738..2593ff90 100644
--- a/src/gui/Pages/AddChat/CurrentUserAvatar.qml
+++ b/src/gui/Pages/AddChat/CurrentUserAvatar.qml
@@ -1,10 +1,17 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
+import QtQuick.Layouts 1.12
import "../../Base"
HUserAvatar {
- userId: addChatPage.userId
- displayName: addChatPage.account ? addChatPage.account.display_name : ""
- mxc: addChatPage.account ? addChatPage.account.avatar_url : ""
+ property QtObject account
+
+ // userId: (set me)
+ displayName: account ? account.display_name : ""
+ mxc: account ? account.avatar_url : ""
+
+ Layout.alignment: Qt.AlignCenter
+ Layout.preferredWidth: 128
+ Layout.preferredHeight: Layout.preferredWidth
}
diff --git a/src/gui/Pages/AddChat/DirectChat.qml b/src/gui/Pages/AddChat/DirectChat.qml
index 3c174759..337fa0b8 100644
--- a/src/gui/Pages/AddChat/DirectChat.qml
+++ b/src/gui/Pages/AddChat/DirectChat.qml
@@ -2,77 +2,91 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
+import "../.."
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: addChatBox
- clickButtonOnEnter: "apply"
+HFlickableColumnPage {
+ id: page
- onFocusChanged: userField.item.forceActiveFocus()
- buttonModel: [
- {
- name: "apply",
- text: qsTr("Start chat"),
- iconName: "start-direct-chat",
- enabled: Boolean(userField.item.text.trim())
- },
- { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
- ]
+ property string userId
+ readonly property QtObject account: ModelStore.get("accounts").find(userId)
- buttonCallbacks: ({
- apply: button => {
- button.loading = true
- errorMessage.text = ""
- const args = [userField.item.text.trim(), encryptCheckBox.checked]
+ function takeFocus() {
+ userField.item.forceActiveFocus()
+ }
- py.callClientCoro(userId, "new_direct_chat", args, roomId => {
- button.loading = false
- errorMessage.text = ""
- pageLoader.showRoom(userId, roomId)
- mainPane.roomList.startCorrectItemSearch()
+ function startChat() {
+ applyButton.loading = true
+ errorMessage.text = ""
- }, (type, args) => {
- button.loading = false
+ const args = [userField.item.text.trim(), encryptCheckBox.checked]
- let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
-
- if (type === "InvalidUserInContext")
- txt = qsTr("Can't start chatting with yourself")
-
- if (type === "InvalidUserId")
- txt = qsTr("Invalid user ID, expected format is " +
- "@username:homeserver")
-
- if (type === "MatrixNotFound")
- txt = qsTr("User not found, please verify the entered ID")
-
- if (type === "MatrixBadGateway")
- txt = qsTr(
- "Could not contact this user's server, " +
- "please verify the entered ID"
- )
-
- errorMessage.text = txt
- })
- },
-
- cancel: button => {
- userField.item.text = ""
+ py.callClientCoro(userId, "new_direct_chat", args, roomId => {
+ applyButton.loading = false
errorMessage.text = ""
- pageLoader.showPrevious()
+ pageLoader.showRoom(userId, roomId)
+ mainPane.roomList.startCorrectItemSearch()
+
+ }, (type, args) => {
+ applyButton.loading = false
+
+ let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
+
+ if (type === "InvalidUserInContext")
+ txt = qsTr("Can't start chatting with yourself")
+
+ if (type === "InvalidUserId")
+ txt = qsTr("Invalid user ID, expected format is " +
+ "@username:homeserver")
+
+ if (type === "MatrixNotFound")
+ txt = qsTr("User not found, please verify the entered ID")
+
+ if (type === "MatrixBadGateway")
+ txt = qsTr(
+ "Could not contact this user's server, " +
+ "please verify the entered ID"
+ )
+
+ errorMessage.text = txt
+ })
+ }
+
+ function cancel() {
+ userField.item.reset()
+ errorMessage.text = ""
+
+ pageLoader.showPrevious()
+ }
+
+
+ footer: ButtonLayout {
+ ApplyButton {
+ id: applyButton
+ text: qsTr("Start chat")
+ icon.name: "start-direct-chat"
+ enabled: Boolean(userField.item.text.trim())
+ onClicked: startChat()
}
- })
+ CancelButton {
+ onClicked: {
+ userField.item.text = ""
+ errorMessage.text = ""
+ pageLoader.showPrevious()
+ }
+ }
+ }
- readonly property string userId: addChatPage.userId
+ Keys.onEscapePressed: cancel()
CurrentUserAvatar {
- Layout.alignment: Qt.AlignCenter
- Layout.preferredWidth: 128
- Layout.preferredHeight: Layout.preferredWidth
+ userId: page.userId
+ account: page.account
}
HLabeledItem {
diff --git a/src/gui/Pages/AddChat/JoinRoom.qml b/src/gui/Pages/AddChat/JoinRoom.qml
index 083644ee..f1317b85 100644
--- a/src/gui/Pages/AddChat/JoinRoom.qml
+++ b/src/gui/Pages/AddChat/JoinRoom.qml
@@ -2,70 +2,79 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
+import "../.."
import "../../Base"
+import "../../Base/ButtonLayout"
-HBox {
- id: addChatBox
- clickButtonOnEnter: "apply"
+HFlickableColumnPage {
+ id: page
- onFocusChanged: roomField.item.forceActiveFocus()
- buttonModel: [
- {
- name: "apply",
- text: qsTr("Join"),
- iconName: "room-join",
- enabled: Boolean(roomField.item.text.trim()),
- },
- { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
- ]
+ property string userId
+ readonly property QtObject account: ModelStore.get("accounts").find(userId)
- buttonCallbacks: ({
- apply: button => {
- button.loading = true
- errorMessage.text = ""
- const args = [roomField.item.text.trim()]
+ function takeFocus() {
+ roomField.item.forceActiveFocus()
+ }
- py.callClientCoro(userId, "room_join", args, roomId => {
- button.loading = false
- errorMessage.text = ""
- pageLoader.showRoom(userId, roomId)
- mainPane.roomList.startCorrectItemSearch()
+ function join() {
+ joinButton.loading = true
+ errorMessage.text = ""
- }, (type, args) => {
- button.loading = false
+ const args = [roomField.item.text.trim()]
- let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
+ py.callClientCoro(userId, "room_join", args, roomId => {
+ joinButton.loading = false
+ errorMessage.text = ""
+ pageLoader.showRoom(userId, roomId)
+ mainPane.roomList.startCorrectItemSearch()
- if (type === "ValueError")
- txt = qsTr("Unrecognized alias, room ID or URL")
+ }, (type, args) => {
+ joinButton.loading = false
- if (type === "MatrixNotFound")
- txt = qsTr("Room not found")
+ let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
- if (type === "MatrixForbidden")
- txt = qsTr("You do not have permission to join this room")
+ if (type === "ValueError")
+ txt = qsTr("Unrecognized alias, room ID or URL")
- errorMessage.text = txt
- })
- },
+ if (type === "MatrixNotFound")
+ txt = qsTr("Room not found")
- cancel: button => {
- roomField.item.text = ""
- errorMessage.text = ""
- pageLoader.showPrevious()
+ if (type === "MatrixForbidden")
+ txt = qsTr("You do not have permission to join this room")
+
+ errorMessage.text = txt
+ })
+ }
+
+ function cancel() {
+ roomField.item.reset()
+ errorMessage.reset()
+
+ pageLoader.showPrevious()
+ }
+
+
+ footer: ButtonLayout {
+ ApplyButton {
+ text: qsTr("Join")
+ icon.name: "room-join"
+ enabled: Boolean(roomField.item.text.trim())
+ onClicked: join()
}
- })
+ CancelButton {
+ onClicked: cancel()
+ }
+ }
- readonly property string userId: addChatPage.userId
+ Keys.onEscapePressed: cancel()
CurrentUserAvatar {
- Layout.alignment: Qt.AlignCenter
- Layout.preferredWidth: 128
- Layout.preferredHeight: Layout.preferredWidth
+ userId: page.userId
+ account: page.account
}
HLabeledItem {
diff --git a/src/gui/Pages/Chat/ChatPage.qml b/src/gui/Pages/Chat/ChatPage.qml
index 34d000d9..e2fea337 100644
--- a/src/gui/Pages/Chat/ChatPage.qml
+++ b/src/gui/Pages/Chat/ChatPage.qml
@@ -11,6 +11,7 @@ import "Timeline"
HColumnPage {
id: chatPage
padding: 0
+ column.spacing: 0
onLoadEventListChanged: if (loadEventList) loadedOnce = true
Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel()
diff --git a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml b/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml
index 351c572d..f9a78bd1 100644
--- a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml
+++ b/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml
@@ -67,10 +67,7 @@ HTile {
roomId: chat.roomId,
targetUserId: model.id,
targetDisplayName: model.display_name,
- operation:
- model.invited ?
- RemoveMemberPopup.Operation.Disinvite :
- RemoveMemberPopup.Operation.Kick,
+ operation: model.invited ? "disinvite" : "kick",
})
Component.onCompleted: py.callClientCoro(
@@ -94,7 +91,7 @@ HTile {
roomId: chat.roomId,
targetUserId: model.id,
targetDisplayName: model.display_name,
- operation: RemoveMemberPopup.Operation.Ban,
+ operation: "ban",
})
Component.onCompleted: py.callClientCoro(
diff --git a/src/gui/Pages/Chat/RoomPane/SettingsView.qml b/src/gui/Pages/Chat/RoomPane/SettingsView.qml
index 7ae80127..492eb602 100644
--- a/src/gui/Pages/Chat/RoomPane/SettingsView.qml
+++ b/src/gui/Pages/Chat/RoomPane/SettingsView.qml
@@ -57,9 +57,6 @@ HFlickableColumnPage {
}
- useVariableSpacing: false
- column.spacing: theme.spacing * 1.5
-
flickShortcuts.active:
! mainUI.debugConsole.visible && ! chat.composerHasFocus
@@ -82,6 +79,8 @@ HFlickableColumnPage {
}
}
+ Keys.onEscapePressed: cancel()
+
HRoomAvatar {
id: avatar
diff --git a/src/gui/Popups/BoxPopup.qml b/src/gui/Popups/BoxPopup.qml
deleted file mode 100644
index f7833aa9..00000000
--- a/src/gui/Popups/BoxPopup.qml
+++ /dev/null
@@ -1,79 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-import QtQuick 2.12
-import QtQuick.Layouts 1.12
-import "../Base"
-
-HPopup {
- id: popup
- onAboutToShow: okClicked = false
-
-
- signal ok()
- signal cancel()
-
-
- default property alias boxData: box.body
- property alias box: box
- property bool fillAvailableHeight: false
-
- property alias summary: summary
- property alias details: details
-
- property string okText: qsTr("OK")
- property string okIcon: "ok"
- property bool okEnabled: true
- property bool okClicked: false
- property string cancelText: qsTr("Cancel")
-
-
- Binding on height {
- value: popup.maximumPreferredHeight
- when: popup.fillAvailableHeight
- }
-
- HBox {
- id: box
- implicitWidth: Math.min(
- window.width - popup.leftMargin - popup.rightMargin,
- theme.controls.popup.defaultWidth,
- )
- fillAvailableHeight: popup.fillAvailableHeight
- clickButtonOnEnter: "ok"
-
- buttonModel: [
- { name: "ok", text: okText, iconName: okIcon, enabled: okEnabled},
- { name: "cancel", text: cancelText, iconName: "cancel" },
- ]
-
- buttonCallbacks: ({
- ok: button => { okClicked = true; popup.ok(); popup.close() },
- cancel: button => {
- okClicked = false; popup.cancel(); popup.close()
- },
- })
-
-
- Binding on height {
- value: popup.maximumPreferredHeight
- when: popup.fillAvailableHeight
- }
-
- HLabel {
- id: summary
- wrapMode: Text.Wrap
- font.bold: true
- visible: Boolean(text)
-
- Layout.fillWidth: true
- }
-
- HLabel {
- id: details
- wrapMode: Text.Wrap
- visible: Boolean(text)
-
- Layout.fillWidth: true
- }
- }
-}
diff --git a/src/gui/Popups/ClearMessagesPopup.qml b/src/gui/Popups/ClearMessagesPopup.qml
index 888c2c1d..6652e291 100644
--- a/src/gui/Popups/ClearMessagesPopup.qml
+++ b/src/gui/Popups/ClearMessagesPopup.qml
@@ -1,19 +1,42 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
+import "../Base/ButtonLayout"
-BoxPopup {
- summary.text: qsTr("Clear this room's messages?")
- details.text: qsTr(
- "The messages will only be removed on your side. " +
- "They will be available again after you restart the application."
- )
- okText: qsTr("Clear")
- box.focusButton: "ok"
-
- onOk: py.callClientCoro(userId, "clear_events", [roomId])
+HFlickableColumnPopup {
+ id: popup
property string userId: ""
property string roomId: ""
+
+
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: clearButton
+ text: qsTr("Clear")
+ icon.name: "clear-messages"
+ onClicked: {
+ py.callClientCoro(userId, "clear_events", [roomId])
+ popup.close()
+ }
+ }
+
+ CancelButton {
+ onClicked: popup.close()
+ }
+ }
+
+ SummaryLabel {
+ text: qsTr("Clear this room's messages?")
+ }
+
+ DetailsLabel {
+ text: qsTr(
+ "The messages will only be removed on your side. " +
+ "They will be available again after you restart the application."
+ )
+ }
+
+ onOpened: clearButton.forceActiveFocus()
}
diff --git a/src/gui/Popups/DetailsLabel.qml b/src/gui/Popups/DetailsLabel.qml
new file mode 100644
index 00000000..1bc54487
--- /dev/null
+++ b/src/gui/Popups/DetailsLabel.qml
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import "../Base"
+
+HLabel {
+ wrapMode: Text.Wrap
+ visible: Boolean(text)
+
+ Layout.fillWidth: true
+}
diff --git a/src/gui/Popups/ForgetRoomPopup.qml b/src/gui/Popups/ForgetRoomPopup.qml
index 8b3feb9a..04f12486 100644
--- a/src/gui/Popups/ForgetRoomPopup.qml
+++ b/src/gui/Popups/ForgetRoomPopup.qml
@@ -1,35 +1,10 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
+import "../Base/ButtonLayout"
-BoxPopup {
+HFlickableColumnPopup {
id: popup
- summary.text: qsTr("Leave %1 and lose the history?").arg(roomName)
- summary.textFormat: Text.StyledText
- details.text: qsTr(
- "You will not be able to see the messages you received in " +
- "this room anymore.\n\n" +
-
- "If all members forget the room, it will be removed from the servers."
- )
-
- okText: qsTr("Forget")
- box.focusButton: "ok"
-
- onOk: py.callClientCoro(userId, "room_forget", [roomId], () => {
- if (window.uiState.page === "Pages/Chat/Chat.qml" &&
- window.uiState.pageProperties.userId === userId &&
- window.uiState.pageProperties.roomId === roomId)
- {
- window.mainUI.pageLoader.showPrevious() ||
- window.mainUI.pageLoader.showPage("Default")
-
- Qt.callLater(popup.destroy)
- }
- })
-
- onCancel: canDestroy = true
- onClosed: if (canDestroy) Qt.callLater(popup.destroy)
property string userId: ""
@@ -37,4 +12,55 @@ BoxPopup {
property string roomName: ""
property bool canDestroy: false
+
+
+ function forget() {
+ py.callClientCoro(userId, "room_forget", [roomId], () => {
+ if (window.uiState.page === "Pages/Chat/Chat.qml" &&
+ window.uiState.pageProperties.userId === userId &&
+ window.uiState.pageProperties.roomId === roomId)
+ {
+ window.mainUI.pageLoader.showPrevious() ||
+ window.mainUI.pageLoader.showPage("Default")
+
+ Qt.callLater(popup.destroy)
+ }
+ })
+ }
+
+
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: forgetButton
+ text: qsTr("Forget")
+ icon.name: "room-forget"
+ onClicked: forget()
+ }
+
+ CancelButton {
+ onClicked: {
+ canDestroy = true
+ popup.close()
+ }
+ }
+ }
+
+ onOpened: forgetButton.forceActiveFocus()
+ onClosed: if (canDestroy) Qt.callLater(popup.destroy)
+
+
+ SummaryLabel {
+ text: qsTr("Leave %1 and lose the history?").arg(roomName)
+ textFormat: Text.StyledText
+ }
+
+ DetailsLabel {
+ text: qsTr(
+ "You will not be able to see the messages you received in " +
+ "this room anymore.\n\n" +
+
+ "If all members forget the room, it will be removed from the " +
+ "servers."
+ )
+ }
}
diff --git a/src/gui/Popups/HColumnPopup.qml b/src/gui/Popups/HColumnPopup.qml
new file mode 100644
index 00000000..08531bd8
--- /dev/null
+++ b/src/gui/Popups/HColumnPopup.qml
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import "../Base"
+
+HPopup {
+ id: popup
+
+
+ default property alias pageData: page.columnData
+ readonly property alias page: page
+
+
+ HColumnPage {
+ id: page
+ implicitWidth: Math.min(
+ popup.maximumPreferredWidth,
+ theme.controls.popup.defaultWidth,
+ )
+ implicitHeight: Math.min(
+ popup.maximumPreferredHeight,
+ implicitHeaderHeight + implicitFooterHeight +
+ topPadding + bottomPadding + implicitContentHeight,
+ )
+ useVariableSpacing: false
+ }
+}
diff --git a/src/gui/Popups/HFlickableColumnPopup.qml b/src/gui/Popups/HFlickableColumnPopup.qml
new file mode 100644
index 00000000..a2aefec0
--- /dev/null
+++ b/src/gui/Popups/HFlickableColumnPopup.qml
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import "../Base"
+
+HPopup {
+ id: popup
+
+
+ default property alias pageData: page.columnData
+ readonly property alias page: page
+
+
+ HFlickableColumnPage {
+ id: page
+ implicitWidth: Math.min(
+ popup.maximumPreferredWidth,
+ theme.controls.popup.defaultWidth,
+ )
+ implicitHeight: Math.min(
+ popup.maximumPreferredHeight,
+ implicitHeaderHeight + implicitFooterHeight + contentHeight,
+ )
+ }
+}
diff --git a/src/gui/Popups/InviteToRoomPopup.qml b/src/gui/Popups/InviteToRoomPopup.qml
index 58588d7d..799da2ee 100644
--- a/src/gui/Popups/InviteToRoomPopup.qml
+++ b/src/gui/Popups/InviteToRoomPopup.qml
@@ -4,51 +4,10 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../Base"
+import "../Base/ButtonLayout"
-BoxPopup {
+HColumnPopup {
id: popup
- // fillAvailableHeight: true
- summary.text: qsTr("Invite members to %1").arg(roomName)
- summary.textFormat: Text.StyledText
- okText: qsTr("Invite")
- okEnabled: invitingAllowed && Boolean(inviteArea.text.trim())
-
- onOpened: inviteArea.forceActiveFocus()
-
- onInvitingAllowedChanged:
- if (! invitingAllowed && inviteFuture) inviteFuture.cancel()
-
- box.buttonCallbacks: ({
- ok: button => {
- button.loading = true
-
- const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter(
- user => ! successfulInvites.includes(user)
- )
-
- inviteFuture = py.callClientCoro(
- userId,
- "room_mass_invite",
- [roomId, ...inviteesLeft],
-
- ([successes, errors]) => {
- if (errors.length < 1) {
- popup.close()
- return
- }
-
- successfulInvites = successes
- failedInvites = errors
- button.loading = false
- }
- )
- },
-
- cancel: button => {
- if (inviteFuture) inviteFuture.cancel()
- popup.close()
- },
- })
property string userId
@@ -61,13 +20,68 @@ BoxPopup {
property var failedInvites: []
+ function invite() {
+ inviteButton.loading = true
+
+ const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter(
+ user => ! successfulInvites.includes(user)
+ )
+
+ inviteFuture = py.callClientCoro(
+ userId,
+ "room_mass_invite",
+ [roomId, ...inviteesLeft],
+
+ ([successes, errors]) => {
+ if (errors.length < 1) {
+ popup.close()
+ return
+ }
+
+ successfulInvites = successes
+ failedInvites = errors
+ inviteButton.loading = false
+ }
+ )
+ }
+
+
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: inviteButton
+ text: qsTr("Invite")
+ icon.name: "room-send-invite"
+ enabled: invitingAllowed && Boolean(inviteArea.text.trim())
+ onClicked: invite()
+ }
+
+ CancelButton {
+ id: cancelButton
+ onClicked: popup.close()
+ }
+ }
+
+ onOpened: inviteArea.forceActiveFocus()
+ onClosed: if (inviteFuture) inviteFuture.cancel()
+
+ onInvitingAllowedChanged:
+ if (! invitingAllowed && inviteFuture) inviteFuture.cancel()
+
+
+ SummaryLabel {
+ text: qsTr("Invite members to %1").arg(roomName)
+ textFormat: Text.StyledText
+ }
+
HScrollView {
+ clip: true
+
Layout.fillWidth: true
Layout.fillHeight: true
HTextArea {
id: inviteArea
- focusItemOnTab: box.firstButton
+ focusItemOnTab: inviteButton.enabled ? inviteButton : cancelButton
placeholderText:
qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)")
}
diff --git a/src/gui/Popups/LeaveRoomPopup.qml b/src/gui/Popups/LeaveRoomPopup.qml
index 2be62842..ebcfbd60 100644
--- a/src/gui/Popups/LeaveRoomPopup.qml
+++ b/src/gui/Popups/LeaveRoomPopup.qml
@@ -1,21 +1,46 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
+import "../Base/ButtonLayout"
-BoxPopup {
- summary.text: qsTr("Leave %1?").arg(roomName)
- summary.textFormat: Text.StyledText
- details.text: qsTr(
- "If this room is private, you will not be able to rejoin it."
- )
- okText: qsTr("Leave")
- box.focusButton: "ok"
-
- onOk: py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
+HFlickableColumnPopup {
+ id: popup
property string userId: ""
property string roomId: ""
property string roomName: ""
property var leftCallback: null
+
+
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: leaveButton
+ icon.name: "room-leave"
+ text: qsTr("Leave")
+
+ onClicked: {
+ py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
+ popup.close()
+ }
+ }
+
+ CancelButton {
+ onClicked: popup.close()
+ }
+ }
+
+ onOpened: leaveButton.forceActiveFocus()
+
+
+ SummaryLabel {
+ text: qsTr("Leave %1?").arg(roomName)
+ textFormat: Text.StyledText
+ }
+
+ DetailsLabel {
+ text: qsTr(
+ "If this room is private, you will not be able to rejoin it."
+ )
+ }
}
diff --git a/src/gui/Popups/PasswordPopup.qml b/src/gui/Popups/PasswordPopup.qml
index b13b1355..53bbcb56 100644
--- a/src/gui/Popups/PasswordPopup.qml
+++ b/src/gui/Popups/PasswordPopup.qml
@@ -3,29 +3,22 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
+import "../Base/ButtonLayout"
-BoxPopup {
+HFlickableColumnPopup {
id: popup
- okEnabled: Boolean(passwordField.text)
-
- onAboutToShow: {
- okClicked = false
- acceptedPassword = ""
- passwordValid = null
- errorMessage.text = ""
- }
- onOpened: passwordField.forceActiveFocus()
-
-
- signal cancelled()
property bool validateWhileTyping: false
property string acceptedPassword: ""
property var passwordValid: null
+ property bool okClicked: false
- property alias field: passwordField
+ readonly property alias summary: summary
+ readonly property alias validateButton: validateButton
+
+ signal cancelled()
function verifyPassword(pass, callback) {
@@ -35,40 +28,59 @@ BoxPopup {
callback(true)
}
+ function validate() {
+ const password = passwordField.text
+ okClicked = true
+ validateButton.loading = true
+ errorMessage.text = ""
- box.buttonCallbacks: ({
- ok: button => {
- const password = passwordField.text
- okClicked = true
- button.loading = true
- errorMessage.text = ""
+ verifyPassword(password, result => {
+ if (result === true) {
+ passwordValid = true
+ popup.acceptedPassword = password
+ popup.close()
+ } else if (result === false) {
+ passwordValid = false
+ } else {
+ errorMessage.text = result
+ }
- verifyPassword(password, result => {
- if (result === true) {
- passwordValid = true
- popup.acceptedPassword = password
- popup.close()
- } else if (result === false) {
- passwordValid = false
- } else {
- errorMessage.text = result
- }
+ validateButton.loading = false
+ })
+ }
- button.loading = false
- })
- },
- cancel: button => {
- popup.close()
- cancelled()
- },
- })
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: validateButton
+ text: qsTr("Validate")
+ enabled: Boolean(passwordField.text)
+ onClicked: validate()
+ }
+
+ CancelButton {
+ onClicked: {
+ popup.close()
+ cancelled()
+ }
+ }
+ }
+
+ onAboutToShow: {
+ okClicked = false
+ acceptedPassword = ""
+ passwordValid = null
+ errorMessage.text = ""
+ }
+
+ onOpened: passwordField.forceActiveFocus()
+
+
+ SummaryLabel { id: summary }
HRowLayout {
spacing: theme.spacing
- Layout.fillWidth: true
-
HTextField {
id: passwordField
echoMode: TextInput.Password
@@ -78,6 +90,8 @@ BoxPopup {
onTextChanged: passwordValid =
validateWhileTyping ? verifyPassword(text) : null
+ onAccepted: popup.validate()
+
Layout.fillWidth: true
}
@@ -91,7 +105,8 @@ BoxPopup {
Layout.preferredWidth:
passwordValid === null ||
(validateWhileTyping && ! okClicked && ! passwordValid) ?
- 0 :implicitWidth
+ 0 :
+ implicitWidth
Behavior on Layout.preferredWidth { HNumberAnimation {} }
}
diff --git a/src/gui/Popups/RedactPopup.qml b/src/gui/Popups/RedactPopup.qml
index 96090438..b8984030 100644
--- a/src/gui/Popups/RedactPopup.qml
+++ b/src/gui/Popups/RedactPopup.qml
@@ -3,28 +3,21 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
+import "../Base/ButtonLayout"
-BoxPopup {
- summary.text:
- isLast ?
- qsTr("Remove your last message?") :
+HFlickableColumnPopup {
+ id: popup
- eventSenderAndIds.length > 1 ?
- qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) :
- qsTr("Remove this message?")
+ property string preferUserId: ""
+ property string roomId: ""
- details.color: theme.colors.warningText
- details.text:
- onlyOwnMessageWarning ?
- qsTr("Only your messages can be removed") :
- ""
+ property var eventSenderAndIds: [] // [[senderId, event.id], ...]
+ property bool onlyOwnMessageWarning: false
+ property bool isLast: false
- okText: qsTr("Remove")
- // box.focusButton: "ok"
- onOpened: reasonField.item.forceActiveFocus()
- onOk: {
+ function remove() {
const idsForSender = {} // {senderId: [event.id, ...]}
for (const [senderId, eventClientId] of eventSenderAndIds) {
@@ -40,16 +33,44 @@ BoxPopup {
"room_mass_redact",
[roomId, reasonField.item.text, ...eventClientIds]
)
+
+ popup.close()
}
- property string preferUserId: ""
- property string roomId: ""
+ page.footer: ButtonLayout {
+ ApplyButton {
+ text: qsTr("Remove")
+ icon.name: "remove-message"
+ onClicked: remove()
+ }
- property var eventSenderAndIds: [] // [[senderId, event.id], ...]
- property bool onlyOwnMessageWarning: false
- property bool isLast: false
+ CancelButton {
+ onClicked: popup.close()
+ }
+ }
+ onOpened: reasonField.item.forceActiveFocus()
+
+
+ SummaryLabel {
+ text:
+ isLast ?
+ qsTr("Remove your last message?") :
+
+ eventSenderAndIds.length > 1 ?
+ qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) :
+
+ qsTr("Remove this message?")
+ }
+
+ DetailsLabel {
+ color: theme.colors.warningText
+ text:
+ onlyOwnMessageWarning ?
+ qsTr("Only your messages can be removed") :
+ ""
+ }
HLabeledItem {
id: reasonField
@@ -59,6 +80,7 @@ BoxPopup {
HTextField {
width: parent.width
+ onAccepted: popup.remove()
}
}
}
diff --git a/src/gui/Popups/RemoveMemberPopup.qml b/src/gui/Popups/RemoveMemberPopup.qml
index c13ed83d..2d5d5c01 100644
--- a/src/gui/Popups/RemoveMemberPopup.qml
+++ b/src/gui/Popups/RemoveMemberPopup.qml
@@ -3,48 +3,65 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
+import "../Base/ButtonLayout"
-BoxPopup {
- summary.textFormat: Text.StyledText
- summary.text:
- operation === RemoveMemberPopup.Operation.Disinvite ?
- qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
+HFlickableColumnPopup {
+ id: popup
- operation === RemoveMemberPopup.Operation.Kick ?
- qsTr("Kick %1 out of the room?").arg(coloredTarget) :
-
- qsTr("Ban %1 from the room?").arg(coloredTarget)
-
- okText:
- operation === RemoveMemberPopup.Operation.Disinvite ?
- qsTr("Disinvite") :
-
- operation === RemoveMemberPopup.Operation.Kick ?
- qsTr("Kick") :
-
- qsTr("Ban")
-
- onOpened: reasonField.item.forceActiveFocus()
- onOk: py.callClientCoro(
- userId,
- operation === RemoveMemberPopup.Operation.Ban ?
- "room_ban" : "room_kick",
- [roomId, targetUserId, reasonField.item.text || null],
- )
-
-
- enum Operation { Disinvite, Kick, Ban }
property string userId
property string roomId
property string targetUserId
property string targetDisplayName
- property int operation
+ property string operation // "disinvite", "kick" or "ban"
readonly property string coloredTarget:
utils.coloredNameHtml(targetDisplayName, targetUserId)
+ function remove() {
+ py.callClientCoro(
+ userId,
+ operation === "ban" ? "room_ban" : "room_kick",
+ [roomId, targetUserId, reasonField.item.text || null],
+ )
+
+ popup.close()
+ }
+
+
+ page.footer: ButtonLayout {
+ ApplyButton {
+ text:
+ operation === "disinvite" ? qsTr("Disinvite") :
+ operation === "kick" ? qsTr("Kick") :
+ qsTr("Ban")
+
+ icon.name: operation === "ban" ? "room-ban" : "room-kick"
+
+ onClicked: remove()
+ }
+
+ CancelButton {
+ onClicked: popup.close()
+ }
+ }
+
+ onOpened: reasonField.item.forceActiveFocus()
+
+
+ SummaryLabel {
+ textFormat: Text.StyledText
+ text:
+ operation === "disinvite" ?
+ qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
+
+ operation === "kick" ?
+ qsTr("Kick %1 out of the room?").arg(coloredTarget) :
+
+ qsTr("Ban %1 from the room?").arg(coloredTarget)
+ }
+
HLabeledItem {
id: reasonField
label.text: qsTr("Optional reason:")
@@ -53,6 +70,7 @@ BoxPopup {
HTextField {
width: parent.width
+ onAccepted: popup.remove()
}
}
}
diff --git a/src/gui/Popups/SignOutPopup.qml b/src/gui/Popups/SignOutPopup.qml
index 8b512a4a..e0df1347 100644
--- a/src/gui/Popups/SignOutPopup.qml
+++ b/src/gui/Popups/SignOutPopup.qml
@@ -2,61 +2,74 @@
import QtQuick 2.12
import ".."
+import "../Base/ButtonLayout"
-BoxPopup {
+HFlickableColumnPopup {
id: popup
- summary.text: qsTr("Backup your decryption keys before signing out?")
- details.text: qsTr(
- "Signing out will delete your device's information and the keys " +
- "required to decrypt messages in encrypted rooms.\n\n" +
- "You can export your keys to a passphrase-protected file " +
- "before signing out.\n\n" +
- "This will allow you to restore access to your messages when " +
- "you sign in again, by importing this file in your account settings."
- )
+ property string userId: ""
- box.focusButton: "ok"
- box.buttonModel: [
- { name: "ok", text: qsTr("Export keys"), iconName: "export-keys" },
- { name: "signout", text: qsTr("Sign out now"), iconName: "sign-out",
- iconColor: theme.colors.middleBackground },
- { name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
- ]
- box.buttonCallbacks: ({
- ok: button => {
- utils.makeObject(
+ page.footer: ButtonLayout {
+ ApplyButton {
+ id: exportButton
+ text: qsTr("Export keys")
+ icon.name: "export-keys"
+
+ onClicked: utils.makeObject(
"Dialogs/ExportKeys.qml",
window.mainUI,
{ userId },
obj => {
- button.loading = Qt.binding(() => obj.exporting)
- obj.done.connect(() => {
- box.buttonCallbacks["signout"](button)
- })
+ loading = Qt.binding(() => obj.exporting)
+ obj.done.connect(signOutButton.clicked)
obj.dialog.open()
}
)
- },
+ }
- signout: button => {
- okClicked = true
- popup.ok()
+ OtherButton {
+ id: signOutButton
+ text: qsTr("Sign out now")
+ icon.name: "sign-out"
+ icon.color: theme.colors.middleBackground
- if (ModelStore.get("accounts").count < 2 ||
- window.uiState.pageProperties.userId === userId) {
- window.mainUI.pageLoader.showPage("AddAccount/AddAccount")
+ onClicked: {
+ if (ModelStore.get("accounts").count < 2 ||
+ window.uiState.pageProperties.userId === userId)
+ {
+ window.mainUI.pageLoader.showPage("AddAccount/AddAccount")
+ }
+
+ py.callCoro("logout_client", [userId])
+ popup.close()
}
+ }
- py.callCoro("logout_client", [userId])
- popup.close()
- },
+ CancelButton {
+ onClicked: popup.close()
+ }
+ }
- cancel: button => { okClicked = false; popup.cancel(); popup.close() },
- })
+ onOpened: exportButton.forceActiveFocus()
- property string userId: ""
+ SummaryLabel {
+ text: qsTr("Backup your decryption keys before signing out?")
+ }
+
+ DetailsLabel {
+ text: qsTr(
+ "Signing out will delete your device's information and the keys " +
+ "required to decrypt messages in encrypted rooms.\n\n" +
+
+ "You can export your keys to a passphrase-protected file " +
+ "before signing out.\n\n" +
+
+ "This will allow you to restore access to your messages when " +
+ "you sign in again, by importing this file in your account " +
+ "settings."
+ )
+ }
}
diff --git a/src/gui/Popups/SummaryLabel.qml b/src/gui/Popups/SummaryLabel.qml
new file mode 100644
index 00000000..82d2c210
--- /dev/null
+++ b/src/gui/Popups/SummaryLabel.qml
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Layouts 1.12
+import "../Base"
+
+HLabel {
+ wrapMode: Text.Wrap
+ font.bold: true
+ visible: Boolean(text)
+
+ Layout.fillWidth: true
+}
diff --git a/src/gui/Popups/UnexpectedErrorPopup.qml b/src/gui/Popups/UnexpectedErrorPopup.qml
index fd72e354..e97b128b 100644
--- a/src/gui/Popups/UnexpectedErrorPopup.qml
+++ b/src/gui/Popups/UnexpectedErrorPopup.qml
@@ -4,16 +4,10 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../Base"
+import "../Base/ButtonLayout"
-BoxPopup {
- summary.text: qsTr("Unexpected error occured: %1").arg(errorType)
- summary.textFormat: Text.StyledText
-
- okText: qsTr("Report")
- okIcon: "report-error"
- okEnabled: false // TODO
- cancelText: qsTr("Ignore")
- box.focusButton: "cancel"
+HColumnPopup {
+ id: popup
property string errorType
@@ -21,17 +15,44 @@ BoxPopup {
property string traceback: ""
+ page.footer: ButtonLayout {
+ ApplyButton {
+ text: qsTr("Report")
+ icon.name: "report-error"
+ enabled: false // TODO
+ }
+
+ CancelButton {
+ id: cancelButton
+ text: qsTr("Ignore")
+ onClicked: popup.close()
+ }
+ }
+
+ onOpened: cancelButton.forceActiveFocus()
+
+
+ SummaryLabel {
+ text: qsTr("Unexpected error occured: %1").arg(errorType)
+ textFormat: Text.StyledText
+ }
+
HScrollView {
+ clip: true
+
Layout.fillWidth: true
+ Layout.fillHeight: true
HTextArea {
text: [message, traceback].join("\n\n") || qsTr("No info available")
readOnly: true
font.family: theme.fontFamily.mono
+ focusOnTab: hideCheckBox
}
}
HCheckBox {
+ id: hideCheckBox
text: qsTr("Hide this type of error until restart")
onCheckedChanged:
checked ?
diff --git a/src/gui/ShortcutBundles/TabShortcuts.qml b/src/gui/ShortcutBundles/TabShortcuts.qml
index ba4abaf7..52759de8 100644
--- a/src/gui/ShortcutBundles/TabShortcuts.qml
+++ b/src/gui/ShortcutBundles/TabShortcuts.qml
@@ -8,7 +8,7 @@ HQtObject {
property Item container: parent
- property bool active: true
+ property bool active: container.count > 1
HShortcut {
diff --git a/src/icons/thin/check-mark-partial.svg b/src/icons/thin/check-mark-partial.svg
new file mode 100644
index 00000000..c916d92b
--- /dev/null
+++ b/src/icons/thin/check-mark-partial.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-action-menu.svg b/src/icons/thin/device-action-menu.svg
index 1de1c096..5bf163ca 100644
--- a/src/icons/thin/device-action-menu.svg
+++ b/src/icons/thin/device-action-menu.svg
@@ -1,3 +1,7 @@
diff --git a/src/icons/thin/device-blacklisted.svg b/src/icons/thin/device-blacklisted.svg
new file mode 100644
index 00000000..85cba558
--- /dev/null
+++ b/src/icons/thin/device-blacklisted.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-current.svg b/src/icons/thin/device-current.svg
new file mode 100644
index 00000000..807d6aa2
--- /dev/null
+++ b/src/icons/thin/device-current.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-delete-checked.svg b/src/icons/thin/device-delete-checked.svg
new file mode 100644
index 00000000..5609acb3
--- /dev/null
+++ b/src/icons/thin/device-delete-checked.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-delete.svg b/src/icons/thin/device-delete.svg
new file mode 100644
index 00000000..5609acb3
--- /dev/null
+++ b/src/icons/thin/device-delete.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-ignored.svg b/src/icons/thin/device-ignored.svg
new file mode 100644
index 00000000..807d6aa2
--- /dev/null
+++ b/src/icons/thin/device-ignored.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-rename.svg b/src/icons/thin/device-rename.svg
new file mode 100644
index 00000000..5136cd7d
--- /dev/null
+++ b/src/icons/thin/device-rename.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-unset.svg b/src/icons/thin/device-unset.svg
new file mode 100644
index 00000000..3df91554
--- /dev/null
+++ b/src/icons/thin/device-unset.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-verified.svg b/src/icons/thin/device-verified.svg
new file mode 100644
index 00000000..7cd47f35
--- /dev/null
+++ b/src/icons/thin/device-verified.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/icons/thin/device-verify.svg b/src/icons/thin/device-verify.svg
new file mode 100644
index 00000000..96b915db
--- /dev/null
+++ b/src/icons/thin/device-verify.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/themes/Glass.qpl b/src/themes/Glass.qpl
index f2726d1a..e30e4c4a 100644
--- a/src/themes/Glass.qpl
+++ b/src/themes/Glass.qpl
@@ -66,9 +66,10 @@ colors:
color halfDimText: hsluv(0, 0, intensity * 72)
color dimText: hsluv(0, 0, intensity * 60)
- color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
- color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
- color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
+ color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
+ color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
+ color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
+ color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)
diff --git a/src/themes/Midnight.qpl b/src/themes/Midnight.qpl
index 8e0ee84c..e41718a2 100644
--- a/src/themes/Midnight.qpl
+++ b/src/themes/Midnight.qpl
@@ -69,9 +69,10 @@ colors:
color halfDimText: hsluv(0, 0, intensity * 72)
color dimText: hsluv(0, 0, intensity * 60)
- color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
- color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
- color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
+ color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
+ color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
+ color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
+ color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)