Move remaining code to app module
There's still some stuff that could potentially go elsewhere but I think it's enough for now.
This commit is contained in:
113
src/app/qml/AccountMenu.qml
Normal file
113
src/app/qml/AccountMenu.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
import org.kde.neochat.devtools
|
||||
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
onTriggered: {
|
||||
let qrMax = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||
text: "https://matrix.to/#/" + root.connection.localUser.id,
|
||||
title: root.connection.localUser.displayName,
|
||||
subtitle: root.connection.localUser.id,
|
||||
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
|
||||
avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : ""
|
||||
});
|
||||
if (typeof root.closeDialog === "function") {
|
||||
root.closeDialog();
|
||||
}
|
||||
qrMax.open();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Edit This Account")
|
||||
icon.name: "document-edit"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'AccountEditorPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18n("Account editor")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Notification Settings")
|
||||
icon.name: "notifications"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('notifications');
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Devices")
|
||||
icon.name: "computer-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('devices');
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18n("Open Developer Tools")
|
||||
icon.name: "tools"
|
||||
visible: NeoChatConfig.developerTools
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'DevtoolsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title:window", "Developer Tools"),
|
||||
width: Kirigami.Units.gridUnit * 50,
|
||||
height: Kirigami.Units.gridUnit * 42
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Open Secret Backup")
|
||||
icon.name: "unlock"
|
||||
visible: NeoChatConfig.secretBackup
|
||||
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||
title: i18nc("@title:window", "Open Key Backup")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:inmenu", "Verify This Device")
|
||||
icon.name: "security-low"
|
||||
onTriggered: {
|
||||
root.connection.startSelfVerification();
|
||||
const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, {
|
||||
title: i18nc("@title", "Verification Request Sent"),
|
||||
subtitle: i18nc("@info:label", "To proceed, accept the verification request on another device."),
|
||||
standardButtons: Kirigami.Dialog.Ok
|
||||
})
|
||||
dialog.open();
|
||||
root.connection.onNewKeyVerificationSession.connect(() => {
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Logout")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(root).open()
|
||||
}
|
||||
|
||||
readonly property Component confirmLogoutDialogComponent: ConfirmLogoutDialog {
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
143
src/app/qml/AccountSwitchDialog.qml
Normal file
143
src/app/qml/AccountSwitchDialog.qml
Normal file
@@ -0,0 +1,143 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title: dialog to switch between logged in accounts", "Switch Account")
|
||||
|
||||
onVisibleChanged: if (visible) {
|
||||
accountView.forceActiveFocus()
|
||||
}
|
||||
|
||||
contentItem: ListView {
|
||||
id: accountView
|
||||
property var addAccount
|
||||
|
||||
implicitHeight: contentHeight
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
footer: Delegates.RoundedItemDelegate {
|
||||
id: addDelegate
|
||||
width: parent.width
|
||||
highlighted: focus && !accountView.addAccount.pressed
|
||||
Component.onCompleted: accountView.addAccount = this
|
||||
icon {
|
||||
name: "list-add"
|
||||
width: Kirigami.Units.iconSizes.smallMedium
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
text: i18nc("@button: login to or register a new account.", "Add Account")
|
||||
contentItem: Delegates.SubtitleContentItem {
|
||||
itemDelegate: parent
|
||||
subtitle: i18n("Log in or create a new account")
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
|
||||
title: i18nc("@title:window", "Login")
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
Keys.onUpPressed: {
|
||||
accountView.currentIndex = accountView.count - 1;
|
||||
accountView.forceActiveFocus();
|
||||
}
|
||||
Keys.onDownPressed: {
|
||||
accountView.currentIndex = 0;
|
||||
accountView.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
model: AccountRegistry
|
||||
|
||||
keyNavigationEnabled: false
|
||||
Keys.onDownPressed: {
|
||||
if (accountView.currentIndex === accountView.count - 1) {
|
||||
accountView.addAccount.forceActiveFocus();
|
||||
accountView.currentIndex = -1;
|
||||
} else {
|
||||
accountView.incrementCurrentIndex();
|
||||
}
|
||||
}
|
||||
Keys.onUpPressed: {
|
||||
if (accountView.currentIndex === 0) {
|
||||
accountView.addAccount.forceActiveFocus();
|
||||
accountView.currentIndex = -1;
|
||||
} else {
|
||||
accountView.decrementCurrentIndex();
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: accountView.currentItem.clicked()
|
||||
Keys.onReturnPressed: accountView.currentItem.clicked()
|
||||
|
||||
onVisibleChanged: {
|
||||
for (let i = 0; i < accountView.count; i++) {
|
||||
if (model.data(model.index(i, 0), Qt.DisplayRole) === root.connection.localUser.id) {
|
||||
accountView.currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: userDelegate
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
width: parent.width
|
||||
text: connection.localUser.displayName
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
implicitWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||
implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||
sourceSize {
|
||||
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||
}
|
||||
source: userDelegate.connection.localUser.avatarUrl.toString().length > 0 ? userDelegate.connection.makeMediaUrl(userDelegate.connection.localUser.avatarUrl) : ""
|
||||
name: userDelegate.connection.localUser.displayName ?? userDelegate.connection.localUser.id
|
||||
}
|
||||
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: userDelegate
|
||||
subtitle: userDelegate.connection.localUser.id
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
Controller.activeConnection = userDelegate.connection;
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/app/qml/AskDirectChatConfirmation.qml
Normal file
40
src/app/qml/AskDirectChatConfirmation.qml
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property var user
|
||||
|
||||
width: Math.min(Kirigami.Units.gridUnit * 24, QQC2.ApplicationWindow.window.width)
|
||||
height: Kirigami.Units.gridUnit * 8
|
||||
|
||||
standardButtons: QQC2.Dialog.Close
|
||||
title: i18nc("@title:dialog", "Start a chat")
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
text: i18n("Do you want to start a chat with %1?", root.user.displayName)
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Start Chat")
|
||||
icon.name: "im-user"
|
||||
onTriggered: {
|
||||
root.user.requestDirectChat();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
124
src/app/qml/AttachmentPane.qml
Normal file
124
src/app/qml/AttachmentPane.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
signal attachmentCancelled
|
||||
|
||||
property string attachmentPath
|
||||
|
||||
readonly property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
|
||||
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
||||
readonly property string baseFileName: attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1, attachmentPath.length)
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: i18n("Attachment:")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: editImageButton
|
||||
visible: hasImage
|
||||
icon.name: "document-edit"
|
||||
text: i18n("Edit")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
Component {
|
||||
id: imageEditorPage
|
||||
ImageEditorPage {
|
||||
imagePath: root.attachmentPath
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
let imageEditor = applicationWindow().pageStack.pushDialogLayer(imageEditorPage);
|
||||
imageEditor.newPathChanged.connect(function (newPath) {
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
root.attachmentPath = newPath;
|
||||
});
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: cancelAttachmentButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Cancel sending attachment")
|
||||
icon.name: "dialog-close"
|
||||
onTriggered: attachmentCancelled()
|
||||
shortcut: "Escape"
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
asynchronous: true
|
||||
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
||||
source: hasImage ? root.attachmentPath : ""
|
||||
visible: hasImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
onSourceChanged: {
|
||||
// Reset source size height, which affect implicitHeight
|
||||
sourceSize.height = -1;
|
||||
}
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
|
||||
// This can save a lot of RAM when loading large images.
|
||||
// It also improves visual quality for large images.
|
||||
sourceSize.height = Kirigami.Units.gridUnit * 8;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.BusyIndicator {
|
||||
id: imageBusyIndicator
|
||||
|
||||
visible: running
|
||||
running: image.visible && image.progress < 1
|
||||
}
|
||||
RowLayout {
|
||||
id: fileInfoLayout
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: parent.spacing
|
||||
|
||||
Kirigami.Icon {
|
||||
id: mimetypeIcon
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
source: attachmentMimetype.iconName
|
||||
}
|
||||
QQC2.Label {
|
||||
id: fileLabel
|
||||
text: baseFileName
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/app/qml/AvatarNotification.qml
Normal file
47
src/app/qml/AvatarNotification.qml
Normal file
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: root
|
||||
|
||||
property int notificationCount
|
||||
|
||||
property bool notificationHighlight
|
||||
|
||||
property bool showNotificationLabel
|
||||
|
||||
QQC2.Label {
|
||||
id: notificationCountLabel
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: -Kirigami.Units.smallSpacing
|
||||
anchors.rightMargin: -Kirigami.Units.smallSpacing
|
||||
z: 1
|
||||
width: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
text: root.notificationCount > 0 ? root.notificationCount : ""
|
||||
visible: root.showNotificationLabel
|
||||
color: Kirigami.Theme.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
background: Rectangle {
|
||||
visible: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
Kirigami.Theme.inherit: false
|
||||
color: root.notificationHighlight ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: notificationCountTextMetrics
|
||||
text: notificationCountLabel.text
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/app/qml/AvatarTabButton.qml
Normal file
59
src/app/qml/AvatarTabButton.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Templates as T
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
property url source
|
||||
|
||||
property alias notificationCount: avatarNotification.notificationCount
|
||||
|
||||
property alias notificationHighlight: avatarNotification.notificationHighlight
|
||||
|
||||
property alias showNotificationLabel: avatarNotification.showNotificationLabel
|
||||
|
||||
signal contextMenuRequested
|
||||
signal selected
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
Accessible.onPressAction: selected()
|
||||
Keys.onSpacePressed: selected()
|
||||
Keys.onEnterPressed: selected()
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
acceptedButtons: Qt.RightButton | Qt.LeftButton
|
||||
onTapped: (eventPoint, button) => {
|
||||
if (button === Qt.RightButton) {
|
||||
root.contextMenuRequested();
|
||||
} else {
|
||||
root.selected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: root.contextMenuRequested()
|
||||
}
|
||||
|
||||
contentItem: AvatarNotification {
|
||||
id: avatarNotification
|
||||
source: root.source
|
||||
name: root.text
|
||||
}
|
||||
}
|
||||
33
src/app/qml/ChooseRoomDialog.qml
Normal file
33
src/app/qml/ChooseRoomDialog.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
SearchPage {
|
||||
id: root
|
||||
|
||||
title: i18nc("@title", "Choose a Room")
|
||||
|
||||
showSearchButton: false
|
||||
|
||||
signal chosen(string roomId)
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
model: RoomManager.sortFilterRoomListModel
|
||||
modelDelegate: RoomDelegate {
|
||||
onClicked: {
|
||||
root.chosen(currentRoom.id);
|
||||
root.closeDialog();
|
||||
}
|
||||
connection: root.connection
|
||||
openOnClick: false
|
||||
}
|
||||
}
|
||||
152
src/app/qml/CodeMaximizeComponent.qml
Normal file
152
src/app/qml/CodeMaximizeComponent.qml
Normal file
@@ -0,0 +1,152 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Components.AbstractMaximizeComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*/
|
||||
property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
*/
|
||||
property var time
|
||||
|
||||
/**
|
||||
* @brief The code text to show.
|
||||
*/
|
||||
property string codeText
|
||||
|
||||
/**
|
||||
* @brief The code language, if any.
|
||||
*/
|
||||
property string language
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action", "Copy to clipboard")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: Clipboard.saveText(root.codeText)
|
||||
}
|
||||
]
|
||||
|
||||
leading: RowLayout {
|
||||
Components.Avatar {
|
||||
id: userAvatar
|
||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: root.author.name ?? root.author.displayName
|
||||
source: root.author.avatarUrl
|
||||
color: root.author.color
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
id: userLabel
|
||||
|
||||
text: root.author.name ?? root.author.displayName
|
||||
color: root.author.color
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: dateTimeLabel
|
||||
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: QQC2.ScrollView {
|
||||
id: codeScrollView
|
||||
contentWidth: root.width
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
QQC2.TextArea {
|
||||
id: codeText
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
text: root.codeText
|
||||
readOnly: true
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
font.family: "monospace"
|
||||
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
onWidthChanged: lineModel.resetModel()
|
||||
onHeightChanged: lineModel.resetModel()
|
||||
|
||||
SyntaxHighlighter {
|
||||
property string definitionName: Repository.definitionForName(root.language).name
|
||||
textEdit: definitionName == "None" ? null : codeText
|
||||
definition: definitionName
|
||||
}
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
anchors {
|
||||
top: codeText.top
|
||||
topMargin: codeText.topPadding + 1
|
||||
left: codeText.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: codeText.textDocument
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
required property int index
|
||||
required property int docLineHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: docLineHeight
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: index + 1
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/app/qml/CollapsedRoomDelegate.qml
Normal file
52
src/app/qml/CollapsedRoomDelegate.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom currentRoom
|
||||
required property bool categoryVisible
|
||||
required property string filterText
|
||||
required property url avatar
|
||||
required property string displayName
|
||||
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
width: ListView.view.width
|
||||
height: visible ? ListView.view.width : 0
|
||||
|
||||
visible: root.categoryVisible || filterText.length > 0
|
||||
|
||||
contentItem: KirigamiComponents.Avatar {
|
||||
source: root.avatar
|
||||
name: root.displayName
|
||||
|
||||
sourceSize {
|
||||
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: RoomManager.resolveResource(currentRoom.id)
|
||||
|
||||
Keys.onEnterPressed: RoomManager.resolveResource(currentRoom.id)
|
||||
Keys.onReturnPressed: RoomManager.resolveResource(currentRoom.id)
|
||||
|
||||
QQC2.ToolTip.visible: text.length > 0 && hovered
|
||||
QQC2.ToolTip.text: root.displayName ?? ""
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
34
src/app/qml/ConfirmLeaveDialog.qml
Normal file
34
src/app/qml/ConfirmLeaveDialog.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
title: i18nc("@title:dialog", "Confirm Leaving Room")
|
||||
subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Leave Room")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
icon.name: "arrow-left-symbolic"
|
||||
onClicked: RoomManager.leaveRoom(root.room)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/app/qml/ConfirmLogoutDialog.qml
Normal file
36
src/app/qml/ConfirmLogoutDialog.qml
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: i18nc("@title:dialog", "Sign out")
|
||||
subtitle: i18n("Are you sure you want to sign out?")
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
standardButtons: QQC2.Dialog.Cancel
|
||||
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Sign out")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.connection.logout(true);
|
||||
root.close();
|
||||
root.accepted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/app/qml/ConfirmUrlDialog.qml
Normal file
28
src/app/qml/ConfirmUrlDialog.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
property url link
|
||||
|
||||
title: i18nc("@title:dialog", "Open URL")
|
||||
subtitle: xi18nc("@info", "Do you want to open <link>%1</link>?", root.link)
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
|
||||
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
|
||||
|
||||
onAccepted: {
|
||||
Qt.openUrlExternally(root.link);
|
||||
root.close();
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
39
src/app/qml/ConsentDialog.qml
Normal file
39
src/app/qml/ConsentDialog.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property string url
|
||||
|
||||
width: Math.min(Kirigami.Units.gridUnit * 24, QQC2.ApplicationWindow.window.width)
|
||||
height: Kirigami.Units.gridUnit * 8
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
title: i18nc("@title:dialog", "User Consent")
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
text: i18nc("@info", "Your homeserver requires you to agree to its terms and conditions before being able to use it. Please click the button below to read them.")
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Open")
|
||||
icon.name: "internet-services"
|
||||
onTriggered: {
|
||||
UrlHelper.openUrl(root.url);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
275
src/app/qml/CreateRoomDialog.qml
Normal file
275
src/app/qml/CreateRoomDialog.qml
Normal file
@@ -0,0 +1,275 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
property string parentId: ""
|
||||
|
||||
property bool isSpace: false
|
||||
|
||||
property bool showChildType: false
|
||||
|
||||
property bool showCreateChoice: false
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal addChild(string childId, bool setChildParent, bool canonical)
|
||||
signal newChild(string childName)
|
||||
|
||||
title: isSpace ? i18nc("@title", "Create a Space") : i18nc("@title", "Create a Room")
|
||||
|
||||
Component.onCompleted: roomNameField.forceActiveFocus()
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: root.isSpace ? i18n("New Space Information") : i18n("New Room Information")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: roomTypeCombo
|
||||
property bool isInitialising: true
|
||||
|
||||
visible: root.showChildType
|
||||
|
||||
text: i18n("Select type")
|
||||
model: ListModel {
|
||||
id: roomTypeModel
|
||||
}
|
||||
textRole: "text"
|
||||
valueRole: "isSpace"
|
||||
|
||||
Component.onCompleted: {
|
||||
currentIndex = indexOfValue(root.isSpace);
|
||||
roomTypeModel.append({
|
||||
"text": i18n("Room"),
|
||||
"isSpace": false
|
||||
});
|
||||
roomTypeModel.append({
|
||||
"text": i18n("Space"),
|
||||
"isSpace": true
|
||||
});
|
||||
roomTypeCombo.currentIndex = 0;
|
||||
roomTypeCombo.isInitialising = false;
|
||||
}
|
||||
onCurrentValueChanged: {
|
||||
if (!isInitialising) {
|
||||
root.isSpace = currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
visible: root.showChildType
|
||||
}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: roomNameField
|
||||
label: i18n("Name:")
|
||||
onAccepted: if (roomNameField.text.length > 0) {
|
||||
roomTopicField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: roomTopicField
|
||||
label: i18n("Topic:")
|
||||
onAccepted: ok.clicked()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: newOfficialCheck
|
||||
visible: root.parentId.length > 0
|
||||
text: i18nc("@option:check As in make the space from which this dialog was created an official parent.", "Make this parent official")
|
||||
checked: true
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
visible: root.parentId.length > 0
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
id: ok
|
||||
text: root.isSpace ? i18nc("@action:button", "Create Space") : i18nc("@action:button", "Create Room")
|
||||
enabled: roomNameField.text.length > 0
|
||||
onClicked: {
|
||||
if (root.isSpace) {
|
||||
root.connection.createSpace(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
|
||||
} else {
|
||||
root.connection.createRoom(roomNameField.text, roomTopicField.text, root.parentId, newOfficialCheck.checked);
|
||||
}
|
||||
root.newChild(roomNameField.text);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
visible: root.showChildType
|
||||
title: i18n("Select Existing Room")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: root.showChildType
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: !chosenRoomDelegate.visible
|
||||
text: i18nc("@action:button", "Pick room")
|
||||
onClicked: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
chosenRoomDelegate.roomId = roomId;
|
||||
chosenRoomDelegate.displayName = displayName;
|
||||
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||
chosenRoomDelegate.alias = alias;
|
||||
chosenRoomDelegate.topic = topic;
|
||||
chosenRoomDelegate.memberCount = memberCount;
|
||||
chosenRoomDelegate.isJoined = isJoined;
|
||||
chosenRoomDelegate.visible = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: chosenRoomDelegate
|
||||
property string roomId
|
||||
property string displayName
|
||||
property url avatarUrl
|
||||
property string alias
|
||||
property string topic
|
||||
property int memberCount
|
||||
property bool isJoined
|
||||
|
||||
visible: false
|
||||
|
||||
contentItem: RowLayout {
|
||||
Components.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
source: chosenRoomDelegate.avatarUrl
|
||||
name: chosenRoomDelegate.displayName
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
level: 4
|
||||
text: chosenRoomDelegate.displayName
|
||||
font.bold: true
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: chosenRoomDelegate.isJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text
|
||||
text: chosenRoomDelegate.topic ? chosenRoomDelegate.topic.replace(/(\r\n\t|\n|\r\t)/gm, " ") : ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
QQC2.Label {
|
||||
text: chosenRoomDelegate.memberCount + " " + (chosenRoomDelegate.alias ?? chosenRoomDelegate.roomId)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
chosenRoomDelegate.roomId = roomId;
|
||||
chosenRoomDelegate.displayName = displayName;
|
||||
chosenRoomDelegate.avatarUrl = avatarUrl;
|
||||
chosenRoomDelegate.alias = alias;
|
||||
chosenRoomDelegate.topic = topic;
|
||||
chosenRoomDelegate.memberCount = memberCount;
|
||||
chosenRoomDelegate.isJoined = isJoined;
|
||||
chosenRoomDelegate.visible = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: existingOfficialCheck
|
||||
visible: root.parentId.length > 0
|
||||
text: i18nc("@option:check As in make the space from which this dialog was created an official parent.", "Make this parent official")
|
||||
description: enabled ? i18n("You have the required privilege level in the child to set this state") : i18n("You do not have a high enough privilege level in the child to set this state")
|
||||
checked: enabled
|
||||
|
||||
enabled: {
|
||||
if (chosenRoomDelegate.visible) {
|
||||
let room = root.connection.room(chosenRoomDelegate.roomId);
|
||||
if (room) {
|
||||
if (room.canSendState("m.space.parent")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
visible: root.parentId.length > 0
|
||||
}
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: makeCanonicalCheck
|
||||
text: i18nc("@option:check The canonical parent is the default one if a room has multiple parent spaces.", "Make this space the canonical parent")
|
||||
checked: enabled
|
||||
|
||||
enabled: existingOfficialCheck.enabled
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Ok")
|
||||
enabled: chosenRoomDelegate.visible
|
||||
onClicked: {
|
||||
root.addChild(chosenRoomDelegate.roomId, existingOfficialCheck.checked, makeCanonicalCheck.checked);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/app/qml/DirectChatDrawerHeader.qml
Normal file
85
src/app/qml/DirectChatDrawerHeader.qml
Normal file
@@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.largeSpacing * 2
|
||||
}
|
||||
|
||||
QQC2.AbstractButton {
|
||||
Layout.preferredWidth: Math.round(Kirigami.Units.gridUnit * 3.5)
|
||||
Layout.preferredHeight: Math.round(Kirigami.Units.gridUnit * 3.5)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
onClicked: {
|
||||
RoomManager.resolveResource(root.room.directChatRemoteMember.uri)
|
||||
}
|
||||
|
||||
contentItem: KirigamiComponents.Avatar {
|
||||
name: root.room ? root.room.displayName : ""
|
||||
source: root.room ? root.room.avatarMediaUrl : ""
|
||||
|
||||
Rectangle {
|
||||
visible: root.room.usesEncryption
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
width: Kirigami.Units.gridUnit
|
||||
height: Kirigami.Units.gridUnit
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
radius: Math.round(width / 2)
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "channel-secure-symbolic"
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Kirigami.Icon {
|
||||
id: securityIcon
|
||||
//TODO figure out how to make this update
|
||||
source: room.connection.isUserVerified(root.room.directChatRemoteMember.id) ?
|
||||
(room.connection.allSessionsSelfVerified(root.room.directChatRemoteMember.id) ? "security-high" : "security-medium")
|
||||
: "security-low"
|
||||
|
||||
}
|
||||
Kirigami.Heading {
|
||||
type: Kirigami.Heading.Type.Primary
|
||||
wrapMode: QQC2.Label.Wrap
|
||||
text: root.room.displayName
|
||||
textFormat: Text.PlainText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Item {
|
||||
Layout.preferredWidth: visible ? securityIcon.width : 0
|
||||
visible: securityIcon.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/app/qml/EditMenu.qml
Normal file
86
src/app/qml/EditMenu.qml
Normal file
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import Qt.labs.platform as Labs
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Labs.Menu {
|
||||
id: root
|
||||
|
||||
required property Item field
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canUndo
|
||||
text: i18nc("text editing menu action", "Undo")
|
||||
shortcut: StandardKey.Undo
|
||||
onTriggered: {
|
||||
root.field.undo();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canRedo
|
||||
text: i18nc("text editing menu action", "Redo")
|
||||
shortcut: StandardKey.Redo
|
||||
onTriggered: {
|
||||
root.field.undo();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText
|
||||
text: i18nc("text editing menu action", "Cut")
|
||||
shortcut: StandardKey.Cut
|
||||
onTriggered: {
|
||||
root.field.cut();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText
|
||||
text: i18nc("text editing menu action", "Copy")
|
||||
shortcut: StandardKey.Copy
|
||||
onTriggered: {
|
||||
root.field.copy();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canPaste
|
||||
text: i18nc("text editing menu action", "Paste")
|
||||
shortcut: StandardKey.Paste
|
||||
onTriggered: {
|
||||
root.field.paste();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText !== ""
|
||||
text: i18nc("text editing menu action", "Delete")
|
||||
shortcut: ""
|
||||
onTriggered: {
|
||||
root.field.remove(root.field.selectionStart, root.field.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null
|
||||
text: i18nc("text editing menu action", "Select All")
|
||||
shortcut: StandardKey.SelectAll
|
||||
onTriggered: {
|
||||
root.field.selectAll();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/app/qml/EditStateDialog.qml
Normal file
120
src/app/qml/EditStateDialog.qml
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
required property string sourceText
|
||||
property bool allowEdit: false
|
||||
|
||||
property NeoChatRoom room
|
||||
property string type
|
||||
property string stateKey
|
||||
|
||||
topPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
title: i18nc("@title As in 'edit the state of this room'", "Edit State")
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action", "Revert changes")
|
||||
icon.name: "document-revert"
|
||||
onTriggered: sourceTextArea.text = root.sourceText
|
||||
enabled: sourceTextArea.text !== root.sourceText
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action As in 'Apply the changes'", "Apply")
|
||||
icon.name: "document-edit"
|
||||
onTriggered: {
|
||||
root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text);
|
||||
root.closeDialog();
|
||||
}
|
||||
enabled: QmlUtils.isValidJson(sourceTextArea.text)
|
||||
}
|
||||
]
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
|
||||
QQC2.TextArea {
|
||||
id: sourceTextArea
|
||||
Layout.fillWidth: true
|
||||
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
text: root.sourceText
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
onWidthChanged: lineModel.resetModel()
|
||||
onHeightChanged: lineModel.resetModel()
|
||||
|
||||
SyntaxHighlighter {
|
||||
textEdit: sourceTextArea
|
||||
definition: "JSON"
|
||||
repository: Repository
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
|
||||
anchors {
|
||||
top: sourceTextArea.top
|
||||
topMargin: sourceTextArea.topPadding
|
||||
left: sourceTextArea.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: sourceTextArea.textDocument
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
required property int index
|
||||
required property int docLineHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: docLineHeight
|
||||
topPadding: 1
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: index + 1
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/app/qml/EmojiItem.qml
Normal file
33
src/app/qml/EmojiItem.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property alias emoji: emojiLabel.text
|
||||
property alias description: descriptionLabel.text
|
||||
|
||||
QQC2.Label {
|
||||
id: emojiLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "emoji"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4
|
||||
}
|
||||
QQC2.Label {
|
||||
id: descriptionLabel
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
25
src/app/qml/EmojiRow.qml
Normal file
25
src/app/qml/EmojiRow.qml
Normal file
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property alias model: repeater.model
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
delegate: EmojiItem {
|
||||
emoji: modelData.emoji
|
||||
description: modelData.description
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/app/qml/EmojiSas.qml
Normal file
59
src/app/qml/EmojiSas.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
|
||||
signal accept
|
||||
signal reject
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Confirm the emoji below are displayed on both devices, in the same order.")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
EmojiRow {
|
||||
Layout.maximumWidth: implicitWidth
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
model: root.model.slice(0, 4)
|
||||
}
|
||||
EmojiRow {
|
||||
Layout.maximumWidth: implicitWidth
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
model: root.model.slice(4, 7)
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
QQC2.Button {
|
||||
anchors.bottom: parent.bottom
|
||||
text: i18n("They match")
|
||||
icon.name: "dialog-ok"
|
||||
onClicked: root.accept()
|
||||
}
|
||||
QQC2.Button {
|
||||
anchors.bottom: parent.bottom
|
||||
text: i18n("They don't match")
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
140
src/app/qml/ExploreRoomsPage.qml
Normal file
140
src/app/qml/ExploreRoomsPage.qml
Normal file
@@ -0,0 +1,140 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Component for finding rooms for the public list.
|
||||
*
|
||||
* This component is based on a SearchPage, adding the functionality to select or
|
||||
* enter a server in the header, as well as the ability to manually type a room in
|
||||
* if the public room search cannot find it.
|
||||
*
|
||||
* @sa SearchPage
|
||||
*/
|
||||
SearchPage {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current local user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief Whether results should only includes spaces.
|
||||
*/
|
||||
property bool showOnlySpaces: spacesOnlyButton.checked
|
||||
onShowOnlySpacesChanged: updateSearch()
|
||||
|
||||
/**
|
||||
* @brief Whetherthe button to toggle the showOnlySpaces state should be shown.
|
||||
*/
|
||||
property bool showOnlySpacesButton: true
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when a room is selected.
|
||||
*
|
||||
* The signal contains all the room's info so that it can be acted
|
||||
* upon as required, e.g. joining or entering the room or adding the room as
|
||||
* the child of a space.
|
||||
*/
|
||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||
|
||||
title: i18nc("@action:title", "Explore Rooms")
|
||||
customPlaceholderText: publicRoomListModel.redirectedText
|
||||
customPlaceholderIcon: "data-warning"
|
||||
|
||||
Component.onCompleted: focusSearch()
|
||||
|
||||
headerTrailing: RowLayout {
|
||||
QQC2.Button {
|
||||
id: spacesOnlyButton
|
||||
icon.name: "globe"
|
||||
display: QQC2.Button.IconOnly
|
||||
checkable: true
|
||||
text: i18nc("@action:button", "Only show spaces")
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
ServerComboBox {
|
||||
id: serverComboBox
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
model: PublicRoomListModel {
|
||||
id: publicRoomListModel
|
||||
|
||||
connection: root.connection
|
||||
server: serverComboBox.server
|
||||
showOnlySpaces: root.showOnlySpaces
|
||||
}
|
||||
|
||||
modelDelegate: ExplorerDelegate {
|
||||
onRoomSelected: (roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
listHeaderDelegate: Delegates.RoundedItemDelegate {
|
||||
id: delegate
|
||||
|
||||
onClicked: _private.openManualRoomDialog()
|
||||
|
||||
activeFocusOnTab: false // We handle moving to this item via up/down arrows, otherwise the tab order is wacky
|
||||
text: i18n("Enter a Room Manually")
|
||||
visible: publicRoomListModel.redirectedText.length === 0
|
||||
icon.name: "compass"
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
contentItem: Kirigami.IconTitleSubtitle {
|
||||
icon: icon.fromControlsIcon(delegate.icon)
|
||||
title: delegate.text
|
||||
subtitle: i18n("If you already know a room's address or alias, and it isn't shown here.")
|
||||
}
|
||||
}
|
||||
|
||||
listFooterDelegate: QQC2.ProgressBar {
|
||||
width: ListView.view.width
|
||||
leftInset: Kirigami.Units.largeSpacing
|
||||
rightInset: Kirigami.Units.largeSpacing
|
||||
visible: root.count !== 0 && publicRoomListModel.searching
|
||||
indeterminate: true
|
||||
}
|
||||
|
||||
searchFieldPlaceholder: i18n("Find a room…")
|
||||
noResultPlaceholderMessage: i18nc("@info:label", "No public rooms found")
|
||||
|
||||
Component {
|
||||
id: manualRoomDialog
|
||||
ManualRoomDialog {}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
function openManualRoomDialog() {
|
||||
let dialog = manualRoomDialog.createObject(root.QQC2.Overlay.overlay, {
|
||||
connection: root.connection
|
||||
});
|
||||
dialog.parent = root.Window.window.overlay;
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||
root.closeDialog();
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/app/qml/ExplorerDelegate.qml
Normal file
106
src/app/qml/ExplorerDelegate.qml
Normal file
@@ -0,0 +1,106 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
required property string roomId
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
required property string alias
|
||||
required property string topic
|
||||
required property int memberCount
|
||||
required property bool isJoined
|
||||
required property bool isSpace
|
||||
property bool justJoined: false
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when a room delegate is selected.
|
||||
*
|
||||
* The signal contains all the delegate's model info so that it can be acted
|
||||
* upon as required, e.g. joining or entering the room or adding the room as
|
||||
* the child of a space.
|
||||
*/
|
||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||
|
||||
onClicked: {
|
||||
if (!isJoined) {
|
||||
justJoined = true;
|
||||
}
|
||||
root.roomSelected(root.roomId, root.displayName, root.avatarUrl, root.alias, root.topic, root.memberCount, root.isJoined);
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Components.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
source: root.avatarUrl
|
||||
name: root.displayName
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: !spaceLabel.visible
|
||||
level: 4
|
||||
text: root.displayName
|
||||
font.bold: true
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
id: spaceLabel
|
||||
Layout.fillWidth: true
|
||||
visible: root.isSpace
|
||||
text: i18nc("@info:label A matrix space", "Space")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: root.isJoined || root.justJoined
|
||||
text: i18n("Joined")
|
||||
color: Kirigami.Theme.linkColor
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text
|
||||
text: root.topic ? root.topic.replace(/(\r\n\t|\n|\r\t)/gm, " ") : ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.memberCount + " " + (root.alias ?? root.roomId)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/app/qml/FullScreenMap.qml
Normal file
79
src/app/qml/FullScreenMap.qml
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property real latitude: NaN
|
||||
property real longitude: NaN
|
||||
property string asset
|
||||
property var author
|
||||
property QtObject liveLocationModel: null
|
||||
|
||||
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
|
||||
visibility: Qt.WindowFullScreen
|
||||
|
||||
title: i18n("View Location")
|
||||
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
onActivated: root.destroy()
|
||||
}
|
||||
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
background: AbstractButton {
|
||||
onClicked: root.destroy()
|
||||
}
|
||||
|
||||
MapView {
|
||||
id: mapView
|
||||
anchors.fill: parent
|
||||
map.center: root.liveLocationModel ? QtPositioning.coordinate(root.liveLocationModel.boundingBox.y, root.liveLocationModel.boundingBox.x) : QtPositioning.coordinate(root.latitude, root.longitude)
|
||||
map.zoomLevel: 15
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
LocationMapItem {
|
||||
latitude: root.latitude
|
||||
longitude: root.longitude
|
||||
asset: root.asset
|
||||
author: root.author
|
||||
isLive: true
|
||||
heading: NaN
|
||||
visible: !isNaN(root.latitude) && !isNaN(root.longitude)
|
||||
Component.onCompleted: mapView.map.addMapItem(this)
|
||||
}
|
||||
MapItemView {
|
||||
model: root.liveLocationModel
|
||||
delegate: LocationMapItem {}
|
||||
Component.onCompleted: mapView.map.addMapItemView(this)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
text: i18n("Close")
|
||||
icon.name: "dialog-close"
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
width: Kirigami.Units.gridUnit * 2
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
onClicked: root.destroy()
|
||||
}
|
||||
}
|
||||
108
src/app/qml/GlobalMenu.qml
Normal file
108
src/app/qml/GlobalMenu.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Qt.labs.platform as Labs
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
Labs.MenuBar {
|
||||
id: root
|
||||
|
||||
property NeoChatConnection connection
|
||||
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "NeoChat")
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat…")
|
||||
text: i18nc("menu", "Configure NeoChat…")
|
||||
|
||||
shortcut: StandardKey.Preferences
|
||||
onTriggered: NeoChatSettingsView.open()
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Quit NeoChat")
|
||||
|
||||
shortcut: StandardKey.Quit
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "File")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Find your friends")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||
onTriggered: pushReplaceLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "New Group…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
const dialog = createRoomDialog.createObject(root.overlay);
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "Browse Chats…")
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
EditMenu {
|
||||
title: i18nc("menu", "Edit")
|
||||
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "View")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu item that opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Open Quick Switcher")
|
||||
onTriggered: quickSwitcher.open()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "Window")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
||||
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "Help")
|
||||
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "About Matrix")
|
||||
onTriggered: UrlHelper.openUrl("https://matrix.org/docs/chat_basics/matrix-for-im/")
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "About NeoChat")
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
|
||||
}
|
||||
Labs.MenuItem {
|
||||
text: i18nc("menu", "About KDE")
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/app/qml/GroupChatDrawerHeader.qml
Normal file
124
src/app/qml/GroupChatDrawerHeader.qml
Normal file
@@ -0,0 +1,124 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.prison
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.large
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.large
|
||||
|
||||
name: root.room ? root.room.displayName : ""
|
||||
source: root.room ? root.room.avatarMediaUrl : ""
|
||||
|
||||
Rectangle {
|
||||
visible: room.usesEncryption
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
width: Kirigami.Units.gridUnit
|
||||
height: Kirigami.Units.gridUnit
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
radius: Math.round(width / 2)
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "channel-secure-symbolic"
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
text: root.room ? root.room.displayName : i18n("No name")
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Kirigami.SelectableLabel {
|
||||
Layout.fillWidth: true
|
||||
font: Kirigami.Theme.smallFont
|
||||
textFormat: TextEdit.PlainText
|
||||
visible: root.room && root.room.canonicalAlias
|
||||
text: root.room && root.room.canonicalAlias ? root.room.canonicalAlias : ""
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
QQC2.AbstractButton {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.large
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.large
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
contentItem: Barcode {
|
||||
id: barcode
|
||||
barcodeType: Barcode.QRCode
|
||||
content: "https://matrix.to/#/" + root.room.id
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
let map = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||
text: barcode.content,
|
||||
title: root.room ? root.room.displayName : "",
|
||||
subtitle: root.room ? root.room.id : "",
|
||||
avatarSource: root.room ? root.room.avatarMediaUrl : ""
|
||||
});
|
||||
map.open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: barcode.content
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.SelectableLabel {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
visible: text.length > 0
|
||||
|
||||
text: root.room && root.room.topic ? root.room.topic : ""
|
||||
textFormat: TextEdit.MarkdownText
|
||||
wrapMode: Text.Wrap
|
||||
onLinkActivated: link => UrlHelper.openUrl(link)
|
||||
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||
} else {
|
||||
applicationWindow().hoverLinkIndicator.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/app/qml/HoverActions.qml
Normal file
181
src/app/qml/HoverActions.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.chatbar
|
||||
|
||||
/**
|
||||
* @brief A component that provides a set of actions when a message is hovered in the timeline.
|
||||
*
|
||||
* There is also an icon to show that a message has come from a verified device in
|
||||
* encrypted chats.
|
||||
*/
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current message delegate the actions are being shown on.
|
||||
*/
|
||||
property var delegate: null
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom currentRoom
|
||||
|
||||
/**
|
||||
* @brief Whether the actions should be shown.
|
||||
*/
|
||||
readonly property bool showActions: delegate && delegate.hovered
|
||||
|
||||
/**
|
||||
* @brief Request that the chat bar be focussed.
|
||||
*/
|
||||
signal focusChatBar
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
visible: (root.hovered || root.showActions || showActionsTimer.running) && !Kirigami.Settings.isMobile && (!root.delegate.isThreaded || !NeoChatConfig.threads)
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// HACK: delay disapearing by 200ms, otherwise this can create some glitches
|
||||
// See https://invent.kde.org/network/neochat/-/issues/333
|
||||
showActionsTimer.restart();
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: showActionsTimer
|
||||
interval: 200
|
||||
}
|
||||
|
||||
function updatePosition(): void {
|
||||
if (delegate) {
|
||||
root.x = delegate.contentItem.x + delegate.bubbleX + delegate.bubbleWidth - root.implicitWidth;
|
||||
root.y = delegate.mapToItem(parent, 0, 0).y + delegate.bubbleY - height + Kirigami.Units.smallSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
onDelegateChanged: updatePosition()
|
||||
onWidthChanged: updatePosition()
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: actionsLayout
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "security-high"
|
||||
width: height
|
||||
height: root.height
|
||||
visible: root.delegate && root.delegate.verified
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: i18n("This message was sent from a verified device")
|
||||
QQC2.ToolTip.visible: hover.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
text: i18n("React")
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
onClicked: emojiDialog.open()
|
||||
display: QQC2.ToolButton.IconOnly
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: root.delegate && root.delegate.isEditable && !root.currentRoom.readOnly
|
||||
text: i18n("Edit")
|
||||
icon.name: "document-edit"
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
onClicked: {
|
||||
root.currentRoom.editCache.editId = root.delegate.eventId;
|
||||
root.currentRoom.mainCache.replyId = "";
|
||||
root.currentRoom.mainCache.threadId = "";
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: !root.currentRoom.readOnly
|
||||
text: i18n("Reply")
|
||||
icon.name: "mail-replied-symbolic"
|
||||
display: QQC2.Button.IconOnly
|
||||
onClicked: {
|
||||
root.currentRoom.mainCache.replyId = root.delegate.eventId;
|
||||
root.currentRoom.editCache.editId = "";
|
||||
root.currentRoom.mainCache.threadId = "";
|
||||
root.focusChatBar();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: NeoChatConfig.threads && !root.currentRoom.readOnly && !root.delegate?.isPoll
|
||||
text: i18n("Reply in Thread")
|
||||
icon.name: "dialog-messages"
|
||||
display: QQC2.Button.IconOnly
|
||||
onClicked: {
|
||||
root.currentRoom.threadCache.replyId = "";
|
||||
root.currentRoom.threadCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId;
|
||||
root.currentRoom.mainCache.clearRelations();
|
||||
root.currentRoom.editCache.clearRelations();
|
||||
root.focusChatBar();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: (root.delegate?.isPoll ?? false) && !ContentProvider.handlerForPoll(root.currentRoom, root.delegate.eventId).hasEnded
|
||||
text: i18n("End Poll")
|
||||
icon.name: "gtk-stop"
|
||||
display: QQC2.ToolButton.IconOnly
|
||||
onClicked: root.currentRoom.poll(root.delegate.eventId).endPoll()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
currentRoom: root.currentRoom
|
||||
showQuickReaction: true
|
||||
showStickers: false
|
||||
onChosen: emoji => {
|
||||
root.currentRoom.toggleReaction(root.delegate.eventId, emoji);
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
root.focusChatBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/app/qml/HoverLinkIndicator.qml
Normal file
31
src/app/qml/HoverLinkIndicator.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
property string text
|
||||
|
||||
visible: !root.text.startsWith("https://matrix.to/") && root.text.length > 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
z: 20
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
text: root.text.startsWith("https://matrix.to/") ? "" : root.text
|
||||
elide: Text.ElideRight
|
||||
Accessible.description: i18nc("@info screenreader", "The currently selected link")
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
177
src/app/qml/ImageEditorPage.qml
Normal file
177
src/app/qml/ImageEditorPage.qml
Normal file
@@ -0,0 +1,177 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtCore as Core
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kquickimageeditor as KQuickImageEditor
|
||||
|
||||
Kirigami.Page {
|
||||
id: rootEditorView
|
||||
|
||||
property bool resizing: false
|
||||
required property string imagePath
|
||||
|
||||
signal newPathChanged(string newPath)
|
||||
|
||||
title: i18n("Edit")
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
function crop() {
|
||||
const ratioX = editImage.paintedWidth / editImage.nativeWidth;
|
||||
const ratioY = editImage.paintedHeight / editImage.nativeHeight;
|
||||
rootEditorView.resizing = false;
|
||||
imageDoc.crop(selectionTool.selectionX / ratioX, selectionTool.selectionY / ratioY, selectionTool.selectionWidth / ratioX, selectionTool.selectionHeight / ratioY);
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: undoAction
|
||||
text: i18nc("@action:button Undo modification", "Undo")
|
||||
icon.name: "edit-undo"
|
||||
onTriggered: imageDoc.undo()
|
||||
visible: imageDoc.edited
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: okAction
|
||||
text: i18nc("@action:button Accept image modification", "Accept")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
let newPath = Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop();
|
||||
if (imageDoc.saveAs(newPath)) {
|
||||
newPathChanged(newPath);
|
||||
} else {
|
||||
msg.type = Kirigami.MessageType.Error;
|
||||
msg.text = i18n("Unable to save file. Check if you have the correct permission to edit the cache directory.");
|
||||
msg.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
KQuickImageEditor.ImageItem {
|
||||
id: editImage
|
||||
// Assigning this to the contentItem and setting the padding causes weird positioning issues
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.gridUnit
|
||||
fillMode: KQuickImageEditor.ImageItem.PreserveAspectFit
|
||||
image: imageDoc.image
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Undo
|
||||
onActivated: undoAction.trigger()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: [StandardKey.Save, "Enter"]
|
||||
onActivated: saveAction.trigger()
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.SaveAs
|
||||
onActivated: saveAsAction.trigger()
|
||||
}
|
||||
|
||||
KQuickImageEditor.ImageDocument {
|
||||
id: imageDoc
|
||||
path: rootEditorView.imagePath
|
||||
}
|
||||
|
||||
KQuickImageEditor.SelectionTool {
|
||||
id: selectionTool
|
||||
visible: rootEditorView.resizing
|
||||
width: editImage.paintedWidth
|
||||
height: editImage.paintedHeight
|
||||
x: editImage.horizontalPadding
|
||||
y: editImage.verticalPadding
|
||||
KQuickImageEditor.CropBackground {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
insideX: selectionTool.selectionX
|
||||
insideY: selectionTool.selectionY
|
||||
insideWidth: selectionTool.selectionWidth
|
||||
insideHeight: selectionTool.selectionHeight
|
||||
}
|
||||
Connections {
|
||||
target: selectionTool.selectionArea
|
||||
function onDoubleClicked() {
|
||||
rootEditorView.crop();
|
||||
}
|
||||
}
|
||||
}
|
||||
onImageChanged: {
|
||||
selectionTool.selectionX = 0;
|
||||
selectionTool.selectionY = 0;
|
||||
selectionTool.selectionWidth = Qt.binding(() => selectionTool.width);
|
||||
selectionTool.selectionHeight = Qt.binding(() => selectionTool.height);
|
||||
}
|
||||
}
|
||||
|
||||
header: QQC2.ToolBar {
|
||||
contentItem: Kirigami.ActionToolBar {
|
||||
id: actionToolBar
|
||||
display: QQC2.Button.TextBesideIcon
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: rootEditorView.resizing ? "dialog-cancel" : "transform-crop"
|
||||
text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop")
|
||||
onTriggered: {
|
||||
resizeRectangle.width = editImage.paintedWidth;
|
||||
resizeRectangle.height = editImage.paintedHeight;
|
||||
resizeRectangle.x = editImage.horizontalPadding;
|
||||
resizeRectangle.y = editImage.verticalPadding;
|
||||
resizeRectangle.insideX = 100;
|
||||
resizeRectangle.insideY = 100;
|
||||
resizeRectangle.insideWidth = 100;
|
||||
resizeRectangle.insideHeight = 100;
|
||||
rootEditorView.resizing = !rootEditorView.resizing;
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "dialog-ok"
|
||||
visible: rootEditorView.resizing
|
||||
text: i18nc("@action:button Crop an image", "Crop")
|
||||
onTriggered: rootEditorView.crop()
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "object-rotate-left"
|
||||
text: i18nc("@action:button Rotate an image to the left", "Rotate left")
|
||||
onTriggered: imageDoc.rotate(-90)
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "object-rotate-right"
|
||||
text: i18nc("@action:button Rotate an image to the right", "Rotate right")
|
||||
onTriggered: imageDoc.rotate(90)
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "object-flip-vertical"
|
||||
text: i18nc("@action:button Mirror an image vertically", "Flip")
|
||||
onTriggered: imageDoc.mirror(false, true)
|
||||
visible: !rootEditorView.resizing
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "object-flip-horizontal"
|
||||
text: i18nc("@action:button Mirror an image horizontally", "Mirror")
|
||||
onTriggered: imageDoc.mirror(true, false)
|
||||
visible: !rootEditorView.resizing
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
footer: Kirigami.InlineMessage {
|
||||
id: msg
|
||||
type: Kirigami.MessageType.Error
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
}
|
||||
}
|
||||
115
src/app/qml/InvitationView.qml
Normal file
115
src/app/qml/InvitationView.qml
Normal file
@@ -0,0 +1,115 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom currentRoom
|
||||
readonly property var invitingMember: currentRoom.member(currentRoom.invitingUserId())
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
name: root.invitingMember.displayName
|
||||
source: root.invitingMember.avatarUrl
|
||||
color: root.invitingMember.color
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !root.currentRoom.isDirectChat()
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
text: i18nc("@info:label", "%1 has invited you to join", root.invitingMember.displayName)
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: root.currentRoom.displayName
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.currentRoom.isDirectChat()
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
text: root.currentRoom.displayName
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: i18nc("@info:label", "This user is inviting you to chat.")
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
text: i18n("You can reject invitations from unknown users under Security settings.")
|
||||
visible: root.currentRoom.connection.canCheckMutualRooms
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: i18nc("@action:button The thing being rejected is an invitation to chat", "Reject and Ignore User")
|
||||
icon.name: "list-remove-symbolic"
|
||||
|
||||
onClicked: {
|
||||
RoomManager.leaveRoom(root.currentRoom);
|
||||
root.currentRoom.connection.addToIgnoredUsers(root.currentRoom.invitingUserId());
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon.name: "cards-block-symbolic"
|
||||
text: i18nc("@action:button", "Reject")
|
||||
|
||||
onClicked: RoomManager.leaveRoom(root.currentRoom)
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon.name: "dialog-ok-symbolic"
|
||||
text: i18nc("@action:button", "Accept")
|
||||
|
||||
onClicked: root.currentRoom.acceptInvitation()
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/app/qml/InviteUserPage.qml
Normal file
90
src/app/qml/InviteUserPage.qml
Normal file
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
SearchPage {
|
||||
id: root
|
||||
|
||||
property NeoChatRoom room
|
||||
|
||||
title: i18nc("@title:dialog", "Invite a User")
|
||||
|
||||
searchFieldPlaceholder: i18nc("@info:placeholder", "Find a user…")
|
||||
noResultPlaceholderMessage: i18nc("@info:placeholder", "No users found")
|
||||
|
||||
headerTrailing: QQC2.Button {
|
||||
icon.name: "list-add"
|
||||
display: QQC2.Button.IconOnly
|
||||
enabled: root.model.searchText.match(/@(.+):(.+)/g) && !root.room.containsUser(root.model.searchText)
|
||||
|
||||
text: i18nc("@action:button", "Invite this User")
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: root.room.containsUser(root.model.searchText) ? i18nc("@info:tooltip", "User is either already a member or has been invited") : text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: root.room.inviteToRoom(root.model.searchText);
|
||||
}
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
connection: root.room.connection
|
||||
}
|
||||
|
||||
modelDelegate: Delegates.RoundedItemDelegate {
|
||||
id: delegate
|
||||
|
||||
required property string userId
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
|
||||
text: displayName
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
source: delegate.avatarUrl
|
||||
name: delegate.displayName
|
||||
}
|
||||
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: delegate
|
||||
subtitle: delegate.userId
|
||||
labelItem.textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: inviteButton
|
||||
|
||||
readonly property bool inRoom: root.room && root.room.containsUser(delegate.userId)
|
||||
|
||||
icon.name: "document-send"
|
||||
text: i18nc("@action:button", "Send invitation")
|
||||
opacity: inRoom ? 0.5 : 1
|
||||
enabled: !inRoom
|
||||
|
||||
onClicked: {
|
||||
inviteButton.enabled = false;
|
||||
root.room.inviteToRoom(delegate.userId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: !inRoom ? text : i18nc("@info:tooltip", "User is either already a member or has been invited")
|
||||
QQC2.ToolTip.visible: inviteButton.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/app/qml/JoinRoomDialog.qml
Normal file
75
src/app/qml/JoinRoomDialog.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.prison
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property string room
|
||||
required property NeoChatConnection connection
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title:dialog", "Join Room")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
|
||||
name: root.room.slice(1, -1)
|
||||
initialsMode: KirigamiComponents.Avatar.UseInitials
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 1
|
||||
Layout.fillWidth: true
|
||||
font.bold: true
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: root.room
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Join room")
|
||||
icon.name: "irc-join-channel"
|
||||
onClicked: {
|
||||
RoomManager.resolveResource(root.room, "join");
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/app/qml/KeyVerificationDialog.qml
Normal file
173
src/app/qml/KeyVerificationDialog.qml
Normal file
@@ -0,0 +1,173 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
title: i18n("Session Verification")
|
||||
|
||||
required property var session
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "cancelled"
|
||||
when: root.session.state === KeyVerificationSession.CANCELED
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: verificationCanceled
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForVerification"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: emojiSas
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForReady"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORREADY
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "incoming"
|
||||
when: root.session.state === KeyVerificationSession.INCOMING
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForMac"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "ready"
|
||||
when: root.session.state === KeyVerificationSession.READY
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: chooseVerificationComponent
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "done"
|
||||
when: root.session.state === KeyVerificationSession.DONE
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: stateLoader
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
visible: root.session.state === KeyVerificationSession.INCOMING
|
||||
QQC2.DialogButtonBox {
|
||||
anchors.fill: parent
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18n("Accept")
|
||||
icon.name: "dialog-ok"
|
||||
onClicked: root.session.sendReady()
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18n("Decline")
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: root.session.cancelVerification("m.user", "Declined")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verificationCanceled
|
||||
VerificationCanceled {
|
||||
reason: root.session.error
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emojiSas
|
||||
EmojiSas {
|
||||
model: root.session.sasEmojis
|
||||
onReject: root.session.cancelVerification(KeyVerificationSession.MISMATCHED_SAS)
|
||||
onAccept: root.session.sendMac()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: message
|
||||
VerificationMessage {
|
||||
icon: {
|
||||
switch (root.session.state) {
|
||||
case KeyVerificationSession.WAITINGFORREADY:
|
||||
case KeyVerificationSession.INCOMING:
|
||||
case KeyVerificationSession.WAITINGFORMAC:
|
||||
return "security-medium-symbolic";
|
||||
case KeyVerificationSession.DONE:
|
||||
return "security-high";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
text: {
|
||||
switch (root.session.state) {
|
||||
case KeyVerificationSession.WAITINGFORREADY:
|
||||
return i18n("Waiting for device to accept verification.");
|
||||
case KeyVerificationSession.INCOMING:
|
||||
return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId);
|
||||
case KeyVerificationSession.WAITINGFORMAC:
|
||||
return i18n("Waiting for other party to verify.");
|
||||
case KeyVerificationSession.DONE:
|
||||
return i18n("Successfully verified device **%1**", root.session.remoteDeviceId)
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: chooseVerificationComponent
|
||||
Item {
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
QQC2.Label {
|
||||
text: i18nc("@info", "Choose a verification method to continue")
|
||||
}
|
||||
QQC2.Button {
|
||||
id: emojiVerification
|
||||
text: i18nc("@action:button", "Emoji Verification")
|
||||
icon.name: "smiley"
|
||||
onClicked: root.session.sendStartSas()
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/app/qml/LocationChooser.qml
Normal file
100
src/app/qml/LocationChooser.qml
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Components.AbstractMaximizeComponent {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
property var location
|
||||
|
||||
title: i18n("Choose a Location")
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send this location")
|
||||
onTriggered: {
|
||||
root.room.sendLocation(root.location.latitude, root.location.longitude, "");
|
||||
root.close();
|
||||
}
|
||||
enabled: !!root.location
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:intoolbar Re-center the map onto the set location", "Re-Center")
|
||||
icon.name: "snap-bounding-box-center-symbolic"
|
||||
onTriggered: mapView.map.fitViewportToMapItems([mapView.locationMapItem])
|
||||
enabled: root.location !== undefined
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:intoolbar Determine the device's location", "Locate")
|
||||
icon.name: "mark-location-symbolic"
|
||||
enabled: positionSource.valid
|
||||
onTriggered: positionSource.update()
|
||||
}
|
||||
]
|
||||
|
||||
PositionSource {
|
||||
id: positionSource
|
||||
|
||||
active: false
|
||||
|
||||
onPositionChanged: {
|
||||
const coord = position.coordinate;
|
||||
mapView.gpsMapItem.latitude = coord.latitude;
|
||||
mapView.gpsMapItem.longitude = coord.longitude;
|
||||
|
||||
mapView.map.addMapItem(mapView.gpsMapItem);
|
||||
mapView.map.fitViewportToMapItems([mapView.gpsMapItem])
|
||||
}
|
||||
}
|
||||
|
||||
content: MapView {
|
||||
id: mapView
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.location = mapView.map.toCoordinate(Qt.point(mouseX, mouseY), false);
|
||||
mapView.map.addMapItem(mapView.locationMapItem);
|
||||
}
|
||||
}
|
||||
|
||||
readonly property LocationMapItem locationMapItem: LocationMapItem {
|
||||
latitude: root.location.latitude
|
||||
longitude: root.location.longitude
|
||||
isLive: false
|
||||
heading: NaN
|
||||
asset: ""
|
||||
author: null
|
||||
}
|
||||
|
||||
readonly property LocationMapItem gpsMapItem: LocationMapItem {
|
||||
latitude: 0.0
|
||||
longitude: 0.0
|
||||
isLive: true
|
||||
heading: NaN
|
||||
asset: ""
|
||||
author: null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated(link: string) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/app/qml/LocationMapItem.qml
Normal file
76
src/app/qml/LocationMapItem.qml
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/** Location marker for any of the shared location maps. */
|
||||
MapQuickItem {
|
||||
id: root
|
||||
required property real latitude
|
||||
required property real longitude
|
||||
|
||||
required property string asset
|
||||
required property var author
|
||||
|
||||
required property bool isLive
|
||||
|
||||
required property real heading
|
||||
|
||||
anchorPoint.x: sourceItem.width / 2
|
||||
anchorPoint.y: sourceItem.height
|
||||
coordinate: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||
autoFadeIn: false
|
||||
sourceItem: Kirigami.Icon {
|
||||
id: mainIcon
|
||||
width: height
|
||||
height: Kirigami.Units.iconSizes.huge
|
||||
source: "gps"
|
||||
isMask: true
|
||||
color: root.isLive ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -parent.height / 8
|
||||
visible: root.asset === "m.pin"
|
||||
width: height
|
||||
height: parent.height / 3 + 1
|
||||
source: "pin"
|
||||
isMask: true
|
||||
color: parent.color
|
||||
}
|
||||
KirigamiComponents.Avatar {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: -parent.height / 8
|
||||
visible: root.asset === "m.self"
|
||||
width: height
|
||||
height: parent.height / 3 + 1
|
||||
name: root.author.displayName
|
||||
source: root.author.avatarUrl
|
||||
color: root.author.color
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
id: headingIcon
|
||||
source: "go-up-symbolic"
|
||||
color: parent.color
|
||||
visible: !isNaN(root.heading) && root.isLive
|
||||
anchors.bottom: mainIcon.top
|
||||
anchors.horizontalCenter: mainIcon.horizontalCenter
|
||||
transform: Rotation {
|
||||
origin.x: headingIcon.width / 2
|
||||
origin.y: headingIcon.height + mainIcon.height / 2
|
||||
angle: root.heading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/app/qml/LocationsPage.qml
Normal file
72
src/app/qml/LocationsPage.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
required property var room
|
||||
|
||||
title: i18nc("Locations on a map", "Locations")
|
||||
|
||||
padding: 0
|
||||
|
||||
MapView {
|
||||
id: mapView
|
||||
anchors.fill: parent
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
visible: mapView.map.mapItems.length !== 0
|
||||
|
||||
map.center: {
|
||||
let c = LocationHelper.center(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox));
|
||||
return QtPositioning.coordinate(c.y, c.x);
|
||||
}
|
||||
map.zoomLevel: {
|
||||
const zoom = LocationHelper.zoomToFit(LocationHelper.unite(locationsModel.boundingBox, liveLocationsModel.boundingBox), mapView.width, mapView.height)
|
||||
return Math.min(Math.max(zoom, map.minimumZoomLevel), map.maximumZoomLevel);
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
Component.onCompleted: mapView.map.addMapItemView(this)
|
||||
anchors.fill: parent
|
||||
|
||||
model: LocationsModel {
|
||||
id: locationsModel
|
||||
room: root.room
|
||||
}
|
||||
delegate: LocationMapItem {
|
||||
isLive: false
|
||||
heading: NaN
|
||||
}
|
||||
}
|
||||
|
||||
MapItemView {
|
||||
Component.onCompleted: mapView.map.addMapItemView(this)
|
||||
anchors.fill: parent
|
||||
model: LiveLocationsModel {
|
||||
id: liveLocationsModel
|
||||
room: root.room
|
||||
}
|
||||
delegate: LocationMapItem {}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated(link: string) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
text: i18n("There are no locations shared in this room.")
|
||||
visible: mapView.map.mapItems.length === 0
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
362
src/app/qml/Main.qml
Normal file
362
src/app/qml/Main.qml
Normal file
@@ -0,0 +1,362 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.config as KConfig
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.login
|
||||
import org.kde.neochat.settings
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property NeoChatConnection connection: Controller.activeConnection
|
||||
readonly property HoverLinkIndicator hoverLinkIndicator: linkIndicator
|
||||
|
||||
property bool initialized: false
|
||||
|
||||
title: {
|
||||
if (NeoChatConfig.windowTitleFocus) {
|
||||
return activeFocusItem + " " + (activeFocusItem ? activeFocusItem.Accessible.name : "");
|
||||
} else if (RoomManager.currentRoom) {
|
||||
return RoomManager.currentRoom.displayName;
|
||||
} else {
|
||||
return Application.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
minimumWidth: Kirigami.Units.gridUnit * 20
|
||||
minimumHeight: Kirigami.Units.gridUnit * 15
|
||||
|
||||
visible: false // Will be overridden in Component.onCompleted
|
||||
wideScreen: width > Kirigami.Units.gridUnit * 65
|
||||
|
||||
pageStack {
|
||||
initialPage: WelcomePage {
|
||||
showExisting: true
|
||||
onConnectionChosen: root.load()
|
||||
}
|
||||
globalToolBar.canContainHandles: true
|
||||
globalToolBar {
|
||||
style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||
showNavigationButtons: pageStack.currentIndex > 0 || pageStack.layers.depth > 1 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
|
||||
}
|
||||
}
|
||||
|
||||
onConnectionChanged: {
|
||||
CustomEmojiModel.connection = root.connection;
|
||||
SpaceHierarchyCache.connection = root.connection;
|
||||
NeoChatSettingsView.connection = root.connection;
|
||||
if (ShareHandler.text && root.connection) {
|
||||
root.handleShare();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onLoaded() {
|
||||
root.load();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Registration
|
||||
function onLoaded() {
|
||||
root.load();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.quitAction
|
||||
function onTriggered() {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
|
||||
sourceComponent: Qt.createComponent("org.kde.neochat", "GlobalMenu")
|
||||
onActiveChanged: if (active) {
|
||||
item.connection = root.connection;
|
||||
}
|
||||
}
|
||||
|
||||
KConfig.WindowStateSaver {
|
||||
configGroupName: "MainWindow"
|
||||
}
|
||||
|
||||
QuickSwitcher {
|
||||
id: quickSwitcher
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
if (RoomManager.currentRoom && pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
|
||||
let roomPage = pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'), {
|
||||
connection: root.connection
|
||||
});
|
||||
roomPage.backRequested.connect(event => {
|
||||
RoomManager.clearCurrentRoom();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onAskJoinRoom(room) {
|
||||
Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
|
||||
room: room,
|
||||
connection: root.connection
|
||||
}).open();
|
||||
}
|
||||
|
||||
function onShowUserDetail(user, room) {
|
||||
root.showUserDetail(user, room);
|
||||
}
|
||||
|
||||
function goToEvent(event) {
|
||||
if (event.length > 0) {
|
||||
roomItem.goToEvent(event);
|
||||
}
|
||||
roomItem.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onAskDirectChatConfirmation(user) {
|
||||
Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
|
||||
user: user
|
||||
}).open();
|
||||
}
|
||||
|
||||
function onExternalUrl(url) {
|
||||
let dialog = Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this);
|
||||
dialog.link = url;
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
function pushReplaceLayer(page, args) {
|
||||
if (pageStack.layers.depth === 2) {
|
||||
pageStack.layers.replace(page, args);
|
||||
} else {
|
||||
pageStack.layers.push(page, args);
|
||||
}
|
||||
}
|
||||
|
||||
function openRoomDrawer() {
|
||||
pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomDrawerPage'), {
|
||||
connection: root.connection
|
||||
});
|
||||
}
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
id: contextDrawer
|
||||
|
||||
// This is a memory for all user initiated actions on the drawer, i.e. clicking the button
|
||||
// It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width
|
||||
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
|
||||
|
||||
connection: root.connection
|
||||
|
||||
handleClosedIcon.source: "documentinfo-symbolic"
|
||||
handleClosedToolTip: i18nc("@action:button", "Show Room Information")
|
||||
|
||||
// Default icon is fine, only need to override the tooltip text
|
||||
handleOpenToolTip: i18nc("@action:button", "Close Room Information Drawer")
|
||||
|
||||
// Connect to the onClicked function of the RoomDrawer handle button
|
||||
Connections {
|
||||
target: contextDrawer.handle.children[0]
|
||||
function onClicked() {
|
||||
contextDrawer.drawerUserState = contextDrawer.drawerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
modal: (!root.wideScreen || !enabled)
|
||||
onEnabledChanged: drawerOpen = enabled && !modal
|
||||
onModalChanged: {
|
||||
if (NeoChatConfig.autoRoomInfoDrawer) {
|
||||
drawerOpen = !modal && drawerUserState;
|
||||
dim = false;
|
||||
}
|
||||
}
|
||||
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 && (pageStack.visibleItems.length > 1 || pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
|
||||
handleVisible: enabled
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
CustomEmojiModel.connection = root.connection;
|
||||
SpaceHierarchyCache.connection = root.connection;
|
||||
RoomSettingsView.window = root;
|
||||
NeoChatSettingsView.window = root;
|
||||
NeoChatSettingsView.connection = root.connection;
|
||||
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
TextToSpeechWrapper.warmUp();
|
||||
if (ShareHandler.text && root.connection) {
|
||||
root.handleShare()
|
||||
}
|
||||
const hasSystemTray = Controller.supportSystemTray && NeoChatConfig.systemTray;
|
||||
if (Kirigami.Settings.isMobile || !(hasSystemTray && NeoChatConfig.minimizeToSystemTrayOnStartup)) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: NeoChatConfig
|
||||
function onBlurChanged() {
|
||||
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
}
|
||||
function onCompactLayoutChanged() {
|
||||
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
}
|
||||
}
|
||||
|
||||
// blur effect
|
||||
color: NeoChatConfig.blur && !NeoChatConfig.compactLayout ? "transparent" : Kirigami.Theme.backgroundColor
|
||||
|
||||
// we need to apply the translucency effect separately on top of the color
|
||||
background: Rectangle {
|
||||
color: NeoChatConfig.blur && !NeoChatConfig.compactLayout ? Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1 - NeoChatConfig.transparency) : "transparent"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomListComponent
|
||||
RoomListPage {
|
||||
id: roomList
|
||||
|
||||
onSearch: quickSwitcher.open()
|
||||
|
||||
connection: root.connection
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Ctrl+PgUp", "Ctrl+Backtab", "Alt+Up"]
|
||||
onActivated: {
|
||||
roomList.goToPreviousRoom();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Ctrl+PgDown", "Ctrl+Tab", "Alt+Down"]
|
||||
onActivated: {
|
||||
roomList.goToNextRoom();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Shift+Up"
|
||||
onActivated: {
|
||||
roomList.goToPreviousUnreadRoom();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Shift+Down"
|
||||
onActivated: {
|
||||
roomList.goToNextUnreadRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AccountRegistry
|
||||
function onRowsRemoved() {
|
||||
if (AccountRegistry.rowCount() === 0) {
|
||||
pageStack.clear();
|
||||
pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Controller
|
||||
|
||||
function onErrorOccured(error) {
|
||||
showPassiveNotification(error, "short");
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.connection
|
||||
|
||||
function onNewKeyVerificationSession(session) {
|
||||
root.pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "KeyVerificationDialog"), {
|
||||
session: session
|
||||
}, {
|
||||
title: i18nc("@title:window", "Session Verification")
|
||||
});
|
||||
}
|
||||
function onUserConsentRequired(url) {
|
||||
Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
|
||||
url: url
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
HoverLinkIndicator {
|
||||
id: linkIndicator
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
rightMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Shift+,"
|
||||
onActivated: {
|
||||
NeoChatSettingsView.open();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShareHandler
|
||||
function onTextChanged(): void {
|
||||
if (root.connection && ShareHandler.text.length > 0) {
|
||||
root.handleShare();
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleShare(): void {
|
||||
const dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Share"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
dialog.chosen.connect(function(targetRoomId) {
|
||||
RoomManager.resolveResource(targetRoomId)
|
||||
ShareHandler.room = targetRoomId
|
||||
dialog.closeDialog()
|
||||
})
|
||||
}
|
||||
function showUserDetail(user, room) {
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root, {
|
||||
room: room,
|
||||
user: user,
|
||||
connection: root.connection,
|
||||
});
|
||||
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
function load() {
|
||||
pageStack.replace(roomListComponent);
|
||||
RoomManager.loadInitialRoom();
|
||||
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'), {
|
||||
connection: root.connection
|
||||
});
|
||||
roomPage.forceActiveFocus();
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
113
src/app/qml/ManualRoomDialog.qml
Normal file
113
src/app/qml/ManualRoomDialog.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when a valid room id or alias is entered.
|
||||
*/
|
||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||
|
||||
title: i18nc("@title", "Manually Enter a Room")
|
||||
|
||||
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: roomIdAliasText.isValidText
|
||||
text: i18n("OK")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
// We don't necessarily have all the info so fill out the best we can.
|
||||
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
|
||||
let displayName = "";
|
||||
let avatarUrl = "";
|
||||
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
|
||||
let topic = "";
|
||||
let memberCount = -1;
|
||||
let isJoined = false;
|
||||
if (roomIdAliasText.room) {
|
||||
roomId = roomIdAliasText.room.id;
|
||||
displayName = roomIdAliasText.room.displayName;
|
||||
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
|
||||
alias = roomIdAliasText.room.canonicalAlias;
|
||||
topic = roomIdAliasText.room.topic;
|
||||
memberCount = roomIdAliasText.room.joinedCount;
|
||||
isJoined = true;
|
||||
}
|
||||
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: roomIdAliasText
|
||||
property bool isValidText: text.match(/(#|!)(.+):(.+)/g)
|
||||
property bool correctStart: text.startsWith("#") || text.startsWith("!")
|
||||
property NeoChatRoom room: {
|
||||
if (!acceptableInput) {
|
||||
return null;
|
||||
}
|
||||
if (isAlias()) {
|
||||
return root.connection.roomByAlias(text);
|
||||
} else {
|
||||
return root.connection.room(text);
|
||||
}
|
||||
}
|
||||
|
||||
label: i18n("Room ID or Alias:")
|
||||
statusMessage: {
|
||||
if (text.length > 0 && !correctStart) {
|
||||
return i18n("Must start with # for an alias or ! for an ID");
|
||||
}
|
||||
if (timer.running) {
|
||||
return "";
|
||||
}
|
||||
if (text.length > 0 && !isValidText) {
|
||||
return i18n("The input is not a valid room ID or alias");
|
||||
}
|
||||
return correctStart ? "" : i18n("Must start with # for an alias or ! for an ID");
|
||||
}
|
||||
status: text.length > 0 ? Kirigami.MessageType.Error : Kirigami.MessageType.Information
|
||||
|
||||
onTextEdited: timer.restart()
|
||||
|
||||
function isAlias() {
|
||||
return roomIdAliasText.text.startsWith("#");
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
interval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
roomIdAliasText.forceActiveFocus();
|
||||
timer.restart();
|
||||
}
|
||||
}
|
||||
82
src/app/qml/ManualUserDialog.qml
Normal file
82
src/app/qml/ManualUserDialog.qml
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief Thrown when a user is selected.
|
||||
*/
|
||||
signal userSelected
|
||||
|
||||
title: i18nc("@title", "User ID")
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: userIdText.isValidText
|
||||
text: i18n("OK")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
root.connection.requestDirectChat(userIdText.text);
|
||||
root.accept();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: userIdText
|
||||
property bool isValidText: text.match(/@(.+):(.+)/g)
|
||||
property bool correctStart: text.startsWith("@")
|
||||
|
||||
label: i18n("User ID:")
|
||||
statusMessage: {
|
||||
if (text.length > 0 && !correctStart) {
|
||||
return i18n("User IDs Must start with @");
|
||||
}
|
||||
if (timer.running) {
|
||||
return "";
|
||||
}
|
||||
if (text.length > 0 && !isValidText) {
|
||||
return i18n("The input is not a valid user ID");
|
||||
}
|
||||
return correctStart ? "" : i18n("User IDs Must start with @");
|
||||
}
|
||||
status: text.length > 0 ? Kirigami.MessageType.Error : Kirigami.MessageType.Information
|
||||
|
||||
onTextEdited: timer.restart()
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
interval: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
userIdText.forceActiveFocus();
|
||||
timer.restart();
|
||||
}
|
||||
}
|
||||
138
src/app/qml/MessageSourceSheet.qml
Normal file
138
src/app/qml/MessageSourceSheet.qml
Normal file
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
property var model
|
||||
property NeoChatRoom room
|
||||
|
||||
property string type
|
||||
property string stateKey
|
||||
|
||||
property bool allowEdit: false
|
||||
|
||||
property string contentJson: model.stateEventContentJson(root.type, root.stateKey)
|
||||
property string sourceText: model.stateEventJson(root.type, root.stateKey)
|
||||
|
||||
topPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
Connections {
|
||||
enabled: root.model
|
||||
target: root.room
|
||||
function onChanged(): void {
|
||||
root.contentJson = model.stateEventContentJson(root.type, root.stateKey);
|
||||
root.sourceText = model.stateEventJson(root.type, root.stateKey);
|
||||
}
|
||||
}
|
||||
|
||||
title: i18n("Event Source")
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action As in 'edit the state of this room'", "Edit state")
|
||||
icon.name: "document-edit"
|
||||
visible: root.allowEdit
|
||||
enabled: room.canSendState(root.type) && (!root.stateKey.startsWith("@") || root.stateKey === root.room.connection.localUserId) && root.type !== "m.room.create"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "EditStateDialog.qml"), {
|
||||
room: root.room,
|
||||
type: root.type,
|
||||
stateKey: root.stateKey,
|
||||
sourceText: root.contentJson,
|
||||
}, {
|
||||
title: i18nc("@title As in 'edit the state of this room'", "Edit State")
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
QQC2.TextArea {
|
||||
id: sourceTextArea
|
||||
Layout.fillWidth: true
|
||||
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
text: root.sourceText
|
||||
readOnly: true
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
// opt-out of whatever spell checker a styled TextArea might come with
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
onWidthChanged: lineModel.resetModel()
|
||||
onHeightChanged: lineModel.resetModel()
|
||||
|
||||
SyntaxHighlighter {
|
||||
textEdit: sourceTextArea
|
||||
definition: "JSON"
|
||||
repository: Repository
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
|
||||
anchors {
|
||||
top: sourceTextArea.top
|
||||
topMargin: sourceTextArea.topPadding
|
||||
left: sourceTextArea.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: sourceTextArea.textDocument
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
required property int index
|
||||
required property int docLineHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: docLineHeight
|
||||
topPadding: 1
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: index + 1
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/app/qml/NeochatMaximizeComponent.qml
Normal file
138
src/app/qml/NeochatMaximizeComponent.qml
Normal file
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtCore as Core
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs as Dialogs
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Components.AlbumMaximizeComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom currentRoom
|
||||
|
||||
readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.EventIdRole)
|
||||
|
||||
readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.AuthorRole)
|
||||
|
||||
readonly property var currentTime: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.TimeRole)
|
||||
|
||||
readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
|
||||
|
||||
onCurrentProgressInfoChanged: () => {
|
||||
if (root.currentProgressInfo) {
|
||||
root.downloadAction.progress = root.currentProgressInfo.progress / root.currentProgressInfo.total * 100.0;
|
||||
} else {
|
||||
root.downloadAction.progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the delegate is part of a thread timeline.
|
||||
*/
|
||||
property bool isThread: false
|
||||
|
||||
downloadAction: Components.DownloadAction {
|
||||
onTriggered: {
|
||||
currentRoom.downloadFile(root.currentEventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.currentEventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(root.currentEventId));
|
||||
}
|
||||
}
|
||||
|
||||
playAction: Kirigami.Action {
|
||||
onTriggered: {
|
||||
MediaManager.startPlayback();
|
||||
currentItem.play();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: MediaManager
|
||||
function onPlaybackStarted() {
|
||||
if (currentItem.playbackState === MediaPlayer.PlayingState) {
|
||||
currentItem.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentRoom
|
||||
|
||||
function onFileTransferProgress(id, progress, total) {
|
||||
if (id == root.currentEventId) {
|
||||
root.downloadAction.progress = progress / total * 100.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leading: RowLayout {
|
||||
Components.Avatar {
|
||||
id: userAvatar
|
||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: root.currentAuthor.name ?? root.currentAuthor.displayName
|
||||
source: root.currentAuthor.avatarUrl
|
||||
color: root.currentAuthor.color
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
id: userLabel
|
||||
|
||||
text: root.currentAuthor.name ?? root.currentAuthor.displayName
|
||||
color: root.currentAuthor.color
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: dateTimeLabel
|
||||
text: root.currentTime.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom, root.currentAuthor)
|
||||
|
||||
onSaveItem: {
|
||||
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
|
||||
dialog.selectedFile = currentRoom.fileNameToDownload(root.currentEventId);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCloseFullScreen() {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: saveAsDialog
|
||||
Dialogs.FileDialog {
|
||||
fileMode: Dialogs.FileDialog.SaveFile
|
||||
currentFolder: root.saveFolder
|
||||
onAccepted: {
|
||||
NeoChatConfig.lastSaveDirectory = currentFolder;
|
||||
NeoChatConfig.save();
|
||||
if (!selectedFile) {
|
||||
return;
|
||||
}
|
||||
currentRoom.downloadFile(root.currentEventId, selectedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/app/qml/NewPollDialog.qml
Normal file
156
src/app/qml/NewPollDialog.qml
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import Quotient
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
|
||||
text: i18nc("@action:button", "Send")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title: create new poll in the room", "Create Poll")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: pollTypeCombo
|
||||
|
||||
text: i18n("Poll type:")
|
||||
currentIndex: 0
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: PollKind.Disclosed, text: i18n("Open poll") },
|
||||
{ value: PollKind.Undisclosed, text: i18n("Closed poll") }
|
||||
]
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
verticalPadding: 0
|
||||
text: pollTypeCombo.currentValue == 0 ? i18n("Voters can see the result as soon as they have voted") : i18n("Results are revealed only after the poll has closed")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: questionTextField
|
||||
label: i18n("Question:")
|
||||
}
|
||||
Repeater {
|
||||
id: optionRepeater
|
||||
|
||||
model: ListModel {
|
||||
id: optionModel
|
||||
|
||||
readonly property bool allValuesSet: {
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
if (optionModel.get(i).optionText.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
|
||||
function values() {
|
||||
let textValues = []
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
textValues.push(optionModel.get(i).optionText);
|
||||
}
|
||||
return textValues;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: optionDelegate
|
||||
|
||||
required property int index
|
||||
required property string optionText
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.Label {
|
||||
id: optionLabel
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("As in first answer option to the poll", "Option %1:", optionDelegate.index + 1)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
Accessible.ignored: true
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.TextField {
|
||||
id: textField
|
||||
Layout.fillWidth: true
|
||||
Accessible.name: optionLabel.text
|
||||
onTextChanged: {
|
||||
optionModel.set(optionDelegate.index, {optionText: text})
|
||||
optionModel.allValuesSetChanged()
|
||||
}
|
||||
placeholderText: i18n("Enter option")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
id: removeOptionAction
|
||||
text: i18nc("@action:button", "Remove option")
|
||||
icon.name: "edit-delete-remove"
|
||||
onTriggered: optionModel.remove(optionDelegate.index)
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
text: removeOptionAction.text
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
}
|
||||
Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
horizontalPadding: Kirigami.Units.largeSpacing * 2
|
||||
leftInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
rightInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
|
||||
highlighted: true
|
||||
|
||||
icon.name: "list-add"
|
||||
text: i18nc("@action:button", "Add option")
|
||||
|
||||
onClicked: optionModel.append({optionText: ""})
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/app/qml/NotificationsView.qml
Normal file
93
src/app/qml/NotificationsView.qml
Normal file
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigami.delegates as KD
|
||||
import org.kde.kirigamiaddons.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: i18nc("@title", "Notifications")
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
model: NotificationsModel {
|
||||
id: notificationsModel
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
visible: listView.count === 0
|
||||
text: notificationsModel.loading ? i18n("Loading…") : i18n("No Notifications")
|
||||
}
|
||||
|
||||
footer: Kirigami.PlaceholderMessage {
|
||||
width: parent.width
|
||||
text: i18n("Loading…")
|
||||
visible: notificationsModel.nextToken.length > 0 && listView.count > 0
|
||||
}
|
||||
|
||||
delegate: QQC2.ItemDelegate {
|
||||
width: parent?.width ?? 0
|
||||
|
||||
onClicked: RoomManager.resolveResource(model.uri)
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Components.Avatar {
|
||||
source: model.authorAvatar
|
||||
name: model.authorName
|
||||
implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
|
||||
implicitWidth: implicitHeight
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
QQC2.Label {
|
||||
id: label
|
||||
|
||||
text: model.roomDisplayName
|
||||
elide: Text.ElideRight
|
||||
font.weight: Font.Normal
|
||||
textFormat: Text.PlainText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: subtitle
|
||||
|
||||
text: model.text
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: root.hasNotifications ? 0.9 : 0.7
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/app/qml/OpenFileDialog.qml
Normal file
14
src/app/qml/OpenFileDialog.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Dialogs
|
||||
|
||||
FileDialog {
|
||||
id: root
|
||||
|
||||
signal chosen(string path)
|
||||
|
||||
title: i18n("Select a File")
|
||||
onAccepted: root.chosen(selectedFile)
|
||||
}
|
||||
24
src/app/qml/OsmLocationPlugin.qml
Normal file
24
src/app/qml/OsmLocationPlugin.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtLocation
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property string userAgent: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
|
||||
property var plugin: Plugin {
|
||||
name: "osm"
|
||||
PluginParameter {
|
||||
name: "osm.useragent"
|
||||
value: root.userAgent
|
||||
}
|
||||
PluginParameter {
|
||||
name: "osm.mapping.providersrepository.address"
|
||||
value: "https://autoconfig.kde.org/qtlocation/"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/app/qml/QrCodeMaximizeComponent.qml
Normal file
50
src/app/qml/QrCodeMaximizeComponent.qml
Normal file
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.prison
|
||||
|
||||
Components.AbstractMaximizeComponent {
|
||||
id: root
|
||||
|
||||
required property string text
|
||||
property color avatarColor
|
||||
required property url avatarSource
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
Shortcut {
|
||||
sequences: [StandardKey.Cancel]
|
||||
onActivated: root.close()
|
||||
}
|
||||
|
||||
leading: Components.Avatar {
|
||||
id: userAvatar
|
||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: root.title
|
||||
source: root.avatarSource
|
||||
color: root.avatarColor
|
||||
}
|
||||
|
||||
content: Item {
|
||||
Keys.onEscapePressed: root.close()
|
||||
Barcode {
|
||||
barcodeType: Barcode.QRCode
|
||||
content: root.text
|
||||
height: Math.min(parent.height, Kirigami.Units.gridUnit * 20)
|
||||
width: height
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/app/qml/QrScannerPage.qml
Normal file
58
src/app/qml/QrScannerPage.qml
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.prison.scanner as Prison
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
|
||||
required property NeoChatConnection connection
|
||||
padding: 0
|
||||
|
||||
Component.onCompleted: camera.start()
|
||||
|
||||
Connections {
|
||||
target: root.QQC2.ApplicationWindow.window
|
||||
function onClosing() {
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
VideoOutput {
|
||||
id: viewFinder
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
Prison.VideoScanner {
|
||||
id: scanner
|
||||
property string previousText: ""
|
||||
formats: Prison.Format.QRCode | Prison.Format.Aztec
|
||||
onResultChanged: {
|
||||
if (result.text.length > 0 && result.text != scanner.previousText) {
|
||||
RoomManager.resolveResource(result.text, "qr");
|
||||
scanner.previousText = result.text;
|
||||
}
|
||||
root.closeDialog();
|
||||
}
|
||||
videoSink: viewFinder.videoSink
|
||||
}
|
||||
|
||||
CaptureSession {
|
||||
camera: Camera {
|
||||
id: camera
|
||||
}
|
||||
imageCapture: ImageCapture {
|
||||
id: imageCapture
|
||||
}
|
||||
videoOutput: viewFinder
|
||||
}
|
||||
}
|
||||
136
src/app/qml/QuickFormatBar.qml
Normal file
136
src/app/qml/QuickFormatBar.qml
Normal file
@@ -0,0 +1,136 @@
|
||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
QQC2.Popup {
|
||||
id: root
|
||||
|
||||
property var selectionStart
|
||||
property var selectionEnd
|
||||
|
||||
signal formattingSelected(var format, int selectionStart, int selectionEnd)
|
||||
|
||||
padding: 1
|
||||
|
||||
contentItem: Flow {
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-bold"
|
||||
text: i18n("Bold")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: "**",
|
||||
end: "**",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-italic"
|
||||
text: i18n("Italic")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: "*",
|
||||
end: "*",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-strikethrough"
|
||||
text: i18n("Strikethrough")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: "<del>",
|
||||
end: "</del>",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-code"
|
||||
text: i18n("Code block")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: "`",
|
||||
end: "`",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-blockquote"
|
||||
text: i18n("Quote")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: selectionStart == 0 ? ">" : "\n>",
|
||||
end: "\n\n",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "link"
|
||||
text: i18n("Insert link")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: "[",
|
||||
end: "](",
|
||||
extra: ")"
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/app/qml/QuickSwitcher.qml
Normal file
56
src/app/qml/QuickSwitcher.qml
Normal file
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.SearchDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: root.open()
|
||||
}
|
||||
|
||||
onAccepted: if (currentItem) {
|
||||
currentItem.clicked();
|
||||
}
|
||||
|
||||
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
|
||||
model: RoomManager.sortFilterRoomListModel
|
||||
emptyText: i18nc("Placeholder message", "No room found")
|
||||
Kirigami.Action {
|
||||
id: exploreRoomAction
|
||||
text: i18nc("@action:button", "Explore rooms")
|
||||
icon.name: "compass"
|
||||
onTriggered: {
|
||||
root.close()
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: emptyHelpfulAction = exploreRoomAction
|
||||
|
||||
parent: QQC2.Overlay.overlay
|
||||
|
||||
delegate: RoomDelegate {
|
||||
connection: root.connection
|
||||
onClicked: root.close()
|
||||
showConfigure: false
|
||||
}
|
||||
}
|
||||
59
src/app/qml/ReasonDialog.qml
Normal file
59
src/app/qml/ReasonDialog.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
required property string placeholder
|
||||
required property string actionText
|
||||
required property string icon
|
||||
|
||||
signal accepted(reason: string)
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
QQC2.TextArea {
|
||||
id: reason
|
||||
placeholderText: root.placeholder
|
||||
anchors.fill: parent
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.ToolBar {
|
||||
QQC2.DialogButtonBox {
|
||||
anchors.fill: parent
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Button {
|
||||
text: root.actionText
|
||||
icon.name: root.icon
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: {
|
||||
root.accepted(reason.text);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18nc("@action", "Cancel")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/app/qml/RecommendedSpaceDialog.qml
Normal file
70
src/app/qml/RecommendedSpaceDialog.qml
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
property var connection
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
title: i18nc("@title Join <name of a space>", "Join %1", SpaceHierarchyCache.recommendedSpaceDisplayName)
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
FormCard.AbstractFormDelegate {
|
||||
background: null
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
Avatar {
|
||||
source: SpaceHierarchyCache.recommendedSpaceAvatar.toString().length > 0 ? root.connection.makeMediaUrl(SpaceHierarchyCache.recommendedSpaceAvatar) : 0
|
||||
name: SpaceHierarchyCache.recommendedSpaceDisplayName
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
text: SpaceHierarchyCache.recommendedSpaceDisplayName
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: SpaceHierarchyCache.recommendedSpaceDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormDelegateSeparator {}
|
||||
FormCard.FormButtonDelegate {
|
||||
text: i18nc("@action:button", "Join")
|
||||
icon.name: "list-add-symbolic"
|
||||
onClicked: {
|
||||
SpaceHierarchyCache.recommendedSpaceHidden = true;
|
||||
RoomManager.resolveResource(SpaceHierarchyCache.recommendedSpaceId, "join");
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
icon.name: "mail-thread-ignored-symbolic"
|
||||
text: i18nc("@action:button", "Ignore")
|
||||
onClicked: {
|
||||
SpaceHierarchyCache.recommendedSpaceHidden = true;
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/app/qml/RemoveChildDialog.qml
Normal file
49
src/app/qml/RemoveChildDialog.qml
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom parentRoom
|
||||
|
||||
required property string roomId
|
||||
|
||||
required property string displayName
|
||||
|
||||
required property string parentDisplayName
|
||||
|
||||
required property bool canSetParent
|
||||
|
||||
required property bool isDeclaredParent
|
||||
|
||||
title: i18nc("@title", "Remove Child")
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
|
||||
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
|
||||
|
||||
onAccepted: parentRoom.removeChild(root.roomId, removeOfficalCheck.checked)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("The child %1 will be removed from the space %2", root.displayName, root.parentDisplayName)
|
||||
textItem.wrapMode: Text.Wrap
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
id: removeOfficalCheck
|
||||
visible: root.isDeclaredParent
|
||||
enabled: root.canSetParent
|
||||
text: i18n("The current space is the official parent of this room, should this be cleared?")
|
||||
checked: root.canSetParent
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/app/qml/RoomDrawer.qml
Normal file
179
src/app/qml/RoomDrawer.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
Kirigami.OverlayDrawer {
|
||||
id: root
|
||||
|
||||
readonly property NeoChatRoom room: RoomManager.currentRoom
|
||||
required property NeoChatConnection connection
|
||||
|
||||
width: actualWidth
|
||||
interactive: modal
|
||||
|
||||
readonly property int minWidth: Kirigami.Units.gridUnit * 15
|
||||
readonly property int maxWidth: Kirigami.Units.gridUnit * 25
|
||||
readonly property int defaultWidth: Kirigami.Units.gridUnit * 20
|
||||
property int actualWidth: {
|
||||
if (NeoChatConfig.roomDrawerWidth === -1) {
|
||||
return Kirigami.Units.gridUnit * 20;
|
||||
} else {
|
||||
return NeoChatConfig.roomDrawerWidth;
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: undefined
|
||||
width: 2
|
||||
z: 500
|
||||
cursorShape: !Kirigami.Settings.isMobile ? Qt.SplitHCursor : undefined
|
||||
enabled: true
|
||||
visible: true
|
||||
onPressed: _lastX = mapToGlobal(mouseX, mouseY).x
|
||||
onReleased: {
|
||||
NeoChatConfig.roomDrawerWidth = root.actualWidth;
|
||||
NeoChatConfig.save();
|
||||
}
|
||||
property real _lastX: -1
|
||||
|
||||
onPositionChanged: {
|
||||
if (_lastX === -1) {
|
||||
return;
|
||||
}
|
||||
if (Qt.application.layoutDirection === Qt.RightToLeft) {
|
||||
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, NeoChatConfig.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x));
|
||||
} else {
|
||||
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, NeoChatConfig.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x));
|
||||
}
|
||||
}
|
||||
}
|
||||
enabled: true
|
||||
|
||||
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
|
||||
// If modal has been changed and the drawer is closed automatically then dim on popup open will have been switched off in main.qml so switch it back on after the animation completes.
|
||||
// This is to avoid dim being active for a split second when the drawer is switched to modal which looks terrible.
|
||||
onAnimatingChanged: if (dim === false)
|
||||
dim = undefined
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
contentItem: Loader {
|
||||
id: loader
|
||||
active: root.drawerOpen
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
visible: root.modal
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Component.onCompleted: infoAction.toggle()
|
||||
|
||||
QQC2.ToolBar {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredHeight: pageStack.globalToolBar.preferredHeight
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
text: drawerItemLoader.item ? drawerItemLoader.item.title : ""
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: settingsButton
|
||||
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Room settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
|
||||
onClicked: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: drawerItemLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: roomInformation
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomInformation
|
||||
RoomInformation {
|
||||
room: root.room
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomMedia
|
||||
RoomMedia {
|
||||
currentRoom: root.room
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.NavigationTabBar {
|
||||
id: navigationBar
|
||||
Layout.fillWidth: true
|
||||
visible: !root.room.isSpace
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
position: QQC2.ToolBar.Footer
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: infoAction
|
||||
text: i18n("Information")
|
||||
icon.name: "documentinfo"
|
||||
onTriggered: drawerItemLoader.sourceComponent = roomInformation
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Media")
|
||||
icon.name: "mail-attachment-symbollic"
|
||||
onTriggered: drawerItemLoader.sourceComponent = roomMedia
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/app/qml/RoomDrawerPage.qml
Normal file
110
src/app/qml/RoomDrawerPage.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Page for holding a room drawer component.
|
||||
*
|
||||
* This the companion component to RoomDrawer and is designed to be used on mobile
|
||||
* where we want the room drawer to be pushed as a page as thin drawer doesn't
|
||||
* look good.
|
||||
*
|
||||
* @sa RoomDrawer
|
||||
*/
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
readonly property NeoChatRoom room: RoomManager.currentRoom
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: drawerItemLoader.item ? drawerItemLoader.item.title : ""
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
Component.onCompleted: infoAction.toggle()
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
text: i18nc("@action:button", "Room settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: drawerItemLoader
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
sourceComponent: roomInformation
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomInformation
|
||||
RoomInformation {
|
||||
room: root.room
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomMedia
|
||||
RoomMedia {
|
||||
currentRoom: root.room
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
footer: Kirigami.NavigationTabBar {
|
||||
id: navigationBar
|
||||
visible: !root.room.isSpace
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: infoAction
|
||||
text: i18n("Information")
|
||||
icon.name: "documentinfo"
|
||||
onTriggered: drawerItemLoader.sourceComponent = roomInformation
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18n("Media")
|
||||
icon.name: "mail-attachment-symbollic"
|
||||
onTriggered: drawerItemLoader.sourceComponent = roomMedia
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: applicationWindow().pageStack
|
||||
onWideModeChanged: {
|
||||
if (applicationWindow().pageStack.wideMode) {
|
||||
applicationWindow().pageStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackRequested: event => {
|
||||
event.accepted = true;
|
||||
applicationWindow().pageStack.pop();
|
||||
}
|
||||
}
|
||||
298
src/app/qml/RoomInformation.qml
Normal file
298
src/app/qml/RoomInformation.qml
Normal file
@@ -0,0 +1,298 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Component for visualising the room information.
|
||||
*
|
||||
* The component has a header section which changes between group rooms and direct
|
||||
* chats with information like the avatar and topic. Followed by the allowed actions
|
||||
* and finally a user list.
|
||||
*
|
||||
* @note This component is only the contents, it will need to be placed in either
|
||||
* a drawer (desktop) or page (mobile) to be used.
|
||||
*
|
||||
* @sa RoomDrawer, RoomDrawerPage
|
||||
*/
|
||||
QQC2.ScrollView {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief The title that should be displayed for this component if available.
|
||||
*/
|
||||
readonly property string title: root.room.isSpace ? i18nc("@action:title", "Space Members") : i18nc("@action:title", "Room Information")
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: userList
|
||||
header: ColumnLayout {
|
||||
id: columnLayout
|
||||
|
||||
property alias userListSearchField: userListSearchField
|
||||
|
||||
spacing: 0
|
||||
width: ListView.view ? ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin : 0
|
||||
|
||||
Loader {
|
||||
active: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
visible: !root.room.isSpace
|
||||
sourceComponent: root.room.isDirectChat() ? directChatDrawerHeader : groupChatDrawerHeader
|
||||
onItemChanged: if (item) {
|
||||
userList.positionViewAtBeginning();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.ListSectionHeader {
|
||||
visible: !root.room.isSpace
|
||||
label: i18nc("Room actions", "Actions")
|
||||
activeFocusOnTab: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: searchButton
|
||||
visible: !root.room.isSpace
|
||||
icon.name: "search"
|
||||
text: i18n("Search in this room")
|
||||
activeFocusOnTab: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
onClicked: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
|
||||
room: root.room
|
||||
}, {
|
||||
title: i18nc("@action:title", "Search")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
visible: root.room.isDirectChat()
|
||||
icon.name: "security-low-symbolic"
|
||||
text: i18nc("@action:button", "Verify user")
|
||||
|
||||
onClicked: root.room.startVerification()
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: favouriteButton
|
||||
visible: !root.room.isSpace
|
||||
icon.name: root.room && root.room.isFavourite ? "rating" : "rating-unrated"
|
||||
text: root.room && root.room.isFavourite ? i18n("Remove room from favorites") : i18n("Favorite this room")
|
||||
|
||||
onClicked: root.room.isFavourite ? root.room.removeTag("m.favourite") : root.room.addTag("m.favourite", 1.0)
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: locationsButton
|
||||
visible: !root.room.isSpace
|
||||
icon.name: "map-flat"
|
||||
text: i18n("Show locations for this room")
|
||||
activeFocusOnTab: true
|
||||
|
||||
onClicked: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'LocationsPage'), {
|
||||
room: root.room
|
||||
}, {
|
||||
title: i18nc("Locations on a map", "Locations")
|
||||
})
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: pinnedMessagesButton
|
||||
visible: !root.room.isSpace
|
||||
icon.name: "pin-symbolic"
|
||||
text: i18nc("@action:button", "Pinned messages")
|
||||
activeFocusOnTab: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
onClicked: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
|
||||
room: root.room
|
||||
}, {
|
||||
title: i18nc("@title", "Pinned Messages")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: leaveButton
|
||||
icon.name: "arrow-left-symbolic"
|
||||
text: root.room.isSpace ? i18nc("@action:button", "Leave this space") : i18nc("@action:button", "Leave this room")
|
||||
activeFocusOnTab: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
onClicked: {
|
||||
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.ListSectionHeader {
|
||||
label: i18n("Members")
|
||||
activeFocusOnTab: false
|
||||
spacing: 0
|
||||
visible: !root.room.isDirectChat()
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: root.room.canSendState("invite")
|
||||
icon.name: "list-add-user"
|
||||
|
||||
onClicked: {
|
||||
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'InviteUserPage'), {
|
||||
room: root.room
|
||||
}, {
|
||||
title: i18nc("@title", "Invite a User")
|
||||
});
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: i18n("Invite user to room")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: root.room ? i18np("%1 member", "%1 members", root.room.joinedCount) : i18n("No member count")
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: userListSearchField
|
||||
|
||||
visible: !root.room.isDirectChat()
|
||||
onVisibleChanged: if (visible) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
|
||||
focusSequence: "Ctrl+Shift+F"
|
||||
|
||||
onAccepted: userFilterModel.filterText = text
|
||||
}
|
||||
}
|
||||
|
||||
model: root.room.isDirectChat() ? 0 : userFilterModel
|
||||
|
||||
UserFilterModel {
|
||||
id: userFilterModel
|
||||
sourceModel: RoomManager.userListModel
|
||||
allowEmpty: true
|
||||
}
|
||||
|
||||
clip: true
|
||||
focus: true
|
||||
|
||||
section.property: "powerLevelString"
|
||||
section.delegate: Kirigami.ListSectionHeader {
|
||||
required property string section
|
||||
|
||||
width: ListView.view.width
|
||||
text: section
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: userDelegate
|
||||
|
||||
required property int index
|
||||
required property string name
|
||||
required property string userId
|
||||
required property url avatar
|
||||
required property int powerLevel
|
||||
required property string powerLevelString
|
||||
|
||||
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: name
|
||||
|
||||
KeyNavigation.tab: navigationBar.tabGroup.checkedButton
|
||||
KeyNavigation.backtab: index === 0 ? userList.headerItem.userListSearchField : null
|
||||
|
||||
onClicked: {
|
||||
RoomManager.resolveResource(userDelegate.userId, "mention");
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
KirigamiComponents.Avatar {
|
||||
implicitWidth: height
|
||||
sourceSize {
|
||||
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||
}
|
||||
source: userDelegate.avatar
|
||||
name: userDelegate.userId
|
||||
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: userDelegate.name
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: groupChatDrawerHeader
|
||||
GroupChatDrawerHeader {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: directChatDrawerHeader
|
||||
DirectChatDrawerHeader {
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
onRoomChanged: {
|
||||
if (userList.headerItem) {
|
||||
userList.headerItem.userListSearchField.text = "";
|
||||
}
|
||||
userList.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
69
src/app/qml/RoomMedia.qml
Normal file
69
src/app/qml/RoomMedia.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.timeline
|
||||
|
||||
/**
|
||||
* @brief Component for visualising the loaded media items in the room.
|
||||
*
|
||||
* The component is a simple list of media delegates (videos or images) with the
|
||||
* ability to open them in the mamimize component.
|
||||
*
|
||||
* @note This component is only the contents, it will need to be placed in either
|
||||
* a drawer (desktop) or page (mobile) to be used.
|
||||
*
|
||||
* @sa RoomDrawer, RoomDrawerPage
|
||||
*/
|
||||
QQC2.ScrollView {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The title that should be displayed for this component if available.
|
||||
*/
|
||||
readonly property string title: i18nc("@action:title", "Room Media")
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property NeoChatRoom currentRoom
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
clip: true
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
model: RoomManager.mediaMessageFilterModel
|
||||
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MediaMessageFilterModel.Image
|
||||
delegate: MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.currentRoom
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MediaMessageFilterModel.Video
|
||||
delegate: MessageDelegate {
|
||||
alwaysFillWidth: true
|
||||
cardBackground: false
|
||||
room: root.currentRoom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
315
src/app/qml/RoomPage.qml
Normal file
315
src/app/qml/RoomPage.qml
Normal file
@@ -0,0 +1,315 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kitemmodels
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.chatbar
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
/// Not readonly because of the separate window view.
|
||||
property NeoChatRoom currentRoom: RoomManager.currentRoom
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief The TimelineModel to use.
|
||||
*
|
||||
* Required so that new events can be requested when the end of the current
|
||||
* local timeline is reached.
|
||||
*
|
||||
* @note For loading a room in a different window, override this with a new
|
||||
* TimelineModel set with the room to be shown.
|
||||
*
|
||||
* @sa TimelineModel
|
||||
*/
|
||||
property TimelineModel timelineModel: RoomManager.timelineModel
|
||||
|
||||
/**
|
||||
* @brief The MessageFilterModel to use.
|
||||
*
|
||||
* This model has the filtered list of events that should be shown in the timeline.
|
||||
*
|
||||
* @note For loading a room in a different window, override this with a new
|
||||
* MessageFilterModel with the new TimelineModel as the source model.
|
||||
*
|
||||
* @sa TimelineModel, MessageFilterModel
|
||||
*/
|
||||
property MessageFilterModel messageFilterModel: RoomManager.messageFilterModel
|
||||
|
||||
/**
|
||||
* @brief The MediaMessageFilterModel to use.
|
||||
*
|
||||
* This model has the filtered list of media events that should be shown in
|
||||
* the timeline.
|
||||
*
|
||||
* @note For loading a room in a different window, override this with a new
|
||||
* MediaMessageFilterModel with the new MessageFilterModel as the source model.
|
||||
*
|
||||
* @sa TimelineModel, MessageFilterModel
|
||||
*/
|
||||
property MediaMessageFilterModel mediaMessageFilterModel: RoomManager.mediaMessageFilterModel
|
||||
|
||||
property bool loading: !root.currentRoom || (root.currentRoom.timelineSize === 0 && !root.currentRoom.allHistoryLoaded)
|
||||
|
||||
/// Disable cancel shortcut. Used by the separate window since it provides its own cancel implementation.
|
||||
property bool disableCancelShortcut: false
|
||||
|
||||
title: root.currentRoom ? root.currentRoom.displayName : ""
|
||||
focus: true
|
||||
padding: 0
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
visible: Kirigami.Settings.isMobile || !applicationWindow().pageStack.wideMode
|
||||
icon.name: "view-right-new"
|
||||
onTriggered: applicationWindow().openRoomDrawer()
|
||||
}
|
||||
]
|
||||
|
||||
KeyNavigation.left: pageStack.get(0)
|
||||
|
||||
onCurrentRoomChanged: {
|
||||
banner.visible = false;
|
||||
if (!Kirigami.Settings.isMobile && chatBarLoader.item) {
|
||||
chatBarLoader.item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.connection
|
||||
function onIsOnlineChanged() {
|
||||
if (!root.connection.isOnline) {
|
||||
banner.text = i18n("NeoChat is offline. Please check your network connection.");
|
||||
banner.visible = true;
|
||||
banner.type = Kirigami.MessageType.Error;
|
||||
} else {
|
||||
banner.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: Kirigami.InlineMessage {
|
||||
id: banner
|
||||
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: timelineViewLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentRoom && !root.currentRoom.isInvite && !root.loading && !root.currentRoom.isSpace
|
||||
sourceComponent: TimelineView {
|
||||
id: timelineView
|
||||
currentRoom: root.currentRoom
|
||||
page: root
|
||||
timelineModel: root.timelineModel
|
||||
messageFilterModel: root.messageFilterModel
|
||||
onFocusChatBar: {
|
||||
if (chatBarLoader.item) {
|
||||
chatBarLoader.item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: invitationLoader
|
||||
active: root.currentRoom && root.currentRoom.isInvite
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: InvitationView {
|
||||
currentRoom: root.currentRoom
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: spaceLoader
|
||||
active: root.currentRoom && root.currentRoom.isSpace
|
||||
anchors.fill: parent
|
||||
sourceComponent: SpaceHomePage {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !RoomManager.currentRoom
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: Kirigami.PlaceholderMessage {
|
||||
icon.name: "org.kde.neochat"
|
||||
text: i18n("Welcome to NeoChat")
|
||||
explanation: i18n("Select or join a room to get started")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.loading && !invitationLoader.active && RoomManager.currentRoom && !spaceLoader.active
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: Kirigami.LoadingPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
color: NeoChatConfig.compactLayout ? Kirigami.Theme.backgroundColor : "transparent"
|
||||
}
|
||||
|
||||
footer: Loader {
|
||||
id: chatBarLoader
|
||||
height: active ? item.implicitHeight : 0
|
||||
active: timelineViewLoader.active && !root.currentRoom.readOnly
|
||||
sourceComponent: ChatBar {
|
||||
id: chatBar
|
||||
width: parent.width
|
||||
currentRoom: root.currentRoom
|
||||
connection: root.connection
|
||||
onMessageSent: {
|
||||
if (!timelineViewLoader.item.atYEnd) {
|
||||
timelineViewLoader.item.goToLastMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
function onCurrentRoomChanged() {
|
||||
if (root.currentRoom && root.currentRoom.isInvite) {
|
||||
Controller.clearInvitationNotification(root.currentRoom.id);
|
||||
}
|
||||
}
|
||||
|
||||
function onGoToEvent(eventId) {
|
||||
(timelineViewLoader.item as TimelineView).goToEvent(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: {
|
||||
if (!timelineViewLoader.item.atYEnd || !root.currentRoom.partiallyReadStats.empty()) {
|
||||
timelineViewLoader.item.goToLastMessage();
|
||||
root.currentRoom.markAllMessagesAsRead();
|
||||
} else {
|
||||
applicationWindow().pageStack.get(0).forceActiveFocus();
|
||||
}
|
||||
}
|
||||
enabled: !root.disableCancelShortcut
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.connection
|
||||
function onJoinedRoom(room, invited) {
|
||||
if (root.currentRoom.id === invited.id) {
|
||||
RoomManager.resolveResource(room.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_PageUp) {
|
||||
event.accepted = true;
|
||||
timelineViewLoader.item.pageUp();
|
||||
} else if (event.key === Qt.Key_PageDown) {
|
||||
event.accepted = true;
|
||||
timelineViewLoader.item.pageDown();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
|
||||
function onShowMessage(messageType, message) {
|
||||
banner.text = message;
|
||||
banner.type = messageType;
|
||||
banner.visible = true;
|
||||
}
|
||||
|
||||
function onShowEventSource(eventId) {
|
||||
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||
sourceText: root.currentRoom.getEventJsonSource(eventId)
|
||||
}, {
|
||||
title: i18n("Message Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
}
|
||||
|
||||
function onShowMessageMenu(eventId, author, messageComponentType, plainText, htmlText, selectedText, hoveredLink, isThread) {
|
||||
const contextMenu = messageDelegateContextMenu.createObject(root, {
|
||||
selectedText: selectedText,
|
||||
hoveredLink: hoveredLink,
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
messageComponentType: messageComponentType,
|
||||
plainText: plainText,
|
||||
htmlText: htmlText,
|
||||
});
|
||||
contextMenu.popup();
|
||||
}
|
||||
|
||||
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo, isThread) {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(root, {
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
plainText: plainText,
|
||||
mimeType: mimeType,
|
||||
progressInfo: progressInfo,
|
||||
});
|
||||
contextMenu.popup();
|
||||
}
|
||||
|
||||
function onShowMaximizedMedia(index) {
|
||||
var popup = maximizeComponent.createObject(QQC2.Overlay.overlay, {
|
||||
initialIndex: index
|
||||
});
|
||||
popup.closed.connect(() => {
|
||||
timelineViewLoader.item.interactive = true;
|
||||
popup.destroy();
|
||||
});
|
||||
popup.open();
|
||||
}
|
||||
|
||||
function onShowMaximizedCode(author, time, codeText, language) {
|
||||
let popup = Qt.createComponent('org.kde.neochat', 'CodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||
author: author,
|
||||
time: time,
|
||||
codeText: codeText,
|
||||
language: language
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: messageDelegateContextMenu
|
||||
MessageDelegateContextMenu {
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDelegateContextMenu
|
||||
FileDelegateContextMenu {
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: maximizeComponent
|
||||
NeochatMaximizeComponent {
|
||||
currentRoom: root.currentRoom
|
||||
model: root.mediaMessageFilterModel
|
||||
parent: root.QQC2.Overlay.overlay
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/app/qml/RoomPinnedMessagesPage.qml
Normal file
64
src/app/qml/RoomPinnedMessagesPage.qml
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.timeline
|
||||
|
||||
/**
|
||||
* @brief Component for showing the pinned messages in a room.
|
||||
*/
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The room to show the pinned messages for.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
title: i18nc("@title", "Pinned Messages")
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
spacing: 0
|
||||
|
||||
model: PinnedMessageModel {
|
||||
id: pinModel
|
||||
room: root.room
|
||||
}
|
||||
|
||||
delegate: EventDelegate {
|
||||
room: root.room
|
||||
}
|
||||
|
||||
section.property: "section"
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
icon.name: "pin-symbolic"
|
||||
anchors.centerIn: parent
|
||||
text: i18nc("@info:placeholder", "No Pinned Messages")
|
||||
visible: listView.count === 0
|
||||
}
|
||||
|
||||
Kirigami.LoadingPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
visible: listView.count === 0 && pinModel.loading
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
if (listView.currentIndex > 0) {
|
||||
listView.decrementCurrentIndex();
|
||||
} else {
|
||||
listView.currentIndex = -1; // This is so the list view doesn't appear to have two selected items
|
||||
listView.headerItem.forceActiveFocus(Qt.TabFocusReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/app/qml/RoomSearchPage.qml
Normal file
42
src/app/qml/RoomSearchPage.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.timeline
|
||||
|
||||
/**
|
||||
* @brief Component for finding messages in a room.
|
||||
*
|
||||
* This component is based on a SearchPage and allows the user to enter a search
|
||||
* term into the input field and then search the room for messages with text that
|
||||
* matches the input.
|
||||
*
|
||||
* @sa SearchPage
|
||||
*/
|
||||
SearchPage {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The room the search is being performed in.
|
||||
*/
|
||||
required property NeoChatRoom room
|
||||
|
||||
title: i18nc("@action:title", "Search Messages")
|
||||
|
||||
model: SearchModel {
|
||||
id: searchModel
|
||||
room: root.room
|
||||
}
|
||||
|
||||
modelDelegate: EventDelegate {
|
||||
room: root.room
|
||||
}
|
||||
|
||||
searchFieldPlaceholder: i18n("Find messages…")
|
||||
noSearchPlaceholderMessage: i18n("Enter text to start searching")
|
||||
noResultPlaceholderMessage: i18n("No messages found")
|
||||
|
||||
listVerticalLayoutDirection: ListView.BottomToTop
|
||||
}
|
||||
211
src/app/qml/SearchPage.qml
Normal file
211
src/app/qml/SearchPage.qml
Normal file
@@ -0,0 +1,211 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief Component for a generic search page.
|
||||
*
|
||||
* This component provides a header with the search field and a ListView to visualise
|
||||
* search results from the given model.
|
||||
*/
|
||||
Kirigami.ScrollablePage {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief Any additional controls after the search button.
|
||||
*/
|
||||
property alias headerTrailing: headerContent.children
|
||||
|
||||
/**
|
||||
* @brief The model that provides the search results.
|
||||
*
|
||||
* The model needs to provide the following properties:
|
||||
* - searchText
|
||||
* - searching
|
||||
* Where searchText is the text from the searchField and is used to match results
|
||||
* and searching is true while the model is finding results.
|
||||
*
|
||||
* The model must also provide a search() function to start the search if
|
||||
* it doesn't do so when the searchText is changed.
|
||||
*/
|
||||
property alias model: listView.model
|
||||
|
||||
/**
|
||||
* @brief The number of delegates currently in the view.
|
||||
*/
|
||||
property alias count: listView.count
|
||||
|
||||
/**
|
||||
* @brief The delegate to use to visualize the model data.
|
||||
*/
|
||||
property alias modelDelegate: listView.delegate
|
||||
|
||||
/**
|
||||
* @brief The delegate to appear as the header of the list.
|
||||
*/
|
||||
property alias listHeaderDelegate: listView.header
|
||||
|
||||
/**
|
||||
* @brief The delegate to appear as the footer of the list.
|
||||
*/
|
||||
property alias listFooterDelegate: listView.footer
|
||||
|
||||
/**
|
||||
* @brief The placeholder text in the search field.
|
||||
*/
|
||||
property alias searchFieldPlaceholder: searchField.placeholderText
|
||||
|
||||
/**
|
||||
* @brief The text to show when no search term has been entered.
|
||||
*/
|
||||
property alias noSearchPlaceholderMessage: noSearchMessage.text
|
||||
|
||||
/**
|
||||
* @brief The text to show when no results have been found.
|
||||
*/
|
||||
property alias noResultPlaceholderMessage: noResultMessage.text
|
||||
|
||||
/**
|
||||
* @brief The verticalLayoutDirection property of the internal ListView.
|
||||
*/
|
||||
property alias listVerticalLayoutDirection: listView.verticalLayoutDirection
|
||||
|
||||
/**
|
||||
* @brief Set the visibility of the search button.
|
||||
*/
|
||||
property bool showSearchButton: true
|
||||
|
||||
/**
|
||||
* @brief Message to be shown in a custom placeholder.
|
||||
* The custom placeholder will be shown if the text is not empty
|
||||
*/
|
||||
property alias customPlaceholderText: customPlaceholder.text
|
||||
|
||||
/**
|
||||
* @brief icon for the custom placeholder
|
||||
*/
|
||||
property string customPlaceholderIcon: ""
|
||||
|
||||
/**
|
||||
* @brief Force the search field to be focussed.
|
||||
*/
|
||||
function focusSearch() {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Force the search to be updated if the model has a valid search function.
|
||||
*/
|
||||
function updateSearch() {
|
||||
searchTimer.restart();
|
||||
}
|
||||
|
||||
header: QQC2.Control {
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: headerContent
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: searchField
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
Keys.onEnterPressed: searchButton.clicked()
|
||||
Keys.onReturnPressed: searchButton.clicked()
|
||||
onTextChanged: {
|
||||
searchTimer.restart();
|
||||
if (model) {
|
||||
model.searchText = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: searchButton
|
||||
icon.name: "search"
|
||||
display: QQC2.Button.IconOnly
|
||||
visible: root.showSearchButton
|
||||
text: i18nc("@action:button", "Search")
|
||||
|
||||
onClicked: {
|
||||
if (typeof model.search === 'function') {
|
||||
model.search();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
Timer {
|
||||
id: searchTimer
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: if (typeof model.search === 'function') {
|
||||
model.search();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
section.property: "section"
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: noSearchMessage
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length === 0 && listView.count === 0 && customPlaceholder.text.length === 0
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: noResultMessage
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: customPlaceholder
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && text.length > 0
|
||||
icon.name: root.customPlaceholderIcon
|
||||
}
|
||||
|
||||
Kirigami.LoadingPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching && customPlaceholder.text.length === 0
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
if (listView.currentIndex > 0) {
|
||||
listView.decrementCurrentIndex();
|
||||
} else {
|
||||
listView.currentIndex = -1; // This is so the list view doesn't appear to have two selected items
|
||||
listView.headerItem.forceActiveFocus(Qt.TabFocusReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/app/qml/ServerComboBox.qml
Normal file
197
src/app/qml/ServerComboBox.qml
Normal file
@@ -0,0 +1,197 @@
|
||||
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current local user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
/**
|
||||
* @brief The server to get the search results from.
|
||||
*/
|
||||
property string server
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
|
||||
Component.onCompleted: currentIndex = 0
|
||||
|
||||
textRole: "url"
|
||||
valueRole: "url"
|
||||
model: ServerListModel {
|
||||
id: serverListModel
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: serverItem
|
||||
|
||||
required property int index
|
||||
required property string url
|
||||
required property bool isAddServerDelegate
|
||||
required property bool isHomeServer
|
||||
required property bool isDeletable
|
||||
|
||||
text: isAddServerDelegate ? i18n("Add New Server") : url
|
||||
highlighted: index === root.highlightedIndex
|
||||
|
||||
topInset: index === 0 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
bottomInset: index === ListView.view.count - 1 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
|
||||
onClicked: if (isAddServerDelegate) {
|
||||
addServerSheet.parent = QQC2.Overlay.overlay
|
||||
addServerSheet.open();
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: serverItem
|
||||
subtitle: serverItem.isHomeServer ? i18n("Home Server") : ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: serverItem.isAddServerDelegate || serverItem.isDeletable
|
||||
icon.name: serverItem.isAddServerDelegate ? "list-add" : "dialog-close"
|
||||
text: i18nc("@action:button", "Add new server")
|
||||
Accessible.name: text
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
if (root.currentIndex === serverItem.index && serverItem.isDeletable) {
|
||||
root.currentIndex = 0;
|
||||
root.server = root.currentValue;
|
||||
root.popup.close();
|
||||
}
|
||||
if (serverItem.isAddServerDelegate) {
|
||||
addServerSheet.parent = QQC2.Overlay.overlay
|
||||
addServerSheet.open();
|
||||
serverItem.clicked();
|
||||
} else {
|
||||
serverListModel.removeServerAtIndex(serverItem.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
if (currentIndex !== count - 1) {
|
||||
root.server = root.currentValue;
|
||||
} else {
|
||||
// Make sure to reset the combobox as it will display nothing if the "Add Server" item was selected.
|
||||
root.currentIndex = 0;
|
||||
root.server = root.currentValue;
|
||||
|
||||
addServerSheet.parent = QQC2.Overlay.overlay
|
||||
addServerSheet.open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: addServerSheet
|
||||
|
||||
width: Math.min(Kirigami.Units.gridUnit * 15, QQC2.ApplicationWindow.window.width)
|
||||
|
||||
title: i18nc("@title:window", "Add server")
|
||||
|
||||
horizontalPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
onOpened: if (!serverUrlField.isValidServer && !addServerSheet.opened) {
|
||||
root.currentIndex = 0;
|
||||
root.server = root.currentValue;
|
||||
} else if (addServerSheet.opened) {
|
||||
serverUrlField.forceActiveFocus();
|
||||
}
|
||||
|
||||
onClosed: if (serverUrlField.length <= 0) {
|
||||
root.currentIndex = root.indexOfValue(root.server);
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: text != ""
|
||||
|
||||
text: {
|
||||
if (serverUrlField.length > 0) {
|
||||
if (!serverUrlField.acceptableInput) {
|
||||
return i18n("The entered text is not a valid url");
|
||||
}
|
||||
|
||||
if (!serverUrlField.isValidServer) {
|
||||
return i18n("This server cannot be resolved or has already been added");
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: i18n("Server URL:")
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
id: serverUrlField
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
placeholderText: "kde.org"
|
||||
|
||||
property bool isValidServer: false
|
||||
|
||||
onTextChanged: {
|
||||
if (acceptableInput) {
|
||||
serverListModel.checkServer(text);
|
||||
}
|
||||
}
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^([^.]+\.)+[^.]+$/
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: serverListModel
|
||||
function onServerCheckComplete(url, valid) {
|
||||
if (url == serverUrlField.text && valid) {
|
||||
serverUrlField.isValidServer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customFooterActions: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Ok")
|
||||
enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer
|
||||
onTriggered: {
|
||||
serverListModel.addServer(serverUrlField.text);
|
||||
root.currentIndex = root.indexOfValue(serverUrlField.text);
|
||||
root.server = root.currentValue;
|
||||
serverUrlField.text = "";
|
||||
addServerSheet.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/app/qml/ShareAction.qml
Normal file
79
src/app/qml/ShareAction.qml
Normal file
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.purpose as Purpose
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* Action that allows a user to share data with other apps and services
|
||||
* installed on their computer. The goal of this high level API is to
|
||||
* adapt itself for each platform and adopt the native component.
|
||||
*
|
||||
* TODO add Android support
|
||||
*/
|
||||
Kirigami.Action {
|
||||
id: root
|
||||
|
||||
icon.name: "emblem-shared-symbolic"
|
||||
text: i18n("Share")
|
||||
tooltip: i18n("Share the selected media")
|
||||
|
||||
visible: false
|
||||
|
||||
/**
|
||||
* This property holds the input data for purpose.
|
||||
*
|
||||
* @code{.qml}
|
||||
* Purpose.ShareAction {
|
||||
* inputData: {
|
||||
* 'urls': ['file://home/notroot/Pictures/mypicture.png'],
|
||||
* 'mimeType': ['image/png']
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
property var inputData
|
||||
|
||||
required property string eventId
|
||||
required property NeoChatRoom room
|
||||
|
||||
property Instantiator _instantiator: Instantiator {
|
||||
model: Purpose.PurposeAlternativesModel {
|
||||
pluginType: "Export"
|
||||
inputData: root.inputData
|
||||
}
|
||||
|
||||
delegate: Kirigami.Action {
|
||||
property int index
|
||||
text: model.display
|
||||
icon.name: model.iconName
|
||||
onTriggered: {
|
||||
root.room.download(root.eventId, root.inputData.urls[0]);
|
||||
root.room.fileTransferCompleted.connect(share);
|
||||
}
|
||||
function share(id) {
|
||||
if (id != root.eventId) {
|
||||
return;
|
||||
}
|
||||
applicationWindow().pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "ShareDialog"), {
|
||||
title: root.text,
|
||||
index: index,
|
||||
model: root._instantiator.model
|
||||
}, {
|
||||
title: i18nc("@title", "Share")
|
||||
});
|
||||
root.room.fileTransferCompleted.disconnect(share);
|
||||
}
|
||||
}
|
||||
onObjectAdded: (index, object) => {
|
||||
object.index = index;
|
||||
root.children.push(object);
|
||||
}
|
||||
onObjectRemoved: (index, object) => root.children = Array.from(root.children).filter(obj => obj.pluginId !== object.pluginId)
|
||||
}
|
||||
}
|
||||
11
src/app/qml/ShareActionStub.qml
Normal file
11
src/app/qml/ShareActionStub.qml
Normal file
@@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.Action {
|
||||
property var inputData: ({})
|
||||
property var room
|
||||
property string eventId
|
||||
visible: false
|
||||
}
|
||||
73
src/app/qml/ShareDialog.qml
Normal file
73
src/app/qml/ShareDialog.qml
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2017 Atul Sharma <atulsharma406@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.purpose as Purpose
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.notification
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
property alias index: jobView.index
|
||||
property alias model: jobView.model
|
||||
|
||||
QQC2.Action {
|
||||
shortcut: 'Escape'
|
||||
onTriggered: root.closeDialog()
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: sharingFailed
|
||||
eventId: "Share"
|
||||
text: i18n("Sharing failed")
|
||||
urgency: Notification.NormalUrgency
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: sharingSuccess
|
||||
eventId: "Share"
|
||||
flags: Notification.Persistent
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
jobView.start();
|
||||
}
|
||||
|
||||
Purpose.JobView {
|
||||
id: jobView
|
||||
|
||||
anchors.fill: parent
|
||||
onStateChanged: {
|
||||
if (state === Purpose.PurposeJobController.Finished) {
|
||||
if (jobView.job?.output?.url?.length > 0) {
|
||||
sharingSuccess.text = i18n("Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
|
||||
sharingSuccess.sendEvent();
|
||||
Clipboard.saveText(jobView.job.output.url);
|
||||
}
|
||||
root.closeDialog();
|
||||
} else if (state === Purpose.PurposeJobController.Error) {
|
||||
// Show failure notification
|
||||
sharingFailed.sendEvent();
|
||||
root.closeDialog();
|
||||
} else if (state === Purpose.PurposeJobController.Cancelled) {
|
||||
// Do nothing
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/app/qml/TextToSpeechWrapper.qml
Normal file
24
src/app/qml/TextToSpeechWrapper.qml
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2025 Ritchie Frodomar <alkalinethunder@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtTextToSpeech
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property TextToSpeech tts: TextToSpeech {
|
||||
id: tts
|
||||
}
|
||||
|
||||
function warmUp() {
|
||||
// TODO: This method is called on startup to avoid a UI freeze the first time you read a message aloud, but there's nothing for it to do.
|
||||
// This would be a good place to check if TTS can actually be used.
|
||||
}
|
||||
|
||||
function say(text: String) {
|
||||
tts.say(text)
|
||||
}
|
||||
}
|
||||
122
src/app/qml/TypingPane.qml
Normal file
122
src/app/qml/TypingPane.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
* SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2021 Srevin Saju <srevinsaju@sugarlabs.org>
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
property string labelText: ""
|
||||
|
||||
active: visible
|
||||
sourceComponent: QQC2.Pane {
|
||||
id: typingPane
|
||||
|
||||
leftPadding: Kirigami.Units.largeSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
}
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: typingPane.spacing
|
||||
Row {
|
||||
id: dotRow
|
||||
property int duration: 400
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Repeater {
|
||||
model: 3
|
||||
delegate: Rectangle {
|
||||
id: dot
|
||||
color: Kirigami.Theme.textColor
|
||||
radius: height / 2
|
||||
implicitWidth: fontMetrics.xHeight
|
||||
implicitHeight: fontMetrics.xHeight
|
||||
// rotating 45 degrees makes the dots look a bit smoother when scaled up
|
||||
rotation: 45
|
||||
opacity: 0.5
|
||||
scale: 1
|
||||
// FIXME: Sometimes the animation timings for each
|
||||
// dot drift slightly reletative to each other.
|
||||
// Not everyone can see this, but I'm pretty sure it's there.
|
||||
SequentialAnimation {
|
||||
running: true
|
||||
PauseAnimation {
|
||||
duration: dotRow.duration * index / 2
|
||||
}
|
||||
SequentialAnimation {
|
||||
loops: Animation.Infinite
|
||||
ParallelAnimation {
|
||||
// Animators unfortunately sync up instead of being
|
||||
// staggered, so I'm using NumberAnimations instead.
|
||||
NumberAnimation {
|
||||
target: dot
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 1.33
|
||||
duration: dotRow.duration
|
||||
}
|
||||
NumberAnimation {
|
||||
target: dot
|
||||
property: "opacity"
|
||||
from: 0.5
|
||||
to: 1
|
||||
duration: dotRow.duration
|
||||
}
|
||||
}
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: dot
|
||||
property: "scale"
|
||||
from: 1.33
|
||||
to: 1
|
||||
duration: dotRow.duration
|
||||
}
|
||||
NumberAnimation {
|
||||
target: dot
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0.5
|
||||
duration: dotRow.duration
|
||||
}
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: dotRow.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: typingLabel
|
||||
elide: Text.ElideRight
|
||||
text: root.labelText
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
leftInset: !mirrored ? 0 : -background.radius
|
||||
rightInset: mirrored ? 0 : -background.radius
|
||||
bottomInset: -background.radius
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
border.color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/app/qml/UnlockSSSSDialog.qml
Normal file
137
src/app/qml/UnlockSSSSDialog.qml
Normal file
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: root
|
||||
|
||||
title: i18nc("@title:window", "Load your encrypted messages")
|
||||
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
header: Kirigami.InlineMessage {
|
||||
id: banner
|
||||
showCloseButton: true
|
||||
visible: false
|
||||
type: Kirigami.MessageType.Error
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
}
|
||||
|
||||
property SSSSHandler ssssHandler: SSSSHandler {
|
||||
id: ssssHandler
|
||||
|
||||
property bool processing: false
|
||||
|
||||
connection: Controller.activeConnection
|
||||
onKeyBackupUnlocked: {
|
||||
ssssHandler.processing = false
|
||||
banner.text = i18nc("@info:status", "Encryption keys restored.")
|
||||
banner.type = Kirigami.MessageType.Positive
|
||||
banner.visible = true
|
||||
}
|
||||
onError: error => {
|
||||
if (error !== SSSSHandler.WrongKeyError) {
|
||||
banner.text = error
|
||||
banner.visible = true
|
||||
return;
|
||||
}
|
||||
passwordField.clear()
|
||||
ssssHandler.processing = false
|
||||
banner.text = i18nc("@info:status", "The security phrase was not correct.")
|
||||
banner.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Unlock using Passphrase")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
description: i18nc("@info", "If you have a backup passphrase for this account, enter it below.")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: passwordField
|
||||
label: i18nc("@label:textbox", "Backup Password:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: unlockButton
|
||||
text: i18nc("@action:button", "Unlock")
|
||||
icon.name: "unlock"
|
||||
enabled: passwordField.text.length > 0 && !ssssHandler.processing
|
||||
onClicked: {
|
||||
ssssHandler.processing = true
|
||||
banner.visible = false
|
||||
ssssHandler.unlockSSSSWithPassphrase(passwordField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Unlock using Security Key")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
description: i18nc("@info", "If you have a security key for this account, enter it below or upload it as a file.")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: securityKeyField
|
||||
label: i18nc("@label:textbox", "Security Key:")
|
||||
echoMode: TextInput.Password
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: uploadSecurityKeyButton
|
||||
text: i18nc("@action:button", "Upload from File")
|
||||
icon.name: "cloud-upload"
|
||||
enabled: !ssssHandler.processing
|
||||
onClicked: {
|
||||
ssssHandler.processing = true
|
||||
openFileDialog.open()
|
||||
}
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: unlockSecurityKeyButton
|
||||
text: i18nc("@action:button", "Unlock")
|
||||
icon.name: "unlock"
|
||||
enabled: securityKeyField.text.length > 0 && !ssssHandler.processing
|
||||
onClicked: {
|
||||
ssssHandler.processing = true
|
||||
ssssHandler.unlockSSSSFromSecurityKey(securityKeyField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Unlock from Cross-Signing")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
description: i18nc("@info", "If you have previously verified this device, you can try loading the backup key from other devices by clicking the button below.")
|
||||
}
|
||||
FormCard.FormButtonDelegate {
|
||||
id: unlockCrossSigningButton
|
||||
icon.name: "emblem-shared-symbolic"
|
||||
text: i18nc("@action:button", "Request from other Devices")
|
||||
enabled: !ssssHandler.processing
|
||||
onClicked: {
|
||||
ssssHandler.processing = true
|
||||
ssssHandler.unlockSSSSFromCrossSigning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property OpenFileDialog openFileDialog: OpenFileDialog {
|
||||
id: openFileDialog
|
||||
onChosen: securityKeyField.text = Controller.loadFileContent(path)
|
||||
}
|
||||
}
|
||||
293
src/app/qml/UserDetailDialog.qml
Normal file
293
src/app/qml/UserDetailDialog.qml
Normal file
@@ -0,0 +1,293 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.prison
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
// This dialog is sometimes used outside the context of a room, e.g., when scanning a user's QR code.
|
||||
// Make sure that code is prepared to deal with this property being null
|
||||
property NeoChatRoom room
|
||||
property var user
|
||||
|
||||
property NeoChatConnection connection
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title:menu Account details dialog", "Account Details")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
|
||||
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||
source: root.room ? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
|
||||
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 1
|
||||
Layout.fillWidth: true
|
||||
font.bold: true
|
||||
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
text: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
|
||||
Kirigami.SelectableLabel {
|
||||
textFormat: TextEdit.PlainText
|
||||
text: root.user.id
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
property CommonRoomsModel model: CommonRoomsModel {
|
||||
connection: root.connection
|
||||
userId: root.user.id
|
||||
}
|
||||
|
||||
text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
visible: model.count > 0
|
||||
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
QQC2.AbstractButton {
|
||||
Layout.minimumHeight: avatar.height * 0.75
|
||||
Layout.maximumHeight: avatar.height * 1.5
|
||||
Layout.maximumWidth: avatar.height * 1.5
|
||||
|
||||
contentItem: Barcode {
|
||||
id: barcode
|
||||
barcodeType: Barcode.QRCode
|
||||
content: "https://matrix.to/#/" + root.user.id
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
let map = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||
text: barcode.content,
|
||||
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
|
||||
subtitle: root.user.id,
|
||||
avatarColor: root.room?.member(root.user.id).color,
|
||||
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
|
||||
});
|
||||
root.close();
|
||||
map.open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: barcode.content
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Chip {
|
||||
visible: root.room
|
||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||
closable: false
|
||||
checkable: false
|
||||
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.user.id !== root.connection.localUserId && !!root.user
|
||||
action: Kirigami.Action {
|
||||
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
|
||||
icon.name: "im-invisible-user"
|
||||
onTriggered: {
|
||||
root.close();
|
||||
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("kick") && room.containsUser(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Kick this user")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.kickMember(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("invite") && !room.containsUser(root.user.id)
|
||||
|
||||
action: Kirigami.Action {
|
||||
enabled: root.room && !root.room.isUserBanned(root.user.id)
|
||||
text: i18n("Invite this user")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: {
|
||||
root.room.inviteToRoom(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && !room.isUserBanned(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Ban this user")
|
||||
icon.name: "im-ban-user"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.ban(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && room.isUserBanned(root.user.id)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Unban this user")
|
||||
icon.name: "im-irc"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
root.room.unban(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.room.canSendState("m.room.power_levels")
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Set user power level")
|
||||
icon.name: "visibility"
|
||||
onTriggered: {
|
||||
let dialog = powerLevelDialog.createObject(this, {
|
||||
room: root.room,
|
||||
userId: root.user.id,
|
||||
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
|
||||
});
|
||||
dialog.open();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: powerLevelDialog
|
||||
PowerLevelDialog {
|
||||
id: powerLevelDialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact"))
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Remove recent messages by this user")
|
||||
icon.name: "delete"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
let dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete"
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.deleteMessagesByUser(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.user.id !== root.connection.localUserId
|
||||
action: Kirigami.Action {
|
||||
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.connection.requestDirectChat(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Copy link")
|
||||
icon.name: "username-copy"
|
||||
onTriggered: {
|
||||
Clipboard.saveText("https://matrix.to/#/" + root.user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/app/qml/UserSearchPage.qml
Normal file
113
src/app/qml/UserSearchPage.qml
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Component for finding users from the public list.
|
||||
*
|
||||
* This component is based on a SearchPage and allows the user to enter a search
|
||||
* term into the input field and then search the room for messages with text that
|
||||
* matches the input.
|
||||
*
|
||||
* @sa SearchPage
|
||||
*/
|
||||
SearchPage {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The connection for the current local user.
|
||||
*/
|
||||
required property NeoChatConnection connection
|
||||
|
||||
title: i18nc("@action:title", "Find Your Friends")
|
||||
|
||||
Component.onCompleted: focusSearch()
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userSearchModel
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
listHeaderDelegate: Delegates.RoundedItemDelegate {
|
||||
onClicked: _private.openManualUserDialog()
|
||||
|
||||
activeFocusOnTab: false // We handle moving to this item via up/down arrows, otherwise the tab order is wacky
|
||||
text: i18n("Enter a user ID")
|
||||
icon.name: "list-add-user"
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
}
|
||||
|
||||
modelDelegate: Delegates.RoundedItemDelegate {
|
||||
id: userDelegate
|
||||
required property string userId
|
||||
required property string displayName
|
||||
required property url avatarUrl
|
||||
required property var directChatExists
|
||||
|
||||
text: displayName
|
||||
|
||||
onClicked: {
|
||||
root.connection.requestDirectChat(userDelegate.userId);
|
||||
root.closeDialog();
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Components.Avatar {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
source: userDelegate.avatarUrl
|
||||
name: userDelegate.displayName
|
||||
}
|
||||
Delegates.SubtitleContentItem {
|
||||
itemDelegate: userDelegate
|
||||
subtitle: userDelegate.userId
|
||||
labelItem.textFormat: Text.PlainText
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: userDelegate.directChatExists
|
||||
text: i18n("Friends")
|
||||
textFormat: Text.PlainText
|
||||
color: Kirigami.Theme.positiveTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchFieldPlaceholder: i18n("Find your friends…")
|
||||
noSearchPlaceholderMessage: i18n("Enter text to start searching for your friends")
|
||||
noResultPlaceholderMessage: i18nc("@info:label", "No matches found")
|
||||
|
||||
Component {
|
||||
id: manualUserDialog
|
||||
ManualUserDialog {}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
function openManualUserDialog() {
|
||||
let dialog = manualUserDialog.createObject(this, {
|
||||
connection: root.connection
|
||||
});
|
||||
dialog.parent = root.Window.window.overlay;
|
||||
dialog.accepted.connect(() => {
|
||||
root.closeDialog();
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/app/qml/VerificationCanceled.qml
Normal file
67
src/app/qml/VerificationCanceled.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQml
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
VerificationMessage {
|
||||
id: root
|
||||
|
||||
required property int reason
|
||||
|
||||
icon: "security-low"
|
||||
text: {
|
||||
switch (root.reason) {
|
||||
case KeyVerificationSession.NONE:
|
||||
return i18n("The session verification was canceled for unknown reason.");
|
||||
case KeyVerificationSession.TIMEOUT:
|
||||
return i18n("The session verification timed out.");
|
||||
case KeyVerificationSession.REMOTE_TIMEOUT:
|
||||
return i18n("The session verification timed out for remote party.");
|
||||
case KeyVerificationSession.USER:
|
||||
return i18n("You canceled the session verification.");
|
||||
case KeyVerificationSession.REMOTE_USER:
|
||||
return i18n("The remote party canceled the session verification.");
|
||||
case KeyVerificationSession.UNEXPECTED_MESSAGE:
|
||||
return i18n("The session verification was canceled because we received an unexpected message.");
|
||||
case KeyVerificationSession.REMOTE_UNEXPECTED_MESSAGE:
|
||||
return i18n("The remote party canceled the session verification because it received an unexpected message.");
|
||||
case KeyVerificationSession.UNKNOWN_TRANSACTION:
|
||||
return i18n("The session verification was canceled because it received a message for an unknown session.");
|
||||
case KeyVerificationSession.REMOTE_UNKNOWN_TRANSACTION:
|
||||
return i18n("The remote party canceled the session verification because it received a message for an unknown session.");
|
||||
case KeyVerificationSession.UNKNOWN_METHOD:
|
||||
return i18n("The session verification was canceled because NeoChat is unable to handle this verification method.");
|
||||
case KeyVerificationSession.REMOTE_UNKNOWN_METHOD:
|
||||
return i18n("The remote party canceled the session verification because it is unable to handle this verification method.");
|
||||
case KeyVerificationSession.KEY_MISMATCH:
|
||||
return i18n("The session verification was canceled because the keys are incorrect.");
|
||||
case KeyVerificationSession.REMOTE_KEY_MISMATCH:
|
||||
return i18n("The remote party canceled the session verification because the keys are incorrect.\n\n**Please log out and log back in, your session is broken/corrupt.**");
|
||||
case KeyVerificationSession.USER_MISMATCH:
|
||||
return i18n("The session verification was canceled because it verifies an unexpected user.");
|
||||
case KeyVerificationSession.REMOTE_USER_MISMATCH:
|
||||
return i18n("The remote party canceled the session verification because it verifies an unexpected user.");
|
||||
case KeyVerificationSession.INVALID_MESSAGE:
|
||||
return i18n("The session verification was canceled because we received an invalid message.");
|
||||
case KeyVerificationSession.REMOTE_INVALID_MESSAGE:
|
||||
return i18n("The remote party canceled the session verification because it received an invalid message.");
|
||||
case KeyVerificationSession.SESSION_ACCEPTED:
|
||||
return i18n("The session was accepted on a different device"); //TODO this should not be visible
|
||||
case KeyVerificationSession.REMOTE_SESSION_ACCEPTED:
|
||||
return i18n("The session was accepted on a different device"); //TODO neither should this
|
||||
case KeyVerificationSession.MISMATCHED_COMMITMENT:
|
||||
return i18n("The session verification was canceled because of a mismatched key.");
|
||||
case KeyVerificationSession.REMOTE_MISMATCHED_COMMITMENT:
|
||||
return i18n("The remote party canceled the session verification because of a mismatched key.");
|
||||
case KeyVerificationSession.MISMATCHED_SAS:
|
||||
return i18n("The session verification was canceled because the keys do not match.");
|
||||
case KeyVerificationSession.REMOTE_MISMATCHED_SAS:
|
||||
return i18n("The remote party canceled the session verification because the keys do not match.");
|
||||
default:
|
||||
return i18n("The session verification was canceled due to an unknown error.");
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/app/qml/VerificationMessage.qml
Normal file
38
src/app/qml/VerificationMessage.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string icon
|
||||
required property string text
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
Kirigami.Icon {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.enormous
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.enormous
|
||||
source: root.icon
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
textFormat: Text.MarkdownText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user