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

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 "../../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 }
}
}

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.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 " +
"<b>until present time</b> can be backed up " +
"<b>until present time</b> can be saved " +
"to a passphrase-protected file.<br><br>" +
"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 "../../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 {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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