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

@@ -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
import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup {
summary.text: qsTr("Clear this room's messages?")
details.text: qsTr(
"The messages will only be removed on your side. " +
"They will be available again after you restart the application."
)
okText: qsTr("Clear")
box.focusButton: "ok"
onOk: py.callClientCoro(userId, "clear_events", [roomId])
HFlickableColumnPopup {
id: popup
property string userId: ""
property string roomId: ""
page.footer: ButtonLayout {
ApplyButton {
id: clearButton
text: qsTr("Clear")
icon.name: "clear-messages"
onClicked: {
py.callClientCoro(userId, "clear_events", [roomId])
popup.close()
}
}
CancelButton {
onClicked: popup.close()
}
}
SummaryLabel {
text: qsTr("Clear this room's messages?")
}
DetailsLabel {
text: qsTr(
"The messages will only be removed on your side. " +
"They will be available again after you restart the application."
)
}
onOpened: clearButton.forceActiveFocus()
}

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,35 +1,10 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup {
HFlickableColumnPopup {
id: popup
summary.text: qsTr("Leave <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")
box.focusButton: "ok"
onOk: py.callClientCoro(userId, "room_forget", [roomId], () => {
if (window.uiState.page === "Pages/Chat/Chat.qml" &&
window.uiState.pageProperties.userId === userId &&
window.uiState.pageProperties.roomId === roomId)
{
window.mainUI.pageLoader.showPrevious() ||
window.mainUI.pageLoader.showPage("Default")
Qt.callLater(popup.destroy)
}
})
onCancel: canDestroy = true
onClosed: if (canDestroy) Qt.callLater(popup.destroy)
property string userId: ""
@@ -37,4 +12,55 @@ BoxPopup {
property string roomName: ""
property bool canDestroy: false
function forget() {
py.callClientCoro(userId, "room_forget", [roomId], () => {
if (window.uiState.page === "Pages/Chat/Chat.qml" &&
window.uiState.pageProperties.userId === userId &&
window.uiState.pageProperties.roomId === roomId)
{
window.mainUI.pageLoader.showPrevious() ||
window.mainUI.pageLoader.showPage("Default")
Qt.callLater(popup.destroy)
}
})
}
page.footer: ButtonLayout {
ApplyButton {
id: forgetButton
text: qsTr("Forget")
icon.name: "room-forget"
onClicked: forget()
}
CancelButton {
onClicked: {
canDestroy = true
popup.close()
}
}
}
onOpened: forgetButton.forceActiveFocus()
onClosed: if (canDestroy) Qt.callLater(popup.destroy)
SummaryLabel {
text: qsTr("Leave <i>%1</i> and lose the history?").arg(roomName)
textFormat: Text.StyledText
}
DetailsLabel {
text: qsTr(
"You will not be able to see the messages you received in " +
"this room anymore.\n\n" +
"If all members forget the room, it will be removed from the " +
"servers."
)
}
}

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

View File

@@ -1,21 +1,46 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import "../Base/ButtonLayout"
BoxPopup {
summary.text: qsTr("Leave <i>%1</i>?").arg(roomName)
summary.textFormat: Text.StyledText
details.text: qsTr(
"If this room is private, you will not be able to rejoin it."
)
okText: qsTr("Leave")
box.focusButton: "ok"
onOk: py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
HFlickableColumnPopup {
id: popup
property string userId: ""
property string roomId: ""
property string roomName: ""
property var leftCallback: null
page.footer: ButtonLayout {
ApplyButton {
id: leaveButton
icon.name: "room-leave"
text: qsTr("Leave")
onClicked: {
py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
popup.close()
}
}
CancelButton {
onClicked: popup.close()
}
}
onOpened: leaveButton.forceActiveFocus()
SummaryLabel {
text: qsTr("Leave <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.Layouts 1.12
import "../Base"
import "../Base/ButtonLayout"
BoxPopup {
HFlickableColumnPopup {
id: popup
okEnabled: Boolean(passwordField.text)
onAboutToShow: {
okClicked = false
acceptedPassword = ""
passwordValid = null
errorMessage.text = ""
}
onOpened: passwordField.forceActiveFocus()
signal cancelled()
property bool validateWhileTyping: false
property string acceptedPassword: ""
property var passwordValid: null
property bool okClicked: false
property alias field: passwordField
readonly property alias summary: summary
readonly property alias validateButton: validateButton
signal cancelled()
function verifyPassword(pass, callback) {
@@ -35,40 +28,59 @@ BoxPopup {
callback(true)
}
function validate() {
const password = passwordField.text
okClicked = true
validateButton.loading = true
errorMessage.text = ""
box.buttonCallbacks: ({
ok: button => {
const password = passwordField.text
okClicked = true
button.loading = true
errorMessage.text = ""
verifyPassword(password, result => {
if (result === true) {
passwordValid = true
popup.acceptedPassword = password
popup.close()
} else if (result === false) {
passwordValid = false
} else {
errorMessage.text = result
}
verifyPassword(password, result => {
if (result === true) {
passwordValid = true
popup.acceptedPassword = password
popup.close()
} else if (result === false) {
passwordValid = false
} else {
errorMessage.text = result
}
validateButton.loading = false
})
}
button.loading = false
})
},
cancel: button => {
popup.close()
cancelled()
},
})
page.footer: ButtonLayout {
ApplyButton {
id: validateButton
text: qsTr("Validate")
enabled: Boolean(passwordField.text)
onClicked: validate()
}
CancelButton {
onClicked: {
popup.close()
cancelled()
}
}
}
onAboutToShow: {
okClicked = false
acceptedPassword = ""
passwordValid = null
errorMessage.text = ""
}
onOpened: passwordField.forceActiveFocus()
SummaryLabel { id: summary }
HRowLayout {
spacing: theme.spacing
Layout.fillWidth: true
HTextField {
id: passwordField
echoMode: TextInput.Password
@@ -78,6 +90,8 @@ BoxPopup {
onTextChanged: passwordValid =
validateWhileTyping ? verifyPassword(text) : null
onAccepted: popup.validate()
Layout.fillWidth: true
}
@@ -91,7 +105,8 @@ BoxPopup {
Layout.preferredWidth:
passwordValid === null ||
(validateWhileTyping && ! okClicked && ! passwordValid) ?
0 :implicitWidth
0 :
implicitWidth
Behavior on Layout.preferredWidth { HNumberAnimation {} }
}

View File

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

View File

@@ -3,48 +3,65 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
import "../Base/ButtonLayout"
BoxPopup {
summary.textFormat: Text.StyledText
summary.text:
operation === RemoveMemberPopup.Operation.Disinvite ?
qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
HFlickableColumnPopup {
id: popup
operation === RemoveMemberPopup.Operation.Kick ?
qsTr("Kick %1 out of the room?").arg(coloredTarget) :
qsTr("Ban %1 from the room?").arg(coloredTarget)
okText:
operation === RemoveMemberPopup.Operation.Disinvite ?
qsTr("Disinvite") :
operation === RemoveMemberPopup.Operation.Kick ?
qsTr("Kick") :
qsTr("Ban")
onOpened: reasonField.item.forceActiveFocus()
onOk: py.callClientCoro(
userId,
operation === RemoveMemberPopup.Operation.Ban ?
"room_ban" : "room_kick",
[roomId, targetUserId, reasonField.item.text || null],
)
enum Operation { Disinvite, Kick, Ban }
property string userId
property string roomId
property string targetUserId
property string targetDisplayName
property int operation
property string operation // "disinvite", "kick" or "ban"
readonly property string coloredTarget:
utils.coloredNameHtml(targetDisplayName, targetUserId)
function remove() {
py.callClientCoro(
userId,
operation === "ban" ? "room_ban" : "room_kick",
[roomId, targetUserId, reasonField.item.text || null],
)
popup.close()
}
page.footer: ButtonLayout {
ApplyButton {
text:
operation === "disinvite" ? qsTr("Disinvite") :
operation === "kick" ? qsTr("Kick") :
qsTr("Ban")
icon.name: operation === "ban" ? "room-ban" : "room-kick"
onClicked: remove()
}
CancelButton {
onClicked: popup.close()
}
}
onOpened: reasonField.item.forceActiveFocus()
SummaryLabel {
textFormat: Text.StyledText
text:
operation === "disinvite" ?
qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
operation === "kick" ?
qsTr("Kick %1 out of the room?").arg(coloredTarget) :
qsTr("Ban %1 from the room?").arg(coloredTarget)
}
HLabeledItem {
id: reasonField
label.text: qsTr("Optional reason:")
@@ -53,6 +70,7 @@ BoxPopup {
HTextField {
width: parent.width
onAccepted: popup.remove()
}
}
}

View File

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

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