Rework HBox-based pages and account settings

- Refactor everything about HBox, and adapt all the pages and popups
  that used it

- Replace HTabContainer by HTabbedBox

- Make boxes swippable

- Make esc presses in boxes click the cancel button

- Make all boxes and popups scrollable when needed

- Replace generic apply button icons in popups

- Fix tab focus for error and invite popups

- Rework (still WIP) the account settings page:
  - Use the standard tabbed design of other pages
  - Ditch the horizontal profile layout, hacky and impossible to extend
  - Add real-time coloring for the display name field

- Implement a device list in account settings (Sessions, still WIP)
This commit is contained in:
miruka 2020-06-25 08:32:08 -04:00
parent 72bd78c77e
commit da4a5ab5cd
66 changed files with 1594 additions and 1173 deletions

25
TODO.md
View File

@ -1,19 +1,27 @@
# TODO # 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 ## 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, - Reorder QML object declarations,
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
## Issues ## 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 - Don't send typing notification when switching to a room where the composer
has preloaded text has preloaded text
@ -21,9 +29,6 @@
the marker will only be updated for accounts that have already received the marker will only be updated for accounts that have already received
it (server lag) 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 - Jumping between accounts (clicking in account bar or alt+(Shift+)N) is
laggy with hundreds of rooms in between laggy with hundreds of rooms in between
- On startup, if a room's last event is a membership change, - On startup, if a room's last event is a membership change,

View File

@ -12,7 +12,6 @@ import sys
import traceback import traceback
from contextlib import suppress from contextlib import suppress
from copy import deepcopy from copy import deepcopy
from dataclasses import asdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
@ -1261,8 +1260,11 @@ class MatrixClient(nio.AsyncClient):
async def devices_info(self) -> List[Dict[str, Any]]: async def devices_info(self) -> List[Dict[str, Any]]:
"""Get list of devices and their info for our user.""" """Get list of devices and their info for our user."""
def get_trust(device_id: str) -> str: def get_type(device_id: str) -> str:
# Returns "verified", "blacklisted", "ignored" or "unset" # 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]: if device_id not in self.olm.device_store[self.user_id]:
return "unset" return "unset"
@ -1274,19 +1276,27 @@ class MatrixClient(nio.AsyncClient):
{ {
"id": device.id, "id": device.id,
"display_name": device.display_name or "", "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_date": device.last_seen_date or ZeroDate,
"last_seen_country": "", "last_seen_country": "",
"trusted": get_trust(device.id) == "verified", "type": get_type(device.id),
"blacklisted": get_trust(device.id) == "blacklisted",
} }
for device in (await self.devices()).devices 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( return sorted(
devices, devices,
# The current device will always be first key = lambda d: (types_order[d["type"]], d["last_seen_date"]),
key = lambda d: (d["id"] == self.device_id, d["last_seen_date"]),
reverse = True, reverse = True,
) )

View File

@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
import ".." import ".."
HButton { HButton {
implicitHeight: theme.baseElementsHeight
text: qsTr("Apply") text: qsTr("Apply")
icon.name: "apply" icon.name: "apply"
icon.color: theme.colors.positiveBackground icon.color: theme.colors.positiveBackground
Layout.preferredHeight: theme.baseElementsHeight
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
import ".." import ".."
HButton { HButton {
implicitHeight: theme.baseElementsHeight
text: qsTr("Cancel") text: qsTr("Cancel")
icon.name: "cancel" icon.name: "cancel"
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
Layout.preferredHeight: theme.baseElementsHeight
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@ -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
}

View File

@ -3,34 +3,17 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
Rectangle { HFlickableColumnPage {
id: box implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
implicitHeight: Math.min(parent.height, flickable.contentHeight)
// XXX
// Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
// Keys.onEnterPressed: Keys.onReturnPressed(event)
background: Rectangle {
color: theme.controls.box.background color: theme.controls.box.background
radius: theme.controls.box.radius radius: theme.controls.box.radius
implicitWidth: theme.controls.box.defaultWidth
implicitHeight: childrenRect.height
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()
}
} }
@ -41,86 +24,6 @@ Rectangle {
overshoot: 3 overshoot: 3
} }
HColumnLayout { Behavior on implicitWidth { HNumberAnimation {} }
id: mainColumn Behavior on implicitHeight { HNumberAnimation {} }
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),
)
}
}
}
}
} }

View File

@ -6,7 +6,6 @@ import QtQuick.Layouts 1.12
HRowLayout { HRowLayout {
id: buttonContent id: buttonContent
implicitHeight: theme.baseElementsHeight
spacing: button.spacing spacing: button.spacing
opacity: loading ? theme.loadingElementsOpacity : opacity: loading ? theme.loadingElementsOpacity :
enabled ? 1 : theme.disabledElementsOpacity enabled ? 1 : theme.disabledElementsOpacity

View File

@ -7,7 +7,7 @@ import QtQuick.Layouts 1.12
CheckBox { CheckBox {
id: box id: box
checked: defaultChecked checked: defaultChecked
spacing: theme.spacing spacing: contentItem.visible ? theme.spacing : 0
padding: 0 padding: 0
indicator: Rectangle { indicator: Rectangle {
@ -33,21 +33,26 @@ CheckBox {
HIcon { HIcon {
anchors.centerIn: parent anchors.centerIn: parent
dimension: parent.width - 2 dimension: parent.width - 2
svgName: "check-mark"
colorize: theme.controls.checkBox.checkIconColorize 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 { Behavior on scale {
HNumberAnimation { HNumberAnimation {
overshoot: 4 overshoot: 4
easing.type: Easing.InOutBack easing.type: Easing.InOutBack
factor: 0.5
} }
} }
} }
} }
contentItem: HColumnLayout { contentItem: HColumnLayout {
visible: mainText.text || subtitleText.text
opacity: box.enabled ? 1 : theme.disabledElementsOpacity opacity: box.enabled ? 1 : theme.disabledElementsOpacity
HLabel { HLabel {

View File

@ -7,10 +7,16 @@ HPage {
default property alias columnData: column.data default property alias columnData: column.data
property alias column: column
implicitWidth: theme.controls.box.defaultWidth
contentHeight: column.childrenRect.height
HColumnLayout { HColumnLayout {
id: column id: column
anchors.fill: parent anchors.fill: parent
spacing: theme.spacing * 1.5
} }
} }

View File

@ -5,6 +5,9 @@ import "../ShortcutBundles"
HPage { HPage {
id: page id: page
implicitWidth: theme.controls.box.defaultWidth
contentHeight:
flickable.contentHeight + flickable.topMargin + flickable.bottomMargin
default property alias columnData: column.data default property alias columnData: column.data
@ -19,9 +22,14 @@ HPage {
HFlickable { HFlickable {
id: flickable id: flickable
anchors.fill: parent anchors.fill: parent
clip: true
contentWidth: parent.width 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 { FlickShortcuts {
id: flickShortcuts id: flickShortcuts
@ -31,13 +39,9 @@ HPage {
HColumnLayout { HColumnLayout {
id: column id: column
x: padding width:
y: padding flickable.width - flickable.leftMargin - flickable.rightMargin
width: flickable.width - padding * 2 spacing: theme.spacing * 1.5
height: flickable.height - padding * 2
property int padding:
page.currentSpacing < theme.spacing ? 0 : page.currentSpacing
} }
} }

View File

@ -9,7 +9,7 @@ HColumnLayout {
default property alias insideData: itemHolder.data 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 label: label
readonly property alias errorLabel: errorLabel readonly property alias errorLabel: errorLabel
readonly property alias toolTip: toolTip readonly property alias toolTip: toolTip

View File

@ -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
}
}

View File

@ -12,7 +12,11 @@ Page {
property int currentSpacing: property int currentSpacing:
useVariableSpacing ? useVariableSpacing ?
Math.min(theme.spacing * width / 400, theme.spacing) : Math.min(
theme.spacing * width / 400,
theme.spacing * height / 400,
theme.spacing,
) :
theme.spacing theme.spacing

View File

@ -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
}
}
}

View File

@ -6,12 +6,15 @@ import CppUtils 0.1
Popup { Popup {
id: popup id: popup
anchors.centerIn: Overlay.overlay
modal: true modal: true
focus: true focus: true
padding: 0 padding: 0
margins: theme.spacing 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 { enter: Transition {
HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 } HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 }
} }

View File

@ -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 {} }
}
}

View File

@ -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()
}
}

View File

@ -37,8 +37,8 @@ HFileDialogOpener {
PasswordPopup { PasswordPopup {
id: exportPasswordPopup id: exportPasswordPopup
details.text: qsTr("Passphrase to protect this file:") summary.text: qsTr("Passphrase to protect this file:")
okText: qsTr("Export") validateButton.text: qsTr("Export")
onAcceptedPasswordChanged: exportKeys(file, acceptedPassword) onAcceptedPasswordChanged: exportKeys(file, acceptedPassword)

View File

@ -16,17 +16,16 @@ HFileDialogOpener {
property string userId: "" property string userId: ""
property bool importing: false
property Future importFuture: null property Future importFuture: null
PasswordPopup { PasswordPopup {
id: importPasswordPopup id: importPasswordPopup
details.text: summary.text:
importing ? importFuture ?
qsTr("This might take a while...") : qsTr("This might take a while...") :
qsTr("Passphrase used to protect this file:") qsTr("Passphrase used to protect this file:")
okText: qsTr("Import") validateButton.text: qsTr("Import")
onClosed: if (importFuture) importFuture.cancel() onClosed: if (importFuture) importFuture.cancel()
@ -35,13 +34,10 @@ HFileDialogOpener {
function verifyPassword(pass, callback) { function verifyPassword(pass, callback) {
importing = true
const call = py.callClientCoro const call = py.callClientCoro
const path = file.toString().replace(/^file:\/\//, "") const path = file.toString().replace(/^file:\/\//, "")
importFuture = call(userId, "import_keys", [path, pass], () => { importFuture = call(userId, "import_keys", [path, pass], () => {
importing = false
importFuture = null importFuture = null
callback(true) callback(true)
@ -78,7 +74,7 @@ HFileDialogOpener {
Binding on closePolicy { Binding on closePolicy {
value: Popup.CloseOnEscape value: Popup.CloseOnEscape
when: importing when: importFuture
} }
} }
} }

View File

@ -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:<br>%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])
}
}
}

View File

@ -6,55 +6,26 @@ import QtQuick.Layouts 1.12
import "../.." import "../.."
import "../../Base" import "../../Base"
HFlickableColumnPage { HPage {
id: accountSettings id: page
title: qsTr("Account settings")
header: HPageHeader {}
property int avatarPreferredSize: 256 * theme.uiScale property string userId
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
HSpacer {} HTabbedBox {
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 anchors.centerIn: parent
width: ready ? parent.width : 96 width: Math.min(implicitWidth, page.availableWidth)
source: ready ? height: Math.min(implicitHeight, page.availableHeight)
modelData :
(modelData === "Profile.qml" ? header: HTabBar {
"../../Base/HBusyIndicator.qml" : "") 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 }
}
} }

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -3,41 +3,53 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
buttonModel: [ id: page
{ name: "export", text: qsTr("Export"), iconName: "export-keys"},
{ name: "import", text: qsTr("Import"), iconName: "import-keys"},
]
buttonCallbacks: ({
export: button => { property string userId
utils.makeObject(
function takeFocus() { exportButton.forceActiveFocus() }
footer: ButtonLayout {
OtherButton {
id: exportButton
text: qsTr("Export")
icon.name: "export-keys"
onClicked: utils.makeObject(
"Dialogs/ExportKeys.qml", "Dialogs/ExportKeys.qml",
accountSettings, page,
{ userId: accountSettings.userId }, { userId: page.userId },
obj => { obj => {
button.loading = Qt.binding(() => obj.exporting) loading = Qt.binding(() => obj.exporting)
obj.dialog.open() obj.dialog.open()
} }
) )
}, }
import: button => {
utils.makeObject( OtherButton {
text: qsTr("Import")
icon.name: "import-keys"
onClicked: utils.makeObject(
"Dialogs/ImportKeys.qml", "Dialogs/ImportKeys.qml",
accountSettings, page,
{ userId: accountSettings.userId }, { userId: page.userId },
obj => { obj.dialog.open() } obj => { obj.dialog.open() }
) )
}, }
}) }
HLabel { HLabel {
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: qsTr( text: qsTr(
"The decryption keys for messages received in encrypted rooms " + "The decryption keys for messages received in encrypted rooms " +
"<b>until present time</b> can be backed up " + "<b>until present time</b> can be saved " +
"to a passphrase-protected file.<br><br>" + "to a passphrase-protected file.<br><br>" +
"You can then import this file on any Matrix account or " + "You can then import this file on any Matrix account or " +

View File

@ -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:<br>%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
}
}
}
}

View File

@ -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 } }
}
}
}

View File

@ -4,16 +4,21 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
HFlickableColumnPage { HPage {
title: qsTr("Add an account") id: page
header: HPageHeader {}
HTabContainer { HTabbedBox {
tabModel: [ anchors.centerIn: parent
qsTr("Sign in"), qsTr("Register"), qsTr("Reset"), 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 {} Register {}
Reset {} Reset {}
} }

View File

@ -3,28 +3,30 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: signInBox function takeFocus() { registerButton.forceActiveFocus() }
clickButtonOnEnter: "ok"
buttonModel: [
{ name: "ok", text: qsTr("Register from Riot"), iconName: "register" },
]
buttonCallbacks: ({ footer: ButtonLayout {
ok: button => { ApplyButton {
Qt.openUrlExternally("https://riot.im/app/#/register") id: registerButton
text: qsTr("Register from Riot")
icon.name: "register"
onClicked: Qt.openUrlExternally("https://riot.im/app/#/register")
Layout.fillWidth: true
}
} }
})
HLabel { HLabel {
wrapMode: Text.Wrap wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
text: qsTr( text: qsTr(
"Not yet implemented\n\nYou can create a new " + "Not implemented yet\n\n" +
"account from another client such as Riot." "You can create a new account from another client such as Riot."
) )
Layout.fillWidth: true Layout.fillWidth: true

View File

@ -3,32 +3,31 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: signInBox function takeFocus() { resetButton.forceActiveFocus() }
clickButtonOnEnter: "ok"
buttonModel: [
{
name: "ok",
text: qsTr("Reset password from Riot"),
iconName: "reset-password"
},
]
buttonCallbacks: ({ footer: ButtonLayout {
ok: button => { ApplyButton {
id: resetButton
text: qsTr("Reset password from Riot")
icon.name: "reset-password"
onClicked:
Qt.openUrlExternally("https://riot.im/app/#/forgot_password") Qt.openUrlExternally("https://riot.im/app/#/forgot_password")
Layout.fillWidth: true
}
} }
})
HLabel { HLabel {
wrapMode: Text.Wrap wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
text: qsTr( text: qsTr(
"Not yet implemented\n\nYou can reset your " + "Not implemented yet\n\n" +
"password using another client such as Riot." "You can reset your password from another client such as Riot."
) )
Layout.fillWidth: true Layout.fillWidth: true

View File

@ -3,28 +3,25 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: signInBox id: page
clickButtonOnEnter: "apply"
onFocusChanged: idField.item.forceActiveFocus()
buttonModel: [ property var loginFuture: null
{
name: "apply",
text: qsTr("Sign in"),
enabled: canSignIn,
iconName: "sign-in",
loading: loginFuture !== null,
disableWhileLoading: false,
},
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel"},
]
buttonCallbacks: ({ property string signInWith: "username"
apply: button => {
if (loginFuture) loginFuture.cancel() readonly property bool canSignIn:
serverField.item.text.trim() && idField.item.text.trim() &&
passwordField.item.text && ! serverField.item.error
function takeFocus() { idField.item.forceActiveFocus() }
function signIn() {
if (page.loginFuture) page.loginFuture.cancel()
signInTimeout.restart() signInTimeout.restart()
@ -35,10 +32,10 @@ HBox {
undefined, serverField.item.text.trim(), undefined, serverField.item.text.trim(),
] ]
loginFuture = py.callCoro("login_client", args, userId => { page.loginFuture = py.callCoro("login_client", args, userId => {
signInTimeout.stop() signInTimeout.stop()
errorMessage.text = "" errorMessage.text = ""
loginFuture = null page.loginFuture = null
py.callCoro( py.callCoro(
rememberAccount.checked ? rememberAccount.checked ?
@ -52,7 +49,7 @@ HBox {
) )
}, (type, args, error, traceback, uuid) => { }, (type, args, error, traceback, uuid) => {
loginFuture = null page.loginFuture = null
signInTimeout.stop() signInTimeout.stop()
let txt = qsTr( let txt = qsTr(
@ -69,25 +66,33 @@ HBox {
errorMessage.text = txt errorMessage.text = txt
}) })
}, }
cancel: button => { function cancel() {
if (! loginFuture) return if (! page.loginFuture) return
signInTimeout.stop() signInTimeout.stop()
loginFuture.cancel() page.loginFuture.cancel()
loginFuture = null page.loginFuture = null
} }
})
property var 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()
}
property string signInWith: "username" CancelButton {
onClicked: page.cancel()
}
}
readonly property bool canSignIn: Keys.onEscapePressed: page.cancel()
serverField.item.text.trim() && idField.item.text.trim() &&
passwordField.item.text && ! serverField.item.error
Timer { Timer {
@ -120,10 +125,10 @@ HBox {
HButton { HButton {
icon.name: modelData icon.name: modelData
circle: true circle: true
checked: signInWith === modelData checked: page.signInWith === modelData
enabled: modelData === "username" enabled: modelData === "username"
autoExclusive: true autoExclusive: true
onClicked: signInWith = modelData onClicked: page.signInWith = modelData
} }
} }
} }
@ -131,8 +136,8 @@ HBox {
HLabeledItem { HLabeledItem {
id: idField id: idField
label.text: qsTr( label.text: qsTr(
signInWith === "email" ? "Email:" : page.signInWith === "email" ? "Email:" :
signInWith === "phone" ? "Phone:" : page.signInWith === "phone" ? "Phone:" :
"Username:" "Username:"
) )
@ -157,9 +162,6 @@ HBox {
HLabeledItem { HLabeledItem {
id: serverField id: serverField
label.text: qsTr("Homeserver:")
Layout.fillWidth: true
// 2019-11-11 https://www.hello-matrix.net/public_servers.php // 2019-11-11 https://www.hello-matrix.net/public_servers.php
readonly property var knownServers: [ readonly property var knownServers: [
@ -182,6 +184,10 @@ HBox {
readonly property bool knownServerChosen: readonly property bool knownServerChosen:
knownServers.includes(item.cleanText) knownServers.includes(item.cleanText)
label.text: qsTr("Homeserver:")
Layout.fillWidth: true
HTextField { HTextField {
width: parent.width width: parent.width
text: "https://matrix.org" text: "https://matrix.org"

View File

@ -2,27 +2,28 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../.."
import "../../Base" import "../../Base"
HFlickableColumnPage { HPage {
id: addChatPage id: page
title: qsTr("Add new chat")
header: HPageHeader {}
property string userId 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 { header: HTabBar {
tabModel: [ HTabButton { text: qsTr("Direct chat") }
qsTr("Direct chat"), qsTr("Join room"), qsTr("Create room"), HTabButton { text: qsTr("Join room") }
] HTabButton { text: qsTr("Create room") }
}
DirectChat { Component.onCompleted: forceActiveFocus() } DirectChat { userId: page.userId }
JoinRoom {} JoinRoom { userId: page.userId }
CreateRoom {} CreateRoom { userId: page.userId }
} }
} }

View File

@ -2,22 +2,22 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../.."
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: addChatBox id: page
clickButtonOnEnter: "apply"
onFocusChanged: nameField.item.forceActiveFocus()
buttonModel: [ property string userId
{ name: "apply", text: qsTr("Create"), iconName: "room-create" }, readonly property QtObject account: ModelStore.get("accounts").find(userId)
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
]
buttonCallbacks: ({
apply: button => { function takeFocus() { nameField.item.forceActiveFocus() }
button.loading = true
function create() {
applyButton.loading = true
errorMessage.text = "" errorMessage.text = ""
const args = [ const args = [
@ -29,30 +29,42 @@ HBox {
] ]
py.callClientCoro(userId, "new_group_chat", args, roomId => { py.callClientCoro(userId, "new_group_chat", args, roomId => {
button.loading = false applyButton.loading = false
pageLoader.showRoom(userId, roomId) pageLoader.showRoom(userId, roomId)
mainPane.roomList.startCorrectItemSearch() mainPane.roomList.startCorrectItemSearch()
}, (type, args) => { }, (type, args) => {
button.loading = false applyButton.loading = false
errorMessage.text = errorMessage.text =
qsTr("Unknown error - %1: %2").arg(type).arg(args) qsTr("Unknown error - %1: %2").arg(type).arg(args)
}) })
}, }
cancel: button => { function cancel() {
nameField.item.text = "" nameField.item.reset()
topicArea.item.text = "" topicArea.item.reset()
publicCheckBox.checked = false publicCheckBox.reset()
encryptCheckBox.checked = false encryptCheckBox.reset()
blockOtherServersCheckBox.checked = false blockOtherServersCheckBox.reset()
pageLoader.showPrevious() pageLoader.showPrevious()
} }
})
readonly property string userId: addChatPage.userId footer: ButtonLayout {
ApplyButton {
id: applyButton
text: qsTr("Create")
icon.name: "room-create"
onClicked: create()
}
CancelButton {
onClicked: cancel()
}
}
Keys.onEscapePressed: cancel()
HRoomAvatar { HRoomAvatar {
@ -70,6 +82,9 @@ HBox {
opacity: nameField.item.text ? 0 : 1 opacity: nameField.item.text ? 0 : 1
visible: opacity > 0 visible: opacity > 0
userId: page.userId
account: page.account
Behavior on opacity { HNumberAnimation {} } Behavior on opacity { HNumberAnimation {} }
} }
} }

View File

@ -1,10 +1,17 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
HUserAvatar { HUserAvatar {
userId: addChatPage.userId property QtObject account
displayName: addChatPage.account ? addChatPage.account.display_name : ""
mxc: addChatPage.account ? addChatPage.account.avatar_url : "" // userId: (set me)
displayName: account ? account.display_name : ""
mxc: account ? account.avatar_url : ""
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: 128
Layout.preferredHeight: Layout.preferredWidth
} }

View File

@ -2,39 +2,36 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../.."
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: addChatBox id: page
clickButtonOnEnter: "apply"
onFocusChanged: userField.item.forceActiveFocus()
buttonModel: [ property string userId
{ readonly property QtObject account: ModelStore.get("accounts").find(userId)
name: "apply",
text: qsTr("Start chat"),
iconName: "start-direct-chat",
enabled: Boolean(userField.item.text.trim())
},
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
]
buttonCallbacks: ({
apply: button => { function takeFocus() {
button.loading = true userField.item.forceActiveFocus()
}
function startChat() {
applyButton.loading = true
errorMessage.text = "" errorMessage.text = ""
const args = [userField.item.text.trim(), encryptCheckBox.checked] const args = [userField.item.text.trim(), encryptCheckBox.checked]
py.callClientCoro(userId, "new_direct_chat", args, roomId => { py.callClientCoro(userId, "new_direct_chat", args, roomId => {
button.loading = false applyButton.loading = false
errorMessage.text = "" errorMessage.text = ""
pageLoader.showRoom(userId, roomId) pageLoader.showRoom(userId, roomId)
mainPane.roomList.startCorrectItemSearch() mainPane.roomList.startCorrectItemSearch()
}, (type, args) => { }, (type, args) => {
button.loading = false applyButton.loading = false
let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
@ -56,23 +53,40 @@ HBox {
errorMessage.text = txt errorMessage.text = txt
}) })
}, }
cancel: button => { 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 = "" userField.item.text = ""
errorMessage.text = "" errorMessage.text = ""
pageLoader.showPrevious() pageLoader.showPrevious()
} }
}) }
}
Keys.onEscapePressed: cancel()
readonly property string userId: addChatPage.userId
CurrentUserAvatar { CurrentUserAvatar {
Layout.alignment: Qt.AlignCenter userId: page.userId
Layout.preferredWidth: 128 account: page.account
Layout.preferredHeight: Layout.preferredWidth
} }
HLabeledItem { HLabeledItem {

View File

@ -2,39 +2,36 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../.."
import "../../Base" import "../../Base"
import "../../Base/ButtonLayout"
HBox { HFlickableColumnPage {
id: addChatBox id: page
clickButtonOnEnter: "apply"
onFocusChanged: roomField.item.forceActiveFocus()
buttonModel: [ property string userId
{ readonly property QtObject account: ModelStore.get("accounts").find(userId)
name: "apply",
text: qsTr("Join"),
iconName: "room-join",
enabled: Boolean(roomField.item.text.trim()),
},
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
]
buttonCallbacks: ({
apply: button => { function takeFocus() {
button.loading = true roomField.item.forceActiveFocus()
}
function join() {
joinButton.loading = true
errorMessage.text = "" errorMessage.text = ""
const args = [roomField.item.text.trim()] const args = [roomField.item.text.trim()]
py.callClientCoro(userId, "room_join", args, roomId => { py.callClientCoro(userId, "room_join", args, roomId => {
button.loading = false joinButton.loading = false
errorMessage.text = "" errorMessage.text = ""
pageLoader.showRoom(userId, roomId) pageLoader.showRoom(userId, roomId)
mainPane.roomList.startCorrectItemSearch() mainPane.roomList.startCorrectItemSearch()
}, (type, args) => { }, (type, args) => {
button.loading = false joinButton.loading = false
let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
@ -49,23 +46,35 @@ HBox {
errorMessage.text = txt errorMessage.text = txt
}) })
}, }
function cancel() {
roomField.item.reset()
errorMessage.reset()
cancel: button => {
roomField.item.text = ""
errorMessage.text = ""
pageLoader.showPrevious() pageLoader.showPrevious()
} }
})
readonly property string userId: addChatPage.userId footer: ButtonLayout {
ApplyButton {
text: qsTr("Join")
icon.name: "room-join"
enabled: Boolean(roomField.item.text.trim())
onClicked: join()
}
CancelButton {
onClicked: cancel()
}
}
Keys.onEscapePressed: cancel()
CurrentUserAvatar { CurrentUserAvatar {
Layout.alignment: Qt.AlignCenter userId: page.userId
Layout.preferredWidth: 128 account: page.account
Layout.preferredHeight: Layout.preferredWidth
} }
HLabeledItem { HLabeledItem {

View File

@ -11,6 +11,7 @@ import "Timeline"
HColumnPage { HColumnPage {
id: chatPage id: chatPage
padding: 0 padding: 0
column.spacing: 0
onLoadEventListChanged: if (loadEventList) loadedOnce = true onLoadEventListChanged: if (loadEventList) loadedOnce = true
Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel() Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel()

View File

@ -67,10 +67,7 @@ HTile {
roomId: chat.roomId, roomId: chat.roomId,
targetUserId: model.id, targetUserId: model.id,
targetDisplayName: model.display_name, targetDisplayName: model.display_name,
operation: operation: model.invited ? "disinvite" : "kick",
model.invited ?
RemoveMemberPopup.Operation.Disinvite :
RemoveMemberPopup.Operation.Kick,
}) })
Component.onCompleted: py.callClientCoro( Component.onCompleted: py.callClientCoro(
@ -94,7 +91,7 @@ HTile {
roomId: chat.roomId, roomId: chat.roomId,
targetUserId: model.id, targetUserId: model.id,
targetDisplayName: model.display_name, targetDisplayName: model.display_name,
operation: RemoveMemberPopup.Operation.Ban, operation: "ban",
}) })
Component.onCompleted: py.callClientCoro( Component.onCompleted: py.callClientCoro(

View File

@ -57,9 +57,6 @@ HFlickableColumnPage {
} }
useVariableSpacing: false
column.spacing: theme.spacing * 1.5
flickShortcuts.active: flickShortcuts.active:
! mainUI.debugConsole.visible && ! chat.composerHasFocus ! mainUI.debugConsole.visible && ! chat.composerHasFocus
@ -82,6 +79,8 @@ HFlickableColumnPage {
} }
} }
Keys.onEscapePressed: cancel()
HRoomAvatar { HRoomAvatar {
id: avatar id: avatar

View File

@ -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
}
}
}

View File

@ -1,19 +1,42 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
summary.text: qsTr("Clear this room's messages?") id: popup
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])
property string userId: "" property string userId: ""
property string roomId: "" 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()
} }

View File

@ -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
}

View File

@ -1,22 +1,21 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
id: popup id: popup
summary.text: qsTr("Leave <i>%1</i> 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") property string userId: ""
box.focusButton: "ok" property string roomId: ""
property string roomName: ""
onOk: py.callClientCoro(userId, "room_forget", [roomId], () => { property bool canDestroy: false
function forget() {
py.callClientCoro(userId, "room_forget", [roomId], () => {
if (window.uiState.page === "Pages/Chat/Chat.qml" && if (window.uiState.page === "Pages/Chat/Chat.qml" &&
window.uiState.pageProperties.userId === userId && window.uiState.pageProperties.userId === userId &&
window.uiState.pageProperties.roomId === roomId) window.uiState.pageProperties.roomId === roomId)
@ -27,14 +26,41 @@ BoxPopup {
Qt.callLater(popup.destroy) Qt.callLater(popup.destroy)
} }
}) })
}
onCancel: canDestroy = true
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) onClosed: if (canDestroy) Qt.callLater(popup.destroy)
property string userId: "" SummaryLabel {
property string roomId: "" text: qsTr("Leave <i>%1</i> and lose the history?").arg(roomName)
property string roomName: "" textFormat: Text.StyledText
}
property bool canDestroy: false
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."
)
}
} }

View File

@ -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
}
}

View File

@ -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,
)
}
}

View File

@ -4,23 +4,24 @@ import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Base/ButtonLayout"
BoxPopup { HColumnPopup {
id: popup id: popup
// fillAvailableHeight: true
summary.text: qsTr("Invite members to <i>%1</i>").arg(roomName)
summary.textFormat: Text.StyledText
okText: qsTr("Invite")
okEnabled: invitingAllowed && Boolean(inviteArea.text.trim())
onOpened: inviteArea.forceActiveFocus()
onInvitingAllowedChanged: property string userId
if (! invitingAllowed && inviteFuture) inviteFuture.cancel() property string roomId
property string roomName
property bool invitingAllowed: true
box.buttonCallbacks: ({ property var inviteFuture: null
ok: button => { property var successfulInvites: []
button.loading = true property var failedInvites: []
function invite() {
inviteButton.loading = true
const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter( const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter(
user => ! successfulInvites.includes(user) user => ! successfulInvites.includes(user)
@ -39,35 +40,48 @@ BoxPopup {
successfulInvites = successes successfulInvites = successes
failedInvites = errors failedInvites = errors
button.loading = false inviteButton.loading = false
} }
) )
}, }
cancel: button => {
if (inviteFuture) inviteFuture.cancel()
popup.close()
},
})
property string userId page.footer: ButtonLayout {
property string roomId ApplyButton {
property string roomName id: inviteButton
property bool invitingAllowed: true text: qsTr("Invite")
icon.name: "room-send-invite"
enabled: invitingAllowed && Boolean(inviteArea.text.trim())
onClicked: invite()
}
property var inviteFuture: null CancelButton {
property var successfulInvites: [] id: cancelButton
property var failedInvites: [] onClicked: popup.close()
}
}
onOpened: inviteArea.forceActiveFocus()
onClosed: if (inviteFuture) inviteFuture.cancel()
onInvitingAllowedChanged:
if (! invitingAllowed && inviteFuture) inviteFuture.cancel()
SummaryLabel {
text: qsTr("Invite members to <i>%1</i>").arg(roomName)
textFormat: Text.StyledText
}
HScrollView { HScrollView {
clip: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
HTextArea { HTextArea {
id: inviteArea id: inviteArea
focusItemOnTab: box.firstButton focusItemOnTab: inviteButton.enabled ? inviteButton : cancelButton
placeholderText: placeholderText:
qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)") qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)")
} }

View File

@ -1,21 +1,46 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
summary.text: qsTr("Leave <i>%1</i>?").arg(roomName) id: popup
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)
property string userId: "" property string userId: ""
property string roomId: "" property string roomId: ""
property string roomName: "" property string roomName: ""
property var leftCallback: null 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 <i>%1</i>?").arg(roomName)
textFormat: Text.StyledText
}
DetailsLabel {
text: qsTr(
"If this room is private, you will not be able to rejoin it."
)
}
} }

View File

@ -3,29 +3,22 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
id: popup id: popup
okEnabled: Boolean(passwordField.text)
onAboutToShow: {
okClicked = false
acceptedPassword = ""
passwordValid = null
errorMessage.text = ""
}
onOpened: passwordField.forceActiveFocus()
signal cancelled()
property bool validateWhileTyping: false property bool validateWhileTyping: false
property string acceptedPassword: "" property string acceptedPassword: ""
property var passwordValid: null 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) { function verifyPassword(pass, callback) {
@ -35,12 +28,10 @@ BoxPopup {
callback(true) callback(true)
} }
function validate() {
box.buttonCallbacks: ({
ok: button => {
const password = passwordField.text const password = passwordField.text
okClicked = true okClicked = true
button.loading = true validateButton.loading = true
errorMessage.text = "" errorMessage.text = ""
verifyPassword(password, result => { verifyPassword(password, result => {
@ -54,21 +45,42 @@ BoxPopup {
errorMessage.text = result errorMessage.text = result
} }
button.loading = false validateButton.loading = false
}) })
}, }
cancel: button => {
page.footer: ButtonLayout {
ApplyButton {
id: validateButton
text: qsTr("Validate")
enabled: Boolean(passwordField.text)
onClicked: validate()
}
CancelButton {
onClicked: {
popup.close() popup.close()
cancelled() cancelled()
}, }
}) }
}
onAboutToShow: {
okClicked = false
acceptedPassword = ""
passwordValid = null
errorMessage.text = ""
}
onOpened: passwordField.forceActiveFocus()
SummaryLabel { id: summary }
HRowLayout { HRowLayout {
spacing: theme.spacing spacing: theme.spacing
Layout.fillWidth: true
HTextField { HTextField {
id: passwordField id: passwordField
echoMode: TextInput.Password echoMode: TextInput.Password
@ -78,6 +90,8 @@ BoxPopup {
onTextChanged: passwordValid = onTextChanged: passwordValid =
validateWhileTyping ? verifyPassword(text) : null validateWhileTyping ? verifyPassword(text) : null
onAccepted: popup.validate()
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -91,7 +105,8 @@ BoxPopup {
Layout.preferredWidth: Layout.preferredWidth:
passwordValid === null || passwordValid === null ||
(validateWhileTyping && ! okClicked && ! passwordValid) ? (validateWhileTyping && ! okClicked && ! passwordValid) ?
0 :implicitWidth 0 :
implicitWidth
Behavior on Layout.preferredWidth { HNumberAnimation {} } Behavior on Layout.preferredWidth { HNumberAnimation {} }
} }

View File

@ -3,28 +3,21 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
summary.text: id: popup
isLast ?
qsTr("Remove your last message?") :
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 property var eventSenderAndIds: [] // [[senderId, event.id], ...]
details.text: property bool onlyOwnMessageWarning: false
onlyOwnMessageWarning ? property bool isLast: false
qsTr("Only your messages can be removed") :
""
okText: qsTr("Remove")
// box.focusButton: "ok"
onOpened: reasonField.item.forceActiveFocus() function remove() {
onOk: {
const idsForSender = {} // {senderId: [event.id, ...]} const idsForSender = {} // {senderId: [event.id, ...]}
for (const [senderId, eventClientId] of eventSenderAndIds) { for (const [senderId, eventClientId] of eventSenderAndIds) {
@ -40,16 +33,44 @@ BoxPopup {
"room_mass_redact", "room_mass_redact",
[roomId, reasonField.item.text, ...eventClientIds] [roomId, reasonField.item.text, ...eventClientIds]
) )
popup.close()
} }
property string preferUserId: "" page.footer: ButtonLayout {
property string roomId: "" ApplyButton {
text: qsTr("Remove")
icon.name: "remove-message"
onClicked: remove()
}
property var eventSenderAndIds: [] // [[senderId, event.id], ...] CancelButton {
property bool onlyOwnMessageWarning: false onClicked: popup.close()
property bool isLast: false }
}
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 { HLabeledItem {
id: reasonField id: reasonField
@ -59,6 +80,7 @@ BoxPopup {
HTextField { HTextField {
width: parent.width width: parent.width
onAccepted: popup.remove()
} }
} }
} }

View File

@ -3,48 +3,65 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
summary.textFormat: Text.StyledText id: popup
summary.text:
operation === RemoveMemberPopup.Operation.Disinvite ?
qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
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 userId
property string roomId property string roomId
property string targetUserId property string targetUserId
property string targetDisplayName property string targetDisplayName
property int operation property string operation // "disinvite", "kick" or "ban"
readonly property string coloredTarget: readonly property string coloredTarget:
utils.coloredNameHtml(targetDisplayName, targetUserId) 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 { HLabeledItem {
id: reasonField id: reasonField
label.text: qsTr("Optional reason:") label.text: qsTr("Optional reason:")
@ -53,6 +70,7 @@ BoxPopup {
HTextField { HTextField {
width: parent.width width: parent.width
onAccepted: popup.remove()
} }
} }
} }

View File

@ -2,11 +2,65 @@
import QtQuick 2.12 import QtQuick 2.12
import ".." import ".."
import "../Base/ButtonLayout"
BoxPopup { HFlickableColumnPopup {
id: popup id: popup
summary.text: qsTr("Backup your decryption keys before signing out?")
details.text: qsTr(
property string userId: ""
page.footer: ButtonLayout {
ApplyButton {
id: exportButton
text: qsTr("Export keys")
icon.name: "export-keys"
onClicked: utils.makeObject(
"Dialogs/ExportKeys.qml",
window.mainUI,
{ userId },
obj => {
loading = Qt.binding(() => obj.exporting)
obj.done.connect(signOutButton.clicked)
obj.dialog.open()
}
)
}
OtherButton {
id: signOutButton
text: qsTr("Sign out now")
icon.name: "sign-out"
icon.color: theme.colors.middleBackground
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()
}
}
CancelButton {
onClicked: popup.close()
}
}
onOpened: exportButton.forceActiveFocus()
SummaryLabel {
text: qsTr("Backup your decryption keys before signing out?")
}
DetailsLabel {
text: qsTr(
"Signing out will delete your device's information and the keys " + "Signing out will delete your device's information and the keys " +
"required to decrypt messages in encrypted rooms.\n\n" + "required to decrypt messages in encrypted rooms.\n\n" +
@ -14,49 +68,8 @@ BoxPopup {
"before signing out.\n\n" + "before signing out.\n\n" +
"This will allow you to restore access to your messages when " + "This will allow you to restore access to your messages when " +
"you sign in again, by importing this file in your account settings." "you sign in again, by importing this file in your account " +
"settings."
) )
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(
"Dialogs/ExportKeys.qml",
window.mainUI,
{ userId },
obj => {
button.loading = Qt.binding(() => obj.exporting)
obj.done.connect(() => {
box.buttonCallbacks["signout"](button)
})
obj.dialog.open()
} }
)
},
signout: button => {
okClicked = true
popup.ok()
if (ModelStore.get("accounts").count < 2 ||
window.uiState.pageProperties.userId === userId) {
window.mainUI.pageLoader.showPage("AddAccount/AddAccount")
}
py.callCoro("logout_client", [userId])
popup.close()
},
cancel: button => { okClicked = false; popup.cancel(); popup.close() },
})
property string userId: ""
} }

View File

@ -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
}

View File

@ -4,16 +4,10 @@ import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Base/ButtonLayout"
BoxPopup { HColumnPopup {
summary.text: qsTr("Unexpected error occured: <i>%1</i>").arg(errorType) id: popup
summary.textFormat: Text.StyledText
okText: qsTr("Report")
okIcon: "report-error"
okEnabled: false // TODO
cancelText: qsTr("Ignore")
box.focusButton: "cancel"
property string errorType property string errorType
@ -21,17 +15,44 @@ BoxPopup {
property string traceback: "" 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: <i>%1</i>").arg(errorType)
textFormat: Text.StyledText
}
HScrollView { HScrollView {
clip: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
HTextArea { HTextArea {
text: [message, traceback].join("\n\n") || qsTr("No info available") text: [message, traceback].join("\n\n") || qsTr("No info available")
readOnly: true readOnly: true
font.family: theme.fontFamily.mono font.family: theme.fontFamily.mono
focusOnTab: hideCheckBox
} }
} }
HCheckBox { HCheckBox {
id: hideCheckBox
text: qsTr("Hide this type of error until restart") text: qsTr("Hide this type of error until restart")
onCheckedChanged: onCheckedChanged:
checked ? checked ?

View File

@ -8,7 +8,7 @@ HQtObject {
property Item container: parent property Item container: parent
property bool active: true property bool active: container.count > 1
HShortcut { HShortcut {

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m20.663663 11.082862h-17.3273255v1.834276h17.3273255z" stroke-width=".575391"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -1,3 +1,7 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 18c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3z"/> <g stroke-width=".853763">
<path d="m12 0c1.208596 0 2.188164.97956763 2.188164 2.1881632 0 1.2085956-.979568 2.1881633-2.188164 2.1881633s-2.1881636-.9795677-2.1881636-2.1881633c0-1.20859557.9795676-2.1881632 2.1881636-2.1881632z"/>
<path d="m12 9.8118368c1.208596 0 2.188164.9795672 2.188164 2.1881632s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.1881632 2.1881636-2.1881632z"/>
<path d="m12 19.623674c1.208596 0 2.188164.979567 2.188164 2.188163s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.188163 2.1881636-2.188163z"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 728 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2.544c2.5 1.806 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.826-7 7.107-5.31-3.277-7-4.064-7-7.107v-9.575c2.446-.124 4.5-.61 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3zm4 14.729-2.771-2.736 2.733-2.761-1.233-1.232-2.737 2.773-2.77-2.735-1.222 1.222 2.774 2.747-2.736 2.771 1.223 1.222 2.745-2.773 2.762 2.735z"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2.544c2.5 1.805 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.827-7 7.107-5.31-3.278-7-4.065-7-7.107v-9.575c2.446-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1,3 @@
<svg clip-rule="evenodd" fill-rule="evenodd" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m4.81 4 13.243 15.714-1.532 1.286-5.092-6h-2.124l-1.046-1.013-1.302 1.019-1.362-1.075-1.407 1.081-4.188-3.448 3.346-3.564h2.21l-2.278-2.714zm8.499 6h-1.504l-1.678-2h2.06c1.145-1.683 3.104-3 5.339-3 3.497 0 6.474 2.866 6.474 6.5 0 3.288-2.444 5.975-5.54 6.431l-1.678-2c.237.045.485.069.744.069 2.412 0 4.474-1.986 4.474-4.5 0-2.498-2.044-4.5-4.479-4.5-2.055 0-3.292 1.433-4.212 3zm5.691-.125c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm-10.626 1.484-1.14-1.359h-3.022l-1.293 1.376 1.312 1.081 1.38-1.061 1.351 1.066z"/>
</svg>

After

Width:  |  Height:  |  Size: 674 B

View File

@ -0,0 +1,3 @@
<svg clip-rule="evenodd" fill-rule="evenodd" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m4.81 4 13.243 15.714-1.532 1.286-5.092-6h-2.124l-1.046-1.013-1.302 1.019-1.362-1.075-1.407 1.081-4.188-3.448 3.346-3.564h2.21l-2.278-2.714zm8.499 6h-1.504l-1.678-2h2.06c1.145-1.683 3.104-3 5.339-3 3.497 0 6.474 2.866 6.474 6.5 0 3.288-2.444 5.975-5.54 6.431l-1.678-2c.237.045.485.069.744.069 2.412 0 4.474-1.986 4.474-4.5 0-2.498-2.044-4.5-4.479-4.5-2.055 0-3.292 1.433-4.212 3zm5.691-.125c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm-10.626 1.484-1.14-1.359h-3.022l-1.293 1.376 1.312 1.081 1.38-1.061 1.351 1.066z"/>
</svg>

After

Width:  |  Height:  |  Size: 674 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2.544c2.5 1.805 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.827-7 7.107-5.31-3.278-7-4.065-7-7.107v-9.575c2.446-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m10.239203 8.7679831c-.4892538-.505678-.463322-1.2992034.040628-1.789322l5.141491-4.9858984c.251542-.2428983.573966-.3656441.894661-.3656441.325017 0 .648305.1262034.894661.380339zm-8.4521691 12.7033219c-.1063221.102-.1599153.23944-.1599153.376881 0 .287848.2342542.524695.524695.524695.1313898 0 .263644-.04927.365644-.147814l.8635424-.840203-.7304237-.753762zm7.2955933-9.710746-2.9554069 2.864644c-1.5879152 1.539509-2.3978644 3.031475-3.1464406 5.113831l1.3043898 1.34761c2.1039661-.682881 3.6192711-1.446152 5.2071864-2.986525l2.9545421-2.865508zm8.6181358-8.8273217-7.611966 7.3820347 3.834508 3.958117 7.611967-7.3803043c.557542-.5411186.837609-1.260305.837609-1.9803558 0-2.4065086-2.915644-3.6832374-4.672118-1.9794916z" stroke-width=".864407"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2.544c2.5 1.806 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.826-7 7.107-5.31-3.277-7-4.064-7-7.107v-9.575c2.446-.124 4.5-.61 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3zm-1 7h2v6h-2zm1 9c-.553 0-1-.447-1-1s.447-1 1-1 1 .447 1 1-.447 1-1 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m15.762 8.047-4.381 4.475-2.215-2.123-1.236 1.239 3.451 3.362 5.619-5.715zm-3.762-5.503c2.5 1.805 4.555 2.292 7 2.416v9.575c0 3.042-1.686 3.827-7 7.107-5.309-3.278-7-4.065-7-7.107v-9.575c2.447-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 4.942c1.827 1.105 3.474 1.6 5 1.833v7.76c0 1.606-.415 1.935-5 4.76zm9-1.942v11.535c0 4.603-3.203 5.804-9 9.465-5.797-3.661-9-4.862-9-9.465v-11.535c3.516 0 5.629-.134 9-3 3.371 2.866 5.484 3 9 3zm-2 1.96c-2.446-.124-4.5-.611-7-2.416-2.5 1.805-4.554 2.292-7 2.416v9.575c0 3.042 1.69 3.83 7 7.107 5.313-3.281 7-4.065 7-7.107z"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@ -66,6 +66,7 @@ colors:
color halfDimText: hsluv(0, 0, intensity * 72) color halfDimText: hsluv(0, 0, intensity * 72)
color dimText: hsluv(0, 0, intensity * 60) color dimText: hsluv(0, 0, intensity * 60)
color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)

View File

@ -69,6 +69,7 @@ colors:
color halfDimText: hsluv(0, 0, intensity * 72) color halfDimText: hsluv(0, 0, intensity * 72)
color dimText: hsluv(0, 0, intensity * 60) color dimText: hsluv(0, 0, intensity * 60)
color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)