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)
25
TODO.md
|
@ -1,19 +1,27 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
- sessions page size
|
||||||
|
- menu click-through padding to close it easily
|
||||||
|
- clear listview checked on message clear
|
||||||
|
- unregister popup/menu when destroyed without being closed
|
||||||
|
- flickshortcuts
|
||||||
|
- Account: wait until accountInfo available
|
||||||
|
- avatar upload/change component
|
||||||
|
- show scrollbars for a few secs if there's content to scroll on beginning
|
||||||
|
- can leave room with a reason?
|
||||||
|
- field/area focus line in popups weird
|
||||||
|
- use new nio `restore_login()`
|
||||||
|
|
||||||
## Refactoring
|
## Refactoring
|
||||||
|
|
||||||
- Rewrite account settings using `HTabbedContainer`
|
|
||||||
- Use new default/reset controls system
|
|
||||||
- Display name field text should be colored
|
|
||||||
|
|
||||||
- Drop the `HBox` `buttonModel`/`buttonCallbacks` `HBox` approach,
|
|
||||||
be more declarative
|
|
||||||
|
|
||||||
- Reorder QML object declarations,
|
- Reorder QML object declarations,
|
||||||
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
|
conform to https://doc-snapshots.qt.io/qt5-dev/qml-codingconventions.html
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
|
- Bottom focus line for an `HTextArea` inside a `ScrollView` is invisible,
|
||||||
|
put the background on `ScrollView` instead?
|
||||||
|
|
||||||
- Don't send typing notification when switching to a room where the composer
|
- Don't send typing notification when switching to a room where the composer
|
||||||
has preloaded text
|
has preloaded text
|
||||||
|
|
||||||
|
@ -21,9 +29,6 @@
|
||||||
the marker will only be updated for accounts that have already received
|
the marker will only be updated for accounts that have already received
|
||||||
it (server lag)
|
it (server lag)
|
||||||
|
|
||||||
- Popups can't be scrolled when not enough height to show all
|
|
||||||
- `TextArea`s in Popups grow past window height instead of being scrollable
|
|
||||||
|
|
||||||
- Jumping between accounts (clicking in account bar or alt+(Shift+)N) is
|
- Jumping between accounts (clicking in account bar or alt+(Shift+)N) is
|
||||||
laggy with hundreds of rooms in between
|
laggy with hundreds of rooms in between
|
||||||
- On startup, if a room's last event is a membership change,
|
- On startup, if a room's last event is a membership change,
|
||||||
|
|
|
@ -12,7 +12,6 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import asdict
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -1261,8 +1260,11 @@ class MatrixClient(nio.AsyncClient):
|
||||||
async def devices_info(self) -> List[Dict[str, Any]]:
|
async def devices_info(self) -> List[Dict[str, Any]]:
|
||||||
"""Get list of devices and their info for our user."""
|
"""Get list of devices and their info for our user."""
|
||||||
|
|
||||||
def get_trust(device_id: str) -> str:
|
def get_type(device_id: str) -> str:
|
||||||
# Returns "verified", "blacklisted", "ignored" or "unset"
|
# Return "current", "verified", "blacklisted", "ignored" or "unset"
|
||||||
|
|
||||||
|
if device_id == self.device_id:
|
||||||
|
return "current"
|
||||||
|
|
||||||
if device_id not in self.olm.device_store[self.user_id]:
|
if device_id not in self.olm.device_store[self.user_id]:
|
||||||
return "unset"
|
return "unset"
|
||||||
|
@ -1274,19 +1276,27 @@ class MatrixClient(nio.AsyncClient):
|
||||||
{
|
{
|
||||||
"id": device.id,
|
"id": device.id,
|
||||||
"display_name": device.display_name or "",
|
"display_name": device.display_name or "",
|
||||||
"last_seen_ip": device.last_seen_ip or "",
|
"last_seen_ip": (device.last_seen_ip or "").strip(" -"),
|
||||||
"last_seen_date": device.last_seen_date or ZeroDate,
|
"last_seen_date": device.last_seen_date or ZeroDate,
|
||||||
"last_seen_country": "",
|
"last_seen_country": "",
|
||||||
"trusted": get_trust(device.id) == "verified",
|
"type": get_type(device.id),
|
||||||
"blacklisted": get_trust(device.id) == "blacklisted",
|
|
||||||
}
|
}
|
||||||
for device in (await self.devices()).devices
|
for device in (await self.devices()).devices
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Reversed due to sorted(reverse=True) call below
|
||||||
|
types_order = {
|
||||||
|
"current": 4,
|
||||||
|
"unset": 3,
|
||||||
|
"verified": 2,
|
||||||
|
"ignored": 1,
|
||||||
|
"blacklisted": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sort by type, then by descending date
|
||||||
return sorted(
|
return sorted(
|
||||||
devices,
|
devices,
|
||||||
# The current device will always be first
|
key = lambda d: (types_order[d["type"]], d["last_seen_date"]),
|
||||||
key = lambda d: (d["id"] == self.device_id, d["last_seen_date"]),
|
|
||||||
reverse = True,
|
reverse = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
|
||||||
import ".."
|
import ".."
|
||||||
|
|
||||||
HButton {
|
HButton {
|
||||||
implicitHeight: theme.baseElementsHeight
|
|
||||||
text: qsTr("Apply")
|
text: qsTr("Apply")
|
||||||
icon.name: "apply"
|
icon.name: "apply"
|
||||||
icon.color: theme.colors.positiveBackground
|
icon.color: theme.colors.positiveBackground
|
||||||
|
|
||||||
|
Layout.preferredHeight: theme.baseElementsHeight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
|
||||||
import ".."
|
import ".."
|
||||||
|
|
||||||
HButton {
|
HButton {
|
||||||
implicitHeight: theme.baseElementsHeight
|
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
icon.name: "cancel"
|
icon.name: "cancel"
|
||||||
icon.color: theme.colors.negativeBackground
|
icon.color: theme.colors.negativeBackground
|
||||||
|
|
||||||
|
Layout.preferredHeight: theme.baseElementsHeight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
10
src/gui/Base/ButtonLayout/OtherButton.qml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
HButton {
|
||||||
|
Layout.preferredHeight: theme.baseElementsHeight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
|
@ -3,34 +3,17 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
Rectangle {
|
HFlickableColumnPage {
|
||||||
id: box
|
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
|
||||||
color: theme.controls.box.background
|
implicitHeight: Math.min(parent.height, flickable.contentHeight)
|
||||||
radius: theme.controls.box.radius
|
|
||||||
implicitWidth: theme.controls.box.defaultWidth
|
|
||||||
implicitHeight: childrenRect.height
|
|
||||||
|
|
||||||
Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
|
// XXX
|
||||||
Keys.onEnterPressed: Keys.onReturnPressed(event)
|
// Keys.onReturnPressed: if (clickButtonOnEnter) enterClickButton()
|
||||||
|
// Keys.onEnterPressed: Keys.onReturnPressed(event)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
property alias buttonModel: buttonRepeater.model
|
color: theme.controls.box.background
|
||||||
property var buttonCallbacks: []
|
radius: theme.controls.box.radius
|
||||||
property string focusButton: ""
|
|
||||||
property string clickButtonOnEnter: ""
|
|
||||||
|
|
||||||
property bool fillAvailableHeight: false
|
|
||||||
|
|
||||||
property HButton firstButton: null
|
|
||||||
|
|
||||||
default property alias body: interfaceBody.data
|
|
||||||
|
|
||||||
|
|
||||||
function enterClickButton() {
|
|
||||||
for (let i = 0; i < buttonModel.length; i++) {
|
|
||||||
const btn = buttonRepeater.itemAt(i)
|
|
||||||
if (btn.enabled && btn.name === clickButtonOnEnter) btn.clicked()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,86 +24,6 @@ Rectangle {
|
||||||
overshoot: 3
|
overshoot: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
HColumnLayout {
|
Behavior on implicitWidth { HNumberAnimation {} }
|
||||||
id: mainColumn
|
Behavior on implicitHeight { HNumberAnimation {} }
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Binding on height {
|
|
||||||
value: box.height
|
|
||||||
when: box.fillAvailableHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
HColumnLayout {
|
|
||||||
id: interfaceBody
|
|
||||||
spacing: theme.spacing * 1.5
|
|
||||||
|
|
||||||
Layout.margins: spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
HGridLayout {
|
|
||||||
id: buttonGrid
|
|
||||||
visible: buttonModel.length > 0
|
|
||||||
flow: width >= buttonRepeater.summedImplicitWidth ?
|
|
||||||
GridLayout.LeftToRight : GridLayout.TopToBottom
|
|
||||||
|
|
||||||
HRepeater {
|
|
||||||
id: buttonRepeater
|
|
||||||
model: []
|
|
||||||
|
|
||||||
onItemAdded: if (index === 0 && box)
|
|
||||||
box.firstButton = buttonRepeater.itemAt(0)
|
|
||||||
|
|
||||||
onItemRemoved: if (index === 0 && box)
|
|
||||||
box.firstButton = null
|
|
||||||
|
|
||||||
HButton {
|
|
||||||
id: button
|
|
||||||
text: modelData.text
|
|
||||||
icon.name: modelData.iconName || ""
|
|
||||||
icon.color: modelData.iconColor || (
|
|
||||||
name === "ok" || name === "apply" || name === "retry" ?
|
|
||||||
theme.colors.positiveBackground :
|
|
||||||
|
|
||||||
name === "cancel" ?
|
|
||||||
theme.colors.negativeBackground :
|
|
||||||
|
|
||||||
theme.icons.colorize
|
|
||||||
)
|
|
||||||
|
|
||||||
enabled:
|
|
||||||
modelData.enabled === undefined ?
|
|
||||||
true : modelData.enabled
|
|
||||||
|
|
||||||
loading: modelData.loading || false
|
|
||||||
|
|
||||||
disableWhileLoading:
|
|
||||||
modelData.disableWhileLoading === undefined ?
|
|
||||||
true : modelData.disableWhileLoading
|
|
||||||
|
|
||||||
onClicked: buttonCallbacks[name](button)
|
|
||||||
|
|
||||||
Keys.onLeftPressed: previous.forceActiveFocus()
|
|
||||||
Keys.onUpPressed: previous.forceActiveFocus()
|
|
||||||
Keys.onRightPressed: next.forceActiveFocus()
|
|
||||||
Keys.onDownPressed: next.forceActiveFocus()
|
|
||||||
|
|
||||||
Component.onCompleted:
|
|
||||||
if (name === focusButton) forceActiveFocus()
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: theme.baseElementsHeight
|
|
||||||
|
|
||||||
|
|
||||||
property string name: modelData.name
|
|
||||||
|
|
||||||
property Item next: buttonRepeater.itemAt(
|
|
||||||
utils.numberWrapAt(index + 1, buttonRepeater.count),
|
|
||||||
)
|
|
||||||
property Item previous: buttonRepeater.itemAt(
|
|
||||||
utils.numberWrapAt(index - 1, buttonRepeater.count),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
HRowLayout {
|
HRowLayout {
|
||||||
id: buttonContent
|
id: buttonContent
|
||||||
implicitHeight: theme.baseElementsHeight
|
|
||||||
spacing: button.spacing
|
spacing: button.spacing
|
||||||
opacity: loading ? theme.loadingElementsOpacity :
|
opacity: loading ? theme.loadingElementsOpacity :
|
||||||
enabled ? 1 : theme.disabledElementsOpacity
|
enabled ? 1 : theme.disabledElementsOpacity
|
||||||
|
|
|
@ -7,7 +7,7 @@ import QtQuick.Layouts 1.12
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: box
|
id: box
|
||||||
checked: defaultChecked
|
checked: defaultChecked
|
||||||
spacing: theme.spacing
|
spacing: contentItem.visible ? theme.spacing : 0
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
indicator: Rectangle {
|
indicator: Rectangle {
|
||||||
|
@ -33,21 +33,26 @@ CheckBox {
|
||||||
HIcon {
|
HIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
dimension: parent.width - 2
|
dimension: parent.width - 2
|
||||||
svgName: "check-mark"
|
|
||||||
colorize: theme.controls.checkBox.checkIconColorize
|
colorize: theme.controls.checkBox.checkIconColorize
|
||||||
|
svgName:
|
||||||
|
box.checkState === Qt.PartiallyChecked ?
|
||||||
|
"check-mark-partial" :
|
||||||
|
"check-mark"
|
||||||
|
|
||||||
scale: box.checked ? 1 : 0
|
scale: box.checkState === Qt.Unchecked ? 0 : 1
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
HNumberAnimation {
|
HNumberAnimation {
|
||||||
overshoot: 4
|
overshoot: 4
|
||||||
easing.type: Easing.InOutBack
|
easing.type: Easing.InOutBack
|
||||||
|
factor: 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: HColumnLayout {
|
contentItem: HColumnLayout {
|
||||||
|
visible: mainText.text || subtitleText.text
|
||||||
opacity: box.enabled ? 1 : theme.disabledElementsOpacity
|
opacity: box.enabled ? 1 : theme.disabledElementsOpacity
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
|
|
|
@ -7,10 +7,16 @@ HPage {
|
||||||
|
|
||||||
|
|
||||||
default property alias columnData: column.data
|
default property alias columnData: column.data
|
||||||
|
property alias column: column
|
||||||
|
|
||||||
|
|
||||||
|
implicitWidth: theme.controls.box.defaultWidth
|
||||||
|
contentHeight: column.childrenRect.height
|
||||||
|
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
id: column
|
id: column
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: theme.spacing * 1.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ import "../ShortcutBundles"
|
||||||
|
|
||||||
HPage {
|
HPage {
|
||||||
id: page
|
id: page
|
||||||
|
implicitWidth: theme.controls.box.defaultWidth
|
||||||
|
contentHeight:
|
||||||
|
flickable.contentHeight + flickable.topMargin + flickable.bottomMargin
|
||||||
|
|
||||||
|
|
||||||
default property alias columnData: column.data
|
default property alias columnData: column.data
|
||||||
|
@ -19,9 +22,14 @@ HPage {
|
||||||
HFlickable {
|
HFlickable {
|
||||||
id: flickable
|
id: flickable
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
|
||||||
contentWidth: parent.width
|
contentWidth: parent.width
|
||||||
contentHeight: column.childrenRect.height + column.padding * 2
|
contentHeight: column.implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
topMargin: theme.spacing
|
||||||
|
bottomMargin: topMargin
|
||||||
|
leftMargin: topMargin
|
||||||
|
rightMargin: topMargin
|
||||||
|
|
||||||
FlickShortcuts {
|
FlickShortcuts {
|
||||||
id: flickShortcuts
|
id: flickShortcuts
|
||||||
|
@ -31,13 +39,9 @@ HPage {
|
||||||
|
|
||||||
HColumnLayout {
|
HColumnLayout {
|
||||||
id: column
|
id: column
|
||||||
x: padding
|
width:
|
||||||
y: padding
|
flickable.width - flickable.leftMargin - flickable.rightMargin
|
||||||
width: flickable.width - padding * 2
|
spacing: theme.spacing * 1.5
|
||||||
height: flickable.height - padding * 2
|
|
||||||
|
|
||||||
property int padding:
|
|
||||||
page.currentSpacing < theme.spacing ? 0 : page.currentSpacing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ HColumnLayout {
|
||||||
|
|
||||||
default property alias insideData: itemHolder.data
|
default property alias insideData: itemHolder.data
|
||||||
|
|
||||||
readonly property Item item: itemHolder.visibleChildren[0]
|
readonly property Item item: itemHolder.children[0]
|
||||||
readonly property alias label: label
|
readonly property alias label: label
|
||||||
readonly property alias errorLabel: errorLabel
|
readonly property alias errorLabel: errorLabel
|
||||||
readonly property alias toolTip: toolTip
|
readonly property alias toolTip: toolTip
|
||||||
|
|
12
src/gui/Base/HMenuSeparator.qml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
|
||||||
|
MenuSeparator {
|
||||||
|
id: separator
|
||||||
|
padding: 0
|
||||||
|
contentItem: Item {
|
||||||
|
implicitHeight: theme.spacing
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,11 @@ Page {
|
||||||
|
|
||||||
property int currentSpacing:
|
property int currentSpacing:
|
||||||
useVariableSpacing ?
|
useVariableSpacing ?
|
||||||
Math.min(theme.spacing * width / 400, theme.spacing) :
|
Math.min(
|
||||||
|
theme.spacing * width / 400,
|
||||||
|
theme.spacing * height / 400,
|
||||||
|
theme.spacing,
|
||||||
|
) :
|
||||||
theme.spacing
|
theme.spacing
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
||||||
|
|
||||||
import QtQuick 2.12
|
|
||||||
import QtQuick.Layouts 1.12
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: page.width
|
|
||||||
height: show ? theme.baseElementsHeight : 0
|
|
||||||
visible: height > 0
|
|
||||||
color: theme.controls.header.background
|
|
||||||
|
|
||||||
|
|
||||||
property HPage page: parent
|
|
||||||
property bool show: mainUI.mainPane.collapse
|
|
||||||
|
|
||||||
|
|
||||||
Behavior on height { HNumberAnimation {} }
|
|
||||||
|
|
||||||
HRowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
HButton {
|
|
||||||
id: goToMainPaneButton
|
|
||||||
padded: false
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
icon.name: "go-back-to-main-pane"
|
|
||||||
toolTip.text: qsTr("Go back to main pane")
|
|
||||||
|
|
||||||
onClicked: mainUI.mainPane.toggleFocus()
|
|
||||||
|
|
||||||
Layout.preferredWidth: theme.baseElementsHeight
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
|
|
||||||
HLabel {
|
|
||||||
text: page.title
|
|
||||||
elide: Text.ElideRight
|
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
|
||||||
verticalAlignment: Qt.AlignVCenter
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.preferredWidth: goToMainPaneButton.width
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,12 +6,15 @@ import CppUtils 0.1
|
||||||
|
|
||||||
Popup {
|
Popup {
|
||||||
id: popup
|
id: popup
|
||||||
anchors.centerIn: Overlay.overlay
|
|
||||||
modal: true
|
modal: true
|
||||||
focus: true
|
focus: true
|
||||||
padding: 0
|
padding: 0
|
||||||
margins: theme.spacing
|
margins: theme.spacing
|
||||||
|
|
||||||
|
// FIXME: Qt 5.15: `anchors.centerIn: Overlay.overlay` + transition broken
|
||||||
|
x: (parent.width - width) / 2
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
|
||||||
enter: Transition {
|
enter: Transition {
|
||||||
HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 }
|
HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 4 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
||||||
|
|
||||||
import QtQuick 2.12
|
|
||||||
import QtQuick.Controls 2.12
|
|
||||||
import QtQuick.Layouts 1.12
|
|
||||||
|
|
||||||
HColumnLayout {
|
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
Layout.fillWidth: false
|
|
||||||
Layout.fillHeight: false
|
|
||||||
Layout.maximumWidth: parent.width
|
|
||||||
|
|
||||||
property alias tabIndex: tabBar.currentIndex
|
|
||||||
property alias tabModel: tabRepeater.model
|
|
||||||
default property alias data: swipeView.contentData
|
|
||||||
|
|
||||||
HTabBar {
|
|
||||||
id: tabBar
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: tabRepeater
|
|
||||||
HTabButton { text: modelData }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SwipeView {
|
|
||||||
id: swipeView
|
|
||||||
clip: true
|
|
||||||
currentIndex: tabBar.currentIndex
|
|
||||||
interactive: false
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Behavior on implicitWidth { HNumberAnimation {} }
|
|
||||||
Behavior on implicitHeight { HNumberAnimation {} }
|
|
||||||
}
|
|
||||||
}
|
|
45
src/gui/Base/HTabbedBox.qml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
|
|
||||||
|
HPage {
|
||||||
|
default property alias swipeViewData: swipeView.contentData
|
||||||
|
|
||||||
|
|
||||||
|
contentWidth:
|
||||||
|
Math.max(swipeView.contentWidth, theme.controls.box.defaultWidth)
|
||||||
|
|
||||||
|
header: HTabBar {}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: theme.controls.box.background
|
||||||
|
radius: theme.controls.box.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
HNumberAnimation on scale {
|
||||||
|
running: true
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
overshoot: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitWidth { HNumberAnimation {} }
|
||||||
|
Behavior on implicitHeight { HNumberAnimation {} }
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: header
|
||||||
|
property: "currentIndex"
|
||||||
|
value: swipeView.currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeView {
|
||||||
|
id: swipeView
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
currentIndex: header.currentIndex
|
||||||
|
|
||||||
|
onCurrentItemChanged: currentItem.takeFocus()
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,8 +37,8 @@ HFileDialogOpener {
|
||||||
|
|
||||||
PasswordPopup {
|
PasswordPopup {
|
||||||
id: exportPasswordPopup
|
id: exportPasswordPopup
|
||||||
details.text: qsTr("Passphrase to protect this file:")
|
summary.text: qsTr("Passphrase to protect this file:")
|
||||||
okText: qsTr("Export")
|
validateButton.text: qsTr("Export")
|
||||||
|
|
||||||
onAcceptedPasswordChanged: exportKeys(file, acceptedPassword)
|
onAcceptedPasswordChanged: exportKeys(file, acceptedPassword)
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,16 @@ HFileDialogOpener {
|
||||||
|
|
||||||
|
|
||||||
property string userId: ""
|
property string userId: ""
|
||||||
property bool importing: false
|
|
||||||
property Future importFuture: null
|
property Future importFuture: null
|
||||||
|
|
||||||
|
|
||||||
PasswordPopup {
|
PasswordPopup {
|
||||||
id: importPasswordPopup
|
id: importPasswordPopup
|
||||||
details.text:
|
summary.text:
|
||||||
importing ?
|
importFuture ?
|
||||||
qsTr("This might take a while...") :
|
qsTr("This might take a while...") :
|
||||||
qsTr("Passphrase used to protect this file:")
|
qsTr("Passphrase used to protect this file:")
|
||||||
okText: qsTr("Import")
|
validateButton.text: qsTr("Import")
|
||||||
|
|
||||||
onClosed: if (importFuture) importFuture.cancel()
|
onClosed: if (importFuture) importFuture.cancel()
|
||||||
|
|
||||||
|
@ -35,13 +34,10 @@ HFileDialogOpener {
|
||||||
|
|
||||||
|
|
||||||
function verifyPassword(pass, callback) {
|
function verifyPassword(pass, callback) {
|
||||||
importing = true
|
|
||||||
|
|
||||||
const call = py.callClientCoro
|
const call = py.callClientCoro
|
||||||
const path = file.toString().replace(/^file:\/\//, "")
|
const path = file.toString().replace(/^file:\/\//, "")
|
||||||
|
|
||||||
importFuture = call(userId, "import_keys", [path, pass], () => {
|
importFuture = call(userId, "import_keys", [path, pass], () => {
|
||||||
importing = false
|
|
||||||
importFuture = null
|
importFuture = null
|
||||||
callback(true)
|
callback(true)
|
||||||
|
|
||||||
|
@ -78,7 +74,7 @@ HFileDialogOpener {
|
||||||
|
|
||||||
Binding on closePolicy {
|
Binding on closePolicy {
|
||||||
value: Popup.CloseOnEscape
|
value: Popup.CloseOnEscape
|
||||||
when: importing
|
when: importFuture
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 "../.."
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HFlickableColumnPage {
|
HPage {
|
||||||
id: accountSettings
|
id: page
|
||||||
title: qsTr("Account settings")
|
|
||||||
header: HPageHeader {}
|
|
||||||
|
|
||||||
|
|
||||||
property int avatarPreferredSize: 256 * theme.uiScale
|
property string userId
|
||||||
|
|
||||||
property string userId: ""
|
|
||||||
|
|
||||||
readonly property bool ready:
|
|
||||||
accountInfo !== null && accountInfo.profile_updated > new Date(1)
|
|
||||||
|
|
||||||
readonly property QtObject accountInfo:
|
|
||||||
ModelStore.get("accounts").find(userId)
|
|
||||||
|
|
||||||
property string headerName: ready ? accountInfo.display_name : userId
|
|
||||||
|
|
||||||
|
|
||||||
HSpacer {}
|
HTabbedBox {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(implicitWidth, page.availableWidth)
|
||||||
|
height: Math.min(implicitHeight, page.availableHeight)
|
||||||
|
|
||||||
Repeater {
|
header: HTabBar {
|
||||||
id: repeater
|
HTabButton { text: qsTr("Account") }
|
||||||
model: ["Profile.qml", "ImportExportKeys.qml"]
|
HTabButton { text: qsTr("Encryption") }
|
||||||
|
HTabButton { text: qsTr("Sessions") }
|
||||||
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" : "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
HSpacer {}
|
Account { userId: page.userId }
|
||||||
|
Encryption { userId: page.userId }
|
||||||
|
Sessions { userId: page.userId }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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
|
@ -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 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
buttonModel: [
|
id: page
|
||||||
{ name: "export", text: qsTr("Export"), iconName: "export-keys"},
|
|
||||||
{ name: "import", text: qsTr("Import"), iconName: "import-keys"},
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
|
||||||
export: button => {
|
property string userId
|
||||||
utils.makeObject(
|
|
||||||
|
|
||||||
|
function takeFocus() { exportButton.forceActiveFocus() }
|
||||||
|
|
||||||
|
|
||||||
|
footer: ButtonLayout {
|
||||||
|
OtherButton {
|
||||||
|
id: exportButton
|
||||||
|
text: qsTr("Export")
|
||||||
|
icon.name: "export-keys"
|
||||||
|
|
||||||
|
onClicked: utils.makeObject(
|
||||||
"Dialogs/ExportKeys.qml",
|
"Dialogs/ExportKeys.qml",
|
||||||
accountSettings,
|
page,
|
||||||
{ userId: accountSettings.userId },
|
{ userId: page.userId },
|
||||||
obj => {
|
obj => {
|
||||||
button.loading = Qt.binding(() => obj.exporting)
|
loading = Qt.binding(() => obj.exporting)
|
||||||
obj.dialog.open()
|
obj.dialog.open()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
import: button => {
|
|
||||||
utils.makeObject(
|
OtherButton {
|
||||||
|
text: qsTr("Import")
|
||||||
|
icon.name: "import-keys"
|
||||||
|
|
||||||
|
onClicked: utils.makeObject(
|
||||||
"Dialogs/ImportKeys.qml",
|
"Dialogs/ImportKeys.qml",
|
||||||
accountSettings,
|
page,
|
||||||
{ userId: accountSettings.userId },
|
{ userId: page.userId },
|
||||||
obj => { obj.dialog.open() }
|
obj => { obj.dialog.open() }
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
text: qsTr(
|
text: qsTr(
|
||||||
"The decryption keys for messages received in encrypted rooms " +
|
"The decryption keys for messages received in encrypted rooms " +
|
||||||
"<b>until present time</b> can be backed up " +
|
"<b>until present time</b> can be saved " +
|
||||||
"to a passphrase-protected file.<br><br>" +
|
"to a passphrase-protected file.<br><br>" +
|
||||||
|
|
||||||
"You can then import this file on any Matrix account or " +
|
"You can then import this file on any Matrix account or " +
|
|
@ -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
|
@ -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 QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HFlickableColumnPage {
|
HPage {
|
||||||
title: qsTr("Add an account")
|
id: page
|
||||||
header: HPageHeader {}
|
|
||||||
|
|
||||||
HTabContainer {
|
HTabbedBox {
|
||||||
tabModel: [
|
anchors.centerIn: parent
|
||||||
qsTr("Sign in"), qsTr("Register"), qsTr("Reset"),
|
width: Math.min(implicitWidth, page.availableWidth)
|
||||||
]
|
height: Math.min(implicitHeight, page.availableHeight)
|
||||||
|
|
||||||
SignIn { Component.onCompleted: forceActiveFocus() }
|
header: HTabBar {
|
||||||
|
HTabButton { text: qsTr("Sign in") }
|
||||||
|
HTabButton { text: qsTr("Register") }
|
||||||
|
HTabButton { text: qsTr("Reset") }
|
||||||
|
}
|
||||||
|
|
||||||
|
SignIn {}
|
||||||
Register {}
|
Register {}
|
||||||
Reset {}
|
Reset {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,30 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: signInBox
|
function takeFocus() { registerButton.forceActiveFocus() }
|
||||||
clickButtonOnEnter: "ok"
|
|
||||||
|
|
||||||
buttonModel: [
|
|
||||||
{ name: "ok", text: qsTr("Register from Riot"), iconName: "register" },
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
footer: ButtonLayout {
|
||||||
ok: button => {
|
ApplyButton {
|
||||||
Qt.openUrlExternally("https://riot.im/app/#/register")
|
id: registerButton
|
||||||
|
text: qsTr("Register from Riot")
|
||||||
|
icon.name: "register"
|
||||||
|
onClicked: Qt.openUrlExternally("https://riot.im/app/#/register")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
HLabel {
|
HLabel {
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
text: qsTr(
|
text: qsTr(
|
||||||
"Not yet implemented\n\nYou can create a new " +
|
"Not implemented yet\n\n" +
|
||||||
"account from another client such as Riot."
|
"You can create a new account from another client such as Riot."
|
||||||
)
|
)
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
@ -3,32 +3,31 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: signInBox
|
function takeFocus() { resetButton.forceActiveFocus() }
|
||||||
clickButtonOnEnter: "ok"
|
|
||||||
|
|
||||||
buttonModel: [
|
|
||||||
{
|
|
||||||
name: "ok",
|
|
||||||
text: qsTr("Reset password from Riot"),
|
|
||||||
iconName: "reset-password"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
footer: ButtonLayout {
|
||||||
ok: button => {
|
ApplyButton {
|
||||||
Qt.openUrlExternally("https://riot.im/app/#/forgot_password")
|
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 {
|
HLabel {
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
text: qsTr(
|
text: qsTr(
|
||||||
"Not yet implemented\n\nYou can reset your " +
|
"Not implemented yet\n\n" +
|
||||||
"password using another client such as Riot."
|
"You can reset your password from another client such as Riot."
|
||||||
)
|
)
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
@ -3,82 +3,10 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: signInBox
|
id: page
|
||||||
clickButtonOnEnter: "apply"
|
|
||||||
|
|
||||||
onFocusChanged: idField.item.forceActiveFocus()
|
|
||||||
|
|
||||||
buttonModel: [
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
property var loginFuture: null
|
property var loginFuture: null
|
||||||
|
@ -90,6 +18,83 @@ HBox {
|
||||||
passwordField.item.text && ! serverField.item.error
|
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 {
|
Timer {
|
||||||
id: signInTimeout
|
id: signInTimeout
|
||||||
interval: 30 * 1000
|
interval: 30 * 1000
|
||||||
|
@ -120,10 +125,10 @@ HBox {
|
||||||
HButton {
|
HButton {
|
||||||
icon.name: modelData
|
icon.name: modelData
|
||||||
circle: true
|
circle: true
|
||||||
checked: signInWith === modelData
|
checked: page.signInWith === modelData
|
||||||
enabled: modelData === "username"
|
enabled: modelData === "username"
|
||||||
autoExclusive: true
|
autoExclusive: true
|
||||||
onClicked: signInWith = modelData
|
onClicked: page.signInWith = modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,8 +136,8 @@ HBox {
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
id: idField
|
id: idField
|
||||||
label.text: qsTr(
|
label.text: qsTr(
|
||||||
signInWith === "email" ? "Email:" :
|
page.signInWith === "email" ? "Email:" :
|
||||||
signInWith === "phone" ? "Phone:" :
|
page.signInWith === "phone" ? "Phone:" :
|
||||||
"Username:"
|
"Username:"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -157,9 +162,6 @@ HBox {
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
id: serverField
|
id: serverField
|
||||||
label.text: qsTr("Homeserver:")
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
|
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
|
||||||
readonly property var knownServers: [
|
readonly property var knownServers: [
|
||||||
|
@ -182,6 +184,10 @@ HBox {
|
||||||
readonly property bool knownServerChosen:
|
readonly property bool knownServerChosen:
|
||||||
knownServers.includes(item.cleanText)
|
knownServers.includes(item.cleanText)
|
||||||
|
|
||||||
|
label.text: qsTr("Homeserver:")
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
HTextField {
|
HTextField {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "https://matrix.org"
|
text: "https://matrix.org"
|
||||||
|
|
|
@ -2,27 +2,28 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../.."
|
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HFlickableColumnPage {
|
HPage {
|
||||||
id: addChatPage
|
id: page
|
||||||
title: qsTr("Add new chat")
|
|
||||||
header: HPageHeader {}
|
|
||||||
|
|
||||||
|
|
||||||
property string userId
|
property string userId
|
||||||
|
|
||||||
readonly property QtObject account: ModelStore.get("accounts").find(userId)
|
|
||||||
|
|
||||||
|
HTabbedBox {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.min(implicitWidth, page.availableWidth)
|
||||||
|
height: Math.min(implicitHeight, page.availableHeight)
|
||||||
|
|
||||||
HTabContainer {
|
header: HTabBar {
|
||||||
tabModel: [
|
HTabButton { text: qsTr("Direct chat") }
|
||||||
qsTr("Direct chat"), qsTr("Join room"), qsTr("Create room"),
|
HTabButton { text: qsTr("Join room") }
|
||||||
]
|
HTabButton { text: qsTr("Create room") }
|
||||||
|
}
|
||||||
|
|
||||||
DirectChat { Component.onCompleted: forceActiveFocus() }
|
DirectChat { userId: page.userId }
|
||||||
JoinRoom {}
|
JoinRoom { userId: page.userId }
|
||||||
CreateRoom {}
|
CreateRoom { userId: page.userId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,57 +2,69 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../.."
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: addChatBox
|
id: page
|
||||||
clickButtonOnEnter: "apply"
|
|
||||||
|
|
||||||
onFocusChanged: nameField.item.forceActiveFocus()
|
|
||||||
|
|
||||||
buttonModel: [
|
property string userId
|
||||||
{ name: "apply", text: qsTr("Create"), iconName: "room-create" },
|
readonly property QtObject account: ModelStore.get("accounts").find(userId)
|
||||||
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
|
||||||
apply: button => {
|
|
||||||
button.loading = true
|
|
||||||
errorMessage.text = ""
|
|
||||||
|
|
||||||
const args = [
|
function takeFocus() { nameField.item.forceActiveFocus() }
|
||||||
nameField.item.text,
|
|
||||||
topicArea.item.text,
|
|
||||||
publicCheckBox.checked,
|
|
||||||
encryptCheckBox.checked,
|
|
||||||
! blockOtherServersCheckBox.checked,
|
|
||||||
]
|
|
||||||
|
|
||||||
py.callClientCoro(userId, "new_group_chat", args, roomId => {
|
function create() {
|
||||||
button.loading = false
|
applyButton.loading = true
|
||||||
pageLoader.showRoom(userId, roomId)
|
errorMessage.text = ""
|
||||||
mainPane.roomList.startCorrectItemSearch()
|
|
||||||
|
|
||||||
}, (type, args) => {
|
const args = [
|
||||||
button.loading = false
|
nameField.item.text,
|
||||||
errorMessage.text =
|
topicArea.item.text,
|
||||||
qsTr("Unknown error - %1: %2").arg(type).arg(args)
|
publicCheckBox.checked,
|
||||||
})
|
encryptCheckBox.checked,
|
||||||
},
|
! blockOtherServersCheckBox.checked,
|
||||||
|
]
|
||||||
|
|
||||||
cancel: button => {
|
py.callClientCoro(userId, "new_group_chat", args, roomId => {
|
||||||
nameField.item.text = ""
|
applyButton.loading = false
|
||||||
topicArea.item.text = ""
|
pageLoader.showRoom(userId, roomId)
|
||||||
publicCheckBox.checked = false
|
mainPane.roomList.startCorrectItemSearch()
|
||||||
encryptCheckBox.checked = false
|
|
||||||
blockOtherServersCheckBox.checked = false
|
|
||||||
|
|
||||||
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 {
|
HRoomAvatar {
|
||||||
|
@ -70,6 +82,9 @@ HBox {
|
||||||
opacity: nameField.item.text ? 0 : 1
|
opacity: nameField.item.text ? 0 : 1
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
|
userId: page.userId
|
||||||
|
account: page.account
|
||||||
|
|
||||||
Behavior on opacity { HNumberAnimation {} }
|
Behavior on opacity { HNumberAnimation {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
|
||||||
HUserAvatar {
|
HUserAvatar {
|
||||||
userId: addChatPage.userId
|
property QtObject account
|
||||||
displayName: addChatPage.account ? addChatPage.account.display_name : ""
|
|
||||||
mxc: addChatPage.account ? addChatPage.account.avatar_url : ""
|
// userId: (set me)
|
||||||
|
displayName: account ? account.display_name : ""
|
||||||
|
mxc: account ? account.avatar_url : ""
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
Layout.preferredWidth: 128
|
||||||
|
Layout.preferredHeight: Layout.preferredWidth
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,77 +2,91 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../.."
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: addChatBox
|
id: page
|
||||||
clickButtonOnEnter: "apply"
|
|
||||||
|
|
||||||
onFocusChanged: userField.item.forceActiveFocus()
|
|
||||||
|
|
||||||
buttonModel: [
|
property string userId
|
||||||
{
|
readonly property QtObject account: ModelStore.get("accounts").find(userId)
|
||||||
name: "apply",
|
|
||||||
text: qsTr("Start chat"),
|
|
||||||
iconName: "start-direct-chat",
|
|
||||||
enabled: Boolean(userField.item.text.trim())
|
|
||||||
},
|
|
||||||
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
|
||||||
apply: button => {
|
|
||||||
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 => {
|
function startChat() {
|
||||||
button.loading = false
|
applyButton.loading = true
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
pageLoader.showRoom(userId, roomId)
|
|
||||||
mainPane.roomList.startCorrectItemSearch()
|
|
||||||
|
|
||||||
}, (type, args) => {
|
const args = [userField.item.text.trim(), encryptCheckBox.checked]
|
||||||
button.loading = false
|
|
||||||
|
|
||||||
let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
|
py.callClientCoro(userId, "new_direct_chat", args, roomId => {
|
||||||
|
applyButton.loading = false
|
||||||
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 = ""
|
|
||||||
errorMessage.text = ""
|
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 {
|
CurrentUserAvatar {
|
||||||
Layout.alignment: Qt.AlignCenter
|
userId: page.userId
|
||||||
Layout.preferredWidth: 128
|
account: page.account
|
||||||
Layout.preferredHeight: Layout.preferredWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
|
|
|
@ -2,70 +2,79 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
|
import "../.."
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../Base/ButtonLayout"
|
||||||
|
|
||||||
HBox {
|
HFlickableColumnPage {
|
||||||
id: addChatBox
|
id: page
|
||||||
clickButtonOnEnter: "apply"
|
|
||||||
|
|
||||||
onFocusChanged: roomField.item.forceActiveFocus()
|
|
||||||
|
|
||||||
buttonModel: [
|
property string userId
|
||||||
{
|
readonly property QtObject account: ModelStore.get("accounts").find(userId)
|
||||||
name: "apply",
|
|
||||||
text: qsTr("Join"),
|
|
||||||
iconName: "room-join",
|
|
||||||
enabled: Boolean(roomField.item.text.trim()),
|
|
||||||
},
|
|
||||||
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
|
|
||||||
]
|
|
||||||
|
|
||||||
buttonCallbacks: ({
|
|
||||||
apply: button => {
|
|
||||||
button.loading = true
|
|
||||||
errorMessage.text = ""
|
|
||||||
|
|
||||||
const args = [roomField.item.text.trim()]
|
function takeFocus() {
|
||||||
|
roomField.item.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
py.callClientCoro(userId, "room_join", args, roomId => {
|
function join() {
|
||||||
button.loading = false
|
joinButton.loading = true
|
||||||
errorMessage.text = ""
|
errorMessage.text = ""
|
||||||
pageLoader.showRoom(userId, roomId)
|
|
||||||
mainPane.roomList.startCorrectItemSearch()
|
|
||||||
|
|
||||||
}, (type, args) => {
|
const args = [roomField.item.text.trim()]
|
||||||
button.loading = false
|
|
||||||
|
|
||||||
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")
|
}, (type, args) => {
|
||||||
txt = qsTr("Unrecognized alias, room ID or URL")
|
joinButton.loading = false
|
||||||
|
|
||||||
if (type === "MatrixNotFound")
|
let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args)
|
||||||
txt = qsTr("Room not found")
|
|
||||||
|
|
||||||
if (type === "MatrixForbidden")
|
if (type === "ValueError")
|
||||||
txt = qsTr("You do not have permission to join this room")
|
txt = qsTr("Unrecognized alias, room ID or URL")
|
||||||
|
|
||||||
errorMessage.text = txt
|
if (type === "MatrixNotFound")
|
||||||
})
|
txt = qsTr("Room not found")
|
||||||
},
|
|
||||||
|
|
||||||
cancel: button => {
|
if (type === "MatrixForbidden")
|
||||||
roomField.item.text = ""
|
txt = qsTr("You do not have permission to join this room")
|
||||||
errorMessage.text = ""
|
|
||||||
pageLoader.showPrevious()
|
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 {
|
CurrentUserAvatar {
|
||||||
Layout.alignment: Qt.AlignCenter
|
userId: page.userId
|
||||||
Layout.preferredWidth: 128
|
account: page.account
|
||||||
Layout.preferredHeight: Layout.preferredWidth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import "Timeline"
|
||||||
HColumnPage {
|
HColumnPage {
|
||||||
id: chatPage
|
id: chatPage
|
||||||
padding: 0
|
padding: 0
|
||||||
|
column.spacing: 0
|
||||||
|
|
||||||
onLoadEventListChanged: if (loadEventList) loadedOnce = true
|
onLoadEventListChanged: if (loadEventList) loadedOnce = true
|
||||||
Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel()
|
Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel()
|
||||||
|
|
|
@ -67,10 +67,7 @@ HTile {
|
||||||
roomId: chat.roomId,
|
roomId: chat.roomId,
|
||||||
targetUserId: model.id,
|
targetUserId: model.id,
|
||||||
targetDisplayName: model.display_name,
|
targetDisplayName: model.display_name,
|
||||||
operation:
|
operation: model.invited ? "disinvite" : "kick",
|
||||||
model.invited ?
|
|
||||||
RemoveMemberPopup.Operation.Disinvite :
|
|
||||||
RemoveMemberPopup.Operation.Kick,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Component.onCompleted: py.callClientCoro(
|
Component.onCompleted: py.callClientCoro(
|
||||||
|
@ -94,7 +91,7 @@ HTile {
|
||||||
roomId: chat.roomId,
|
roomId: chat.roomId,
|
||||||
targetUserId: model.id,
|
targetUserId: model.id,
|
||||||
targetDisplayName: model.display_name,
|
targetDisplayName: model.display_name,
|
||||||
operation: RemoveMemberPopup.Operation.Ban,
|
operation: "ban",
|
||||||
})
|
})
|
||||||
|
|
||||||
Component.onCompleted: py.callClientCoro(
|
Component.onCompleted: py.callClientCoro(
|
||||||
|
|
|
@ -57,9 +57,6 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
useVariableSpacing: false
|
|
||||||
column.spacing: theme.spacing * 1.5
|
|
||||||
|
|
||||||
flickShortcuts.active:
|
flickShortcuts.active:
|
||||||
! mainUI.debugConsole.visible && ! chat.composerHasFocus
|
! mainUI.debugConsole.visible && ! chat.composerHasFocus
|
||||||
|
|
||||||
|
@ -82,6 +79,8 @@ HFlickableColumnPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: cancel()
|
||||||
|
|
||||||
|
|
||||||
HRoomAvatar {
|
HRoomAvatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,42 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
summary.text: qsTr("Clear this room's messages?")
|
id: popup
|
||||||
details.text: qsTr(
|
|
||||||
"The messages will only be removed on your side. " +
|
|
||||||
"They will be available again after you restart the application."
|
|
||||||
)
|
|
||||||
okText: qsTr("Clear")
|
|
||||||
box.focusButton: "ok"
|
|
||||||
|
|
||||||
onOk: py.callClientCoro(userId, "clear_events", [roomId])
|
|
||||||
|
|
||||||
|
|
||||||
property string userId: ""
|
property string userId: ""
|
||||||
property string roomId: ""
|
property string roomId: ""
|
||||||
|
|
||||||
|
|
||||||
|
page.footer: ButtonLayout {
|
||||||
|
ApplyButton {
|
||||||
|
id: clearButton
|
||||||
|
text: qsTr("Clear")
|
||||||
|
icon.name: "clear-messages"
|
||||||
|
onClicked: {
|
||||||
|
py.callClientCoro(userId, "clear_events", [roomId])
|
||||||
|
popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelButton {
|
||||||
|
onClicked: popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SummaryLabel {
|
||||||
|
text: qsTr("Clear this room's messages?")
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailsLabel {
|
||||||
|
text: qsTr(
|
||||||
|
"The messages will only be removed on your side. " +
|
||||||
|
"They will be available again after you restart the application."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: clearButton.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
12
src/gui/Popups/DetailsLabel.qml
Normal 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
|
||||||
|
}
|
|
@ -1,35 +1,10 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
id: popup
|
id: popup
|
||||||
summary.text: qsTr("Leave <i>%1</i> and lose the history?").arg(roomName)
|
|
||||||
summary.textFormat: Text.StyledText
|
|
||||||
details.text: qsTr(
|
|
||||||
"You will not be able to see the messages you received in " +
|
|
||||||
"this room anymore.\n\n" +
|
|
||||||
|
|
||||||
"If all members forget the room, it will be removed from the servers."
|
|
||||||
)
|
|
||||||
|
|
||||||
okText: qsTr("Forget")
|
|
||||||
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: ""
|
property string userId: ""
|
||||||
|
@ -37,4 +12,55 @@ BoxPopup {
|
||||||
property string roomName: ""
|
property string roomName: ""
|
||||||
|
|
||||||
property bool canDestroy: false
|
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."
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
27
src/gui/Popups/HColumnPopup.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
25
src/gui/Popups/HFlickableColumnPopup.qml
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,51 +4,10 @@ import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.12
|
import QtQuick.Controls 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HColumnPopup {
|
||||||
id: popup
|
id: popup
|
||||||
// fillAvailableHeight: true
|
|
||||||
summary.text: qsTr("Invite members to <i>%1</i>").arg(roomName)
|
|
||||||
summary.textFormat: Text.StyledText
|
|
||||||
okText: qsTr("Invite")
|
|
||||||
okEnabled: invitingAllowed && Boolean(inviteArea.text.trim())
|
|
||||||
|
|
||||||
onOpened: inviteArea.forceActiveFocus()
|
|
||||||
|
|
||||||
onInvitingAllowedChanged:
|
|
||||||
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
|
property string userId
|
||||||
|
@ -61,13 +20,68 @@ BoxPopup {
|
||||||
property var failedInvites: []
|
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 {
|
HScrollView {
|
||||||
|
clip: true
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
HTextArea {
|
HTextArea {
|
||||||
id: inviteArea
|
id: inviteArea
|
||||||
focusItemOnTab: box.firstButton
|
focusItemOnTab: inviteButton.enabled ? inviteButton : cancelButton
|
||||||
placeholderText:
|
placeholderText:
|
||||||
qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)")
|
qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,46 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
summary.text: qsTr("Leave <i>%1</i>?").arg(roomName)
|
id: popup
|
||||||
summary.textFormat: Text.StyledText
|
|
||||||
details.text: qsTr(
|
|
||||||
"If this room is private, you will not be able to rejoin it."
|
|
||||||
)
|
|
||||||
okText: qsTr("Leave")
|
|
||||||
box.focusButton: "ok"
|
|
||||||
|
|
||||||
onOk: py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
|
|
||||||
|
|
||||||
|
|
||||||
property string userId: ""
|
property string userId: ""
|
||||||
property string roomId: ""
|
property string roomId: ""
|
||||||
property string roomName: ""
|
property string roomName: ""
|
||||||
property var leftCallback: null
|
property var leftCallback: null
|
||||||
|
|
||||||
|
|
||||||
|
page.footer: ButtonLayout {
|
||||||
|
ApplyButton {
|
||||||
|
id: leaveButton
|
||||||
|
icon.name: "room-leave"
|
||||||
|
text: qsTr("Leave")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
py.callClientCoro(userId, "room_leave", [roomId], leftCallback)
|
||||||
|
popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelButton {
|
||||||
|
onClicked: popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: leaveButton.forceActiveFocus()
|
||||||
|
|
||||||
|
|
||||||
|
SummaryLabel {
|
||||||
|
text: qsTr("Leave <i>%1</i>?").arg(roomName)
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailsLabel {
|
||||||
|
text: qsTr(
|
||||||
|
"If this room is private, you will not be able to rejoin it."
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,22 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
id: popup
|
id: popup
|
||||||
okEnabled: Boolean(passwordField.text)
|
|
||||||
|
|
||||||
onAboutToShow: {
|
|
||||||
okClicked = false
|
|
||||||
acceptedPassword = ""
|
|
||||||
passwordValid = null
|
|
||||||
errorMessage.text = ""
|
|
||||||
}
|
|
||||||
onOpened: passwordField.forceActiveFocus()
|
|
||||||
|
|
||||||
|
|
||||||
signal cancelled()
|
|
||||||
|
|
||||||
|
|
||||||
property bool validateWhileTyping: false
|
property bool validateWhileTyping: false
|
||||||
|
|
||||||
property string acceptedPassword: ""
|
property string acceptedPassword: ""
|
||||||
property var passwordValid: null
|
property var passwordValid: null
|
||||||
|
property bool okClicked: false
|
||||||
|
|
||||||
property alias field: passwordField
|
readonly property alias summary: summary
|
||||||
|
readonly property alias validateButton: validateButton
|
||||||
|
|
||||||
|
signal cancelled()
|
||||||
|
|
||||||
|
|
||||||
function verifyPassword(pass, callback) {
|
function verifyPassword(pass, callback) {
|
||||||
|
@ -35,40 +28,59 @@ BoxPopup {
|
||||||
callback(true)
|
callback(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
const password = passwordField.text
|
||||||
|
okClicked = true
|
||||||
|
validateButton.loading = true
|
||||||
|
errorMessage.text = ""
|
||||||
|
|
||||||
box.buttonCallbacks: ({
|
verifyPassword(password, result => {
|
||||||
ok: button => {
|
if (result === true) {
|
||||||
const password = passwordField.text
|
passwordValid = true
|
||||||
okClicked = true
|
popup.acceptedPassword = password
|
||||||
button.loading = true
|
popup.close()
|
||||||
errorMessage.text = ""
|
} else if (result === false) {
|
||||||
|
passwordValid = false
|
||||||
|
} else {
|
||||||
|
errorMessage.text = result
|
||||||
|
}
|
||||||
|
|
||||||
verifyPassword(password, result => {
|
validateButton.loading = false
|
||||||
if (result === true) {
|
})
|
||||||
passwordValid = true
|
}
|
||||||
popup.acceptedPassword = password
|
|
||||||
popup.close()
|
|
||||||
} else if (result === false) {
|
|
||||||
passwordValid = false
|
|
||||||
} else {
|
|
||||||
errorMessage.text = result
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
HRowLayout {
|
||||||
spacing: theme.spacing
|
spacing: theme.spacing
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
HTextField {
|
HTextField {
|
||||||
id: passwordField
|
id: passwordField
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
|
@ -78,6 +90,8 @@ BoxPopup {
|
||||||
onTextChanged: passwordValid =
|
onTextChanged: passwordValid =
|
||||||
validateWhileTyping ? verifyPassword(text) : null
|
validateWhileTyping ? verifyPassword(text) : null
|
||||||
|
|
||||||
|
onAccepted: popup.validate()
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +105,8 @@ BoxPopup {
|
||||||
Layout.preferredWidth:
|
Layout.preferredWidth:
|
||||||
passwordValid === null ||
|
passwordValid === null ||
|
||||||
(validateWhileTyping && ! okClicked && ! passwordValid) ?
|
(validateWhileTyping && ! okClicked && ! passwordValid) ?
|
||||||
0 :implicitWidth
|
0 :
|
||||||
|
implicitWidth
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
Behavior on Layout.preferredWidth { HNumberAnimation {} }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,21 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
summary.text:
|
id: popup
|
||||||
isLast ?
|
|
||||||
qsTr("Remove your last message?") :
|
|
||||||
|
|
||||||
eventSenderAndIds.length > 1 ?
|
|
||||||
qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) :
|
|
||||||
|
|
||||||
qsTr("Remove this message?")
|
property string preferUserId: ""
|
||||||
|
property string roomId: ""
|
||||||
|
|
||||||
details.color: theme.colors.warningText
|
property var eventSenderAndIds: [] // [[senderId, event.id], ...]
|
||||||
details.text:
|
property bool onlyOwnMessageWarning: false
|
||||||
onlyOwnMessageWarning ?
|
property bool isLast: false
|
||||||
qsTr("Only your messages can be removed") :
|
|
||||||
""
|
|
||||||
|
|
||||||
okText: qsTr("Remove")
|
|
||||||
// box.focusButton: "ok"
|
|
||||||
|
|
||||||
onOpened: reasonField.item.forceActiveFocus()
|
function remove() {
|
||||||
onOk: {
|
|
||||||
const idsForSender = {} // {senderId: [event.id, ...]}
|
const idsForSender = {} // {senderId: [event.id, ...]}
|
||||||
|
|
||||||
for (const [senderId, eventClientId] of eventSenderAndIds) {
|
for (const [senderId, eventClientId] of eventSenderAndIds) {
|
||||||
|
@ -40,16 +33,44 @@ BoxPopup {
|
||||||
"room_mass_redact",
|
"room_mass_redact",
|
||||||
[roomId, reasonField.item.text, ...eventClientIds]
|
[roomId, reasonField.item.text, ...eventClientIds]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
popup.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
property string preferUserId: ""
|
page.footer: ButtonLayout {
|
||||||
property string roomId: ""
|
ApplyButton {
|
||||||
|
text: qsTr("Remove")
|
||||||
|
icon.name: "remove-message"
|
||||||
|
onClicked: remove()
|
||||||
|
}
|
||||||
|
|
||||||
property var eventSenderAndIds: [] // [[senderId, event.id], ...]
|
CancelButton {
|
||||||
property bool onlyOwnMessageWarning: false
|
onClicked: popup.close()
|
||||||
property bool isLast: false
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: reasonField.item.forceActiveFocus()
|
||||||
|
|
||||||
|
|
||||||
|
SummaryLabel {
|
||||||
|
text:
|
||||||
|
isLast ?
|
||||||
|
qsTr("Remove your last message?") :
|
||||||
|
|
||||||
|
eventSenderAndIds.length > 1 ?
|
||||||
|
qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) :
|
||||||
|
|
||||||
|
qsTr("Remove this message?")
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailsLabel {
|
||||||
|
color: theme.colors.warningText
|
||||||
|
text:
|
||||||
|
onlyOwnMessageWarning ?
|
||||||
|
qsTr("Only your messages can be removed") :
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
id: reasonField
|
id: reasonField
|
||||||
|
@ -59,6 +80,7 @@ BoxPopup {
|
||||||
|
|
||||||
HTextField {
|
HTextField {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
onAccepted: popup.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,48 +3,65 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
summary.textFormat: Text.StyledText
|
id: popup
|
||||||
summary.text:
|
|
||||||
operation === RemoveMemberPopup.Operation.Disinvite ?
|
|
||||||
qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
|
|
||||||
|
|
||||||
operation === RemoveMemberPopup.Operation.Kick ?
|
|
||||||
qsTr("Kick %1 out of the room?").arg(coloredTarget) :
|
|
||||||
|
|
||||||
qsTr("Ban %1 from the room?").arg(coloredTarget)
|
|
||||||
|
|
||||||
okText:
|
|
||||||
operation === RemoveMemberPopup.Operation.Disinvite ?
|
|
||||||
qsTr("Disinvite") :
|
|
||||||
|
|
||||||
operation === RemoveMemberPopup.Operation.Kick ?
|
|
||||||
qsTr("Kick") :
|
|
||||||
|
|
||||||
qsTr("Ban")
|
|
||||||
|
|
||||||
onOpened: reasonField.item.forceActiveFocus()
|
|
||||||
onOk: py.callClientCoro(
|
|
||||||
userId,
|
|
||||||
operation === RemoveMemberPopup.Operation.Ban ?
|
|
||||||
"room_ban" : "room_kick",
|
|
||||||
[roomId, targetUserId, reasonField.item.text || null],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
enum Operation { Disinvite, Kick, Ban }
|
|
||||||
|
|
||||||
property string userId
|
property string userId
|
||||||
property string roomId
|
property string roomId
|
||||||
property string targetUserId
|
property string targetUserId
|
||||||
property string targetDisplayName
|
property string targetDisplayName
|
||||||
property int operation
|
property string operation // "disinvite", "kick" or "ban"
|
||||||
|
|
||||||
readonly property string coloredTarget:
|
readonly property string coloredTarget:
|
||||||
utils.coloredNameHtml(targetDisplayName, targetUserId)
|
utils.coloredNameHtml(targetDisplayName, targetUserId)
|
||||||
|
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
py.callClientCoro(
|
||||||
|
userId,
|
||||||
|
operation === "ban" ? "room_ban" : "room_kick",
|
||||||
|
[roomId, targetUserId, reasonField.item.text || null],
|
||||||
|
)
|
||||||
|
|
||||||
|
popup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
page.footer: ButtonLayout {
|
||||||
|
ApplyButton {
|
||||||
|
text:
|
||||||
|
operation === "disinvite" ? qsTr("Disinvite") :
|
||||||
|
operation === "kick" ? qsTr("Kick") :
|
||||||
|
qsTr("Ban")
|
||||||
|
|
||||||
|
icon.name: operation === "ban" ? "room-ban" : "room-kick"
|
||||||
|
|
||||||
|
onClicked: remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelButton {
|
||||||
|
onClicked: popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: reasonField.item.forceActiveFocus()
|
||||||
|
|
||||||
|
|
||||||
|
SummaryLabel {
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text:
|
||||||
|
operation === "disinvite" ?
|
||||||
|
qsTr("Disinvite %1 from the room?").arg(coloredTarget) :
|
||||||
|
|
||||||
|
operation === "kick" ?
|
||||||
|
qsTr("Kick %1 out of the room?").arg(coloredTarget) :
|
||||||
|
|
||||||
|
qsTr("Ban %1 from the room?").arg(coloredTarget)
|
||||||
|
}
|
||||||
|
|
||||||
HLabeledItem {
|
HLabeledItem {
|
||||||
id: reasonField
|
id: reasonField
|
||||||
label.text: qsTr("Optional reason:")
|
label.text: qsTr("Optional reason:")
|
||||||
|
@ -53,6 +70,7 @@ BoxPopup {
|
||||||
|
|
||||||
HTextField {
|
HTextField {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
onAccepted: popup.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,61 +2,74 @@
|
||||||
|
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import ".."
|
import ".."
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HFlickableColumnPopup {
|
||||||
id: popup
|
id: popup
|
||||||
summary.text: qsTr("Backup your decryption keys before signing out?")
|
|
||||||
details.text: qsTr(
|
|
||||||
"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 " +
|
property string userId: ""
|
||||||
"you sign in again, by importing this file in your account settings."
|
|
||||||
)
|
|
||||||
|
|
||||||
box.focusButton: "ok"
|
|
||||||
box.buttonModel: [
|
|
||||||
{ name: "ok", text: qsTr("Export keys"), iconName: "export-keys" },
|
|
||||||
{ name: "signout", text: qsTr("Sign out now"), iconName: "sign-out",
|
|
||||||
iconColor: theme.colors.middleBackground },
|
|
||||||
{ name: "cancel", text: qsTr("Cancel"), iconName: "cancel" },
|
|
||||||
]
|
|
||||||
|
|
||||||
box.buttonCallbacks: ({
|
page.footer: ButtonLayout {
|
||||||
ok: button => {
|
ApplyButton {
|
||||||
utils.makeObject(
|
id: exportButton
|
||||||
|
text: qsTr("Export keys")
|
||||||
|
icon.name: "export-keys"
|
||||||
|
|
||||||
|
onClicked: utils.makeObject(
|
||||||
"Dialogs/ExportKeys.qml",
|
"Dialogs/ExportKeys.qml",
|
||||||
window.mainUI,
|
window.mainUI,
|
||||||
{ userId },
|
{ userId },
|
||||||
obj => {
|
obj => {
|
||||||
button.loading = Qt.binding(() => obj.exporting)
|
loading = Qt.binding(() => obj.exporting)
|
||||||
obj.done.connect(() => {
|
obj.done.connect(signOutButton.clicked)
|
||||||
box.buttonCallbacks["signout"](button)
|
|
||||||
})
|
|
||||||
obj.dialog.open()
|
obj.dialog.open()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
|
||||||
signout: button => {
|
OtherButton {
|
||||||
okClicked = true
|
id: signOutButton
|
||||||
popup.ok()
|
text: qsTr("Sign out now")
|
||||||
|
icon.name: "sign-out"
|
||||||
|
icon.color: theme.colors.middleBackground
|
||||||
|
|
||||||
if (ModelStore.get("accounts").count < 2 ||
|
onClicked: {
|
||||||
window.uiState.pageProperties.userId === userId) {
|
if (ModelStore.get("accounts").count < 2 ||
|
||||||
window.mainUI.pageLoader.showPage("AddAccount/AddAccount")
|
window.uiState.pageProperties.userId === userId)
|
||||||
|
{
|
||||||
|
window.mainUI.pageLoader.showPage("AddAccount/AddAccount")
|
||||||
|
}
|
||||||
|
|
||||||
|
py.callCoro("logout_client", [userId])
|
||||||
|
popup.close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
py.callCoro("logout_client", [userId])
|
CancelButton {
|
||||||
popup.close()
|
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."
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/gui/Popups/SummaryLabel.qml
Normal 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
|
||||||
|
}
|
|
@ -4,16 +4,10 @@ import QtQuick 2.12
|
||||||
import QtQuick.Controls 2.12
|
import QtQuick.Controls 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../Base"
|
import "../Base"
|
||||||
|
import "../Base/ButtonLayout"
|
||||||
|
|
||||||
BoxPopup {
|
HColumnPopup {
|
||||||
summary.text: qsTr("Unexpected error occured: <i>%1</i>").arg(errorType)
|
id: popup
|
||||||
summary.textFormat: Text.StyledText
|
|
||||||
|
|
||||||
okText: qsTr("Report")
|
|
||||||
okIcon: "report-error"
|
|
||||||
okEnabled: false // TODO
|
|
||||||
cancelText: qsTr("Ignore")
|
|
||||||
box.focusButton: "cancel"
|
|
||||||
|
|
||||||
|
|
||||||
property string errorType
|
property string errorType
|
||||||
|
@ -21,17 +15,44 @@ BoxPopup {
|
||||||
property string traceback: ""
|
property string traceback: ""
|
||||||
|
|
||||||
|
|
||||||
|
page.footer: ButtonLayout {
|
||||||
|
ApplyButton {
|
||||||
|
text: qsTr("Report")
|
||||||
|
icon.name: "report-error"
|
||||||
|
enabled: false // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelButton {
|
||||||
|
id: cancelButton
|
||||||
|
text: qsTr("Ignore")
|
||||||
|
onClicked: popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: cancelButton.forceActiveFocus()
|
||||||
|
|
||||||
|
|
||||||
|
SummaryLabel {
|
||||||
|
text: qsTr("Unexpected error occured: <i>%1</i>").arg(errorType)
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
}
|
||||||
|
|
||||||
HScrollView {
|
HScrollView {
|
||||||
|
clip: true
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
HTextArea {
|
HTextArea {
|
||||||
text: [message, traceback].join("\n\n") || qsTr("No info available")
|
text: [message, traceback].join("\n\n") || qsTr("No info available")
|
||||||
readOnly: true
|
readOnly: true
|
||||||
font.family: theme.fontFamily.mono
|
font.family: theme.fontFamily.mono
|
||||||
|
focusOnTab: hideCheckBox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HCheckBox {
|
HCheckBox {
|
||||||
|
id: hideCheckBox
|
||||||
text: qsTr("Hide this type of error until restart")
|
text: qsTr("Hide this type of error until restart")
|
||||||
onCheckedChanged:
|
onCheckedChanged:
|
||||||
checked ?
|
checked ?
|
||||||
|
|
|
@ -8,7 +8,7 @@ HQtObject {
|
||||||
|
|
||||||
|
|
||||||
property Item container: parent
|
property Item container: parent
|
||||||
property bool active: true
|
property bool active: container.count > 1
|
||||||
|
|
||||||
|
|
||||||
HShortcut {
|
HShortcut {
|
||||||
|
|
3
src/icons/thin/check-mark-partial.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m20.663663 11.082862h-17.3273255v1.834276h17.3273255z" stroke-width=".575391"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 184 B |
|
@ -1,3 +1,7 @@
|
||||||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="m12 18c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0-9c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3z"/>
|
<g stroke-width=".853763">
|
||||||
|
<path d="m12 0c1.208596 0 2.188164.97956763 2.188164 2.1881632 0 1.2085956-.979568 2.1881633-2.188164 2.1881633s-2.1881636-.9795677-2.1881636-2.1881633c0-1.20859557.9795676-2.1881632 2.1881636-2.1881632z"/>
|
||||||
|
<path d="m12 9.8118368c1.208596 0 2.188164.9795672 2.188164 2.1881632s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.1881632 2.1881636-2.1881632z"/>
|
||||||
|
<path d="m12 19.623674c1.208596 0 2.188164.979567 2.188164 2.188163s-.979568 2.188163-2.188164 2.188163-2.1881636-.979567-2.1881636-2.188163.9795676-2.188163 2.1881636-2.188163z"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 728 B |
3
src/icons/thin/device-blacklisted.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 2.544c2.5 1.806 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.826-7 7.107-5.31-3.277-7-4.064-7-7.107v-9.575c2.446-.124 4.5-.61 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3zm4 14.729-2.771-2.736 2.733-2.761-1.233-1.232-2.737 2.773-2.77-2.735-1.222 1.222 2.774 2.747-2.736 2.771 1.223 1.222 2.745-2.773 2.762 2.735z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 513 B |
3
src/icons/thin/device-current.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 2.544c2.5 1.805 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.827-7 7.107-5.31-3.278-7-4.065-7-7.107v-9.575c2.446-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 373 B |
3
src/icons/thin/device-delete-checked.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m4.81 4 13.243 15.714-1.532 1.286-5.092-6h-2.124l-1.046-1.013-1.302 1.019-1.362-1.075-1.407 1.081-4.188-3.448 3.346-3.564h2.21l-2.278-2.714zm8.499 6h-1.504l-1.678-2h2.06c1.145-1.683 3.104-3 5.339-3 3.497 0 6.474 2.866 6.474 6.5 0 3.288-2.444 5.975-5.54 6.431l-1.678-2c.237.045.485.069.744.069 2.412 0 4.474-1.986 4.474-4.5 0-2.498-2.044-4.5-4.479-4.5-2.055 0-3.292 1.433-4.212 3zm5.691-.125c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm-10.626 1.484-1.14-1.359h-3.022l-1.293 1.376 1.312 1.081 1.38-1.061 1.351 1.066z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 674 B |
3
src/icons/thin/device-delete.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m4.81 4 13.243 15.714-1.532 1.286-5.092-6h-2.124l-1.046-1.013-1.302 1.019-1.362-1.075-1.407 1.081-4.188-3.448 3.346-3.564h2.21l-2.278-2.714zm8.499 6h-1.504l-1.678-2h2.06c1.145-1.683 3.104-3 5.339-3 3.497 0 6.474 2.866 6.474 6.5 0 3.288-2.444 5.975-5.54 6.431l-1.678-2c.237.045.485.069.744.069 2.412 0 4.474-1.986 4.474-4.5 0-2.498-2.044-4.5-4.479-4.5-2.055 0-3.292 1.433-4.212 3zm5.691-.125c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm-10.626 1.484-1.14-1.359h-3.022l-1.293 1.376 1.312 1.081 1.38-1.061 1.351 1.066z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 674 B |
3
src/icons/thin/device-ignored.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 2.544c2.5 1.805 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.827-7 7.107-5.31-3.278-7-4.065-7-7.107v-9.575c2.446-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 373 B |
3
src/icons/thin/device-rename.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10.239203 8.7679831c-.4892538-.505678-.463322-1.2992034.040628-1.789322l5.141491-4.9858984c.251542-.2428983.573966-.3656441.894661-.3656441.325017 0 .648305.1262034.894661.380339zm-8.4521691 12.7033219c-.1063221.102-.1599153.23944-.1599153.376881 0 .287848.2342542.524695.524695.524695.1313898 0 .263644-.04927.365644-.147814l.8635424-.840203-.7304237-.753762zm7.2955933-9.710746-2.9554069 2.864644c-1.5879152 1.539509-2.3978644 3.031475-3.1464406 5.113831l1.3043898 1.34761c2.1039661-.682881 3.6192711-1.446152 5.2071864-2.986525l2.9545421-2.865508zm8.6181358-8.8273217-7.611966 7.3820347 3.834508 3.958117 7.611967-7.3803043c.557542-.5411186.837609-1.260305.837609-1.9803558 0-2.4065086-2.915644-3.6832374-4.672118-1.9794916z" stroke-width=".864407"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 859 B |
3
src/icons/thin/device-unset.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 2.544c2.5 1.806 4.554 2.292 7 2.416v9.575c0 3.042-1.687 3.826-7 7.107-5.31-3.277-7-4.064-7-7.107v-9.575c2.446-.124 4.5-.61 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3zm-1 7h2v6h-2zm1 9c-.553 0-1-.447-1-1s.447-1 1-1 1 .447 1 1-.447 1-1 1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 442 B |
3
src/icons/thin/device-verified.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m15.762 8.047-4.381 4.475-2.215-2.123-1.236 1.239 3.451 3.362 5.619-5.715zm-3.762-5.503c2.5 1.805 4.555 2.292 7 2.416v9.575c0 3.042-1.686 3.827-7 7.107-5.309-3.278-7-4.065-7-7.107v-9.575c2.447-.124 4.5-.611 7-2.416zm0-2.544c-3.371 2.866-5.484 3-9 3v11.535c0 4.603 3.203 5.804 9 9.465 5.797-3.661 9-4.862 9-9.465v-11.535c-3.516 0-5.629-.134-9-3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 452 B |
3
src/icons/thin/device-verify.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m12 4.942c1.827 1.105 3.474 1.6 5 1.833v7.76c0 1.606-.415 1.935-5 4.76zm9-1.942v11.535c0 4.603-3.203 5.804-9 9.465-5.797-3.661-9-4.862-9-9.465v-11.535c3.516 0 5.629-.134 9-3 3.371 2.866 5.484 3 9 3zm-2 1.96c-2.446-.124-4.5-.611-7-2.416-2.5 1.805-4.554 2.292-7 2.416v9.575c0 3.042 1.69 3.83 7 7.107 5.313-3.281 7-4.065 7-7.107z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 434 B |
|
@ -66,9 +66,10 @@ colors:
|
||||||
color halfDimText: hsluv(0, 0, intensity * 72)
|
color halfDimText: hsluv(0, 0, intensity * 72)
|
||||||
color dimText: hsluv(0, 0, intensity * 60)
|
color dimText: hsluv(0, 0, intensity * 60)
|
||||||
|
|
||||||
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
|
color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
|
||||||
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
|
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
|
||||||
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
|
||||||
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
||||||
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)
|
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
|
|
@ -69,9 +69,10 @@ colors:
|
||||||
color halfDimText: hsluv(0, 0, intensity * 72)
|
color halfDimText: hsluv(0, 0, intensity * 72)
|
||||||
color dimText: hsluv(0, 0, intensity * 60)
|
color dimText: hsluv(0, 0, intensity * 60)
|
||||||
|
|
||||||
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
|
color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity)
|
||||||
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
|
color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity)
|
||||||
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
|
||||||
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity)
|
||||||
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)
|
color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity)
|
||||||
|
|