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:
247
src/gui/Pages/AccountSettings/Account.qml
Normal file
247
src/gui/Pages/AccountSettings/Account.qml
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
113
src/gui/Pages/AccountSettings/DeviceDelegate.qml
Normal file
113
src/gui/Pages/AccountSettings/DeviceDelegate.qml
Normal 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()
|
||||
}
|
75
src/gui/Pages/AccountSettings/DeviceSection.qml
Normal file
75
src/gui/Pages/AccountSettings/DeviceSection.qml
Normal 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
|
||||
}
|
||||
}
|
@@ -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 " +
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
95
src/gui/Pages/AccountSettings/Sessions.qml
Normal file
95
src/gui/Pages/AccountSettings/Sessions.qml
Normal 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 } }
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
@@ -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 {} }
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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()
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user