Compare commits
1 Commits
work/redst
...
work/tobia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092e092e18 |
548
po/ar/neochat.po
548
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
574
po/az/neochat.po
574
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
548
po/ca/neochat.po
548
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
558
po/cs/neochat.po
558
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
560
po/da/neochat.po
560
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/de/neochat.po
557
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
572
po/el/neochat.po
572
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
573
po/eo/neochat.po
573
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
554
po/es/neochat.po
554
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/eu/neochat.po
557
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
559
po/fi/neochat.po
559
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
561
po/fr/neochat.po
561
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
547
po/ga/neochat.po
547
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
564
po/gl/neochat.po
564
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
548
po/he/neochat.po
548
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
570
po/hi/neochat.po
570
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
564
po/hu/neochat.po
564
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
548
po/ia/neochat.po
548
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
572
po/id/neochat.po
572
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
571
po/ie/neochat.po
571
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
560
po/it/neochat.po
560
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
542
po/ja/neochat.po
542
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
548
po/ka/neochat.po
548
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
560
po/ko/neochat.po
560
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/lt/neochat.po
557
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/lv/neochat.po
557
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
550
po/nl/neochat.po
550
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
570
po/nn/neochat.po
570
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
574
po/pa/neochat.po
574
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
562
po/pl/neochat.po
562
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
572
po/pt/neochat.po
572
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
557
po/ro/neochat.po
557
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
564
po/ru/neochat.po
564
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
570
po/sa/neochat.po
570
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
569
po/sk/neochat.po
569
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
550
po/sl/neochat.po
550
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
557
po/sv/neochat.po
557
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
559
po/ta/neochat.po
559
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
554
po/tr/neochat.po
554
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
548
po/uk/neochat.po
548
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,17 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Open Profile")
|
||||
icon.name: "im-user-symbolic"
|
||||
onTriggered: RoomManager.resolveResource(root.connection.localUserId, "qr") // Use "qr" action to make sure a room isn't passed, see RoomManager::visitUser
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
onTriggered: {
|
||||
(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) : ""
|
||||
}) as QrCodeMaximizeComponent).open();
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
@@ -89,9 +97,9 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
text: i18nc("@action:inmenu Open support dialog", "Support")
|
||||
icon.name: "help-contents-symbolic"
|
||||
onTriggered: {
|
||||
(Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
|
||||
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
|
||||
connection: root.connection,
|
||||
}) as SupportDialog).open();
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
|
||||
property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the event as a neoChatDateTime.
|
||||
* @brief The timestamp of the event as a NeoChatDateTime.
|
||||
*/
|
||||
required property neoChatDateTime dateTime
|
||||
required property NeoChatDateTime dateTime
|
||||
|
||||
/**
|
||||
* @brief The code text to show.
|
||||
@@ -79,7 +79,7 @@ Components.AbstractMaximizeComponent {
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
QQC2.TextArea {
|
||||
id: codeTextEdit
|
||||
id: codeText
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
@@ -100,15 +100,15 @@ Components.AbstractMaximizeComponent {
|
||||
|
||||
SyntaxHighlighter {
|
||||
property string definitionName: Repository.definitionForName(root.language).name
|
||||
textEdit: definitionName == "None" ? null : codeTextEdit
|
||||
textEdit: definitionName == "None" ? null : codeText
|
||||
definition: definitionName
|
||||
}
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
anchors {
|
||||
top: codeTextEdit.top
|
||||
topMargin: codeTextEdit.topPadding + 1
|
||||
left: codeTextEdit.left
|
||||
top: codeText.top
|
||||
topMargin: codeText.topPadding + 1
|
||||
left: codeText.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
Component.onCompleted: setDocument(codeTextEdit.textDocument)
|
||||
Component.onCompleted: setDocument(codeText.textDocument)
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
@@ -150,6 +150,4 @@ Components.AbstractMaximizeComponent {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -8,31 +8,13 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
import Quotient
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
|
||||
subtitle: {
|
||||
if (root.room) {
|
||||
let message = xi18nc("@info Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml)
|
||||
|
||||
// List any possible side-effects the user needs to be made aware of.
|
||||
if (root.room.historyVisibility !== "world_readable" && root.room.historyVisibility !== "shared") {
|
||||
message += xi18nc("@info", "<br><strong>This room's history is limited to when you rejoin the room.</strong>")
|
||||
}
|
||||
|
||||
if (root.room.joinRule === JoinRule.JoinRule.Invite) {
|
||||
message += xi18nc("@info", "<br><strong>This room can only be rejoined with an invite.</strong>");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
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: {
|
||||
@@ -46,7 +28,7 @@ Kirigami.PromptDialog {
|
||||
text: i18nc("@action:button", "Leave Room")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
icon.name: "arrow-left-symbolic"
|
||||
//onClicked: root.room.forget();
|
||||
onClicked: root.room.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,13 @@ ColumnLayout {
|
||||
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()
|
||||
|
||||
@@ -9,6 +9,8 @@ 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
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: (root.currentRoom.connection as NeoChatConnection).canCheckMutualRooms
|
||||
visible: root.currentRoom.connection.canCheckMutualRooms
|
||||
spacing: 0
|
||||
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
@@ -10,6 +8,7 @@ import QtQuick.Window
|
||||
import QtQml
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
@@ -43,8 +45,6 @@ Components.AbstractMaximizeComponent {
|
||||
}
|
||||
]
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
PositionSource {
|
||||
id: positionSource
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
|
||||
let roomPage = root.pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
roomPage.forceActiveFocus();
|
||||
roomPage.backRequested.connect(event => {
|
||||
RoomManager.clearCurrentRoom();
|
||||
@@ -151,6 +151,8 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -176,9 +178,9 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
// Connect to the onClicked function of the RoomDrawer handle button
|
||||
Connections {
|
||||
target: root.contextDrawer.handle.children[0]
|
||||
target: contextDrawer.handle.children[0]
|
||||
function onClicked() {
|
||||
root.contextDrawer.drawerUserState = root.contextDrawer.drawerOpen;
|
||||
contextDrawer.drawerUserState = contextDrawer.drawerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -18,7 +16,7 @@ Kirigami.PromptDialog {
|
||||
|
||||
customFooterActions: Kirigami.Action {
|
||||
icon.name: "camera-video-symbolic"
|
||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
onTriggered: root.accept()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ Kirigami.Dialog {
|
||||
id: optionModel
|
||||
|
||||
readonly property bool allValuesSet: {
|
||||
for (let i = 0; i < optionModel.rowCount(); i++) {
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
if (optionModel.get(i).optionText.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ Kirigami.Dialog {
|
||||
|
||||
function values() {
|
||||
let textValues = []
|
||||
for(let i = 0; i < optionModel.rowCount(); i++) {
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
textValues.push(optionModel.get(i).optionText);
|
||||
}
|
||||
return textValues;
|
||||
|
||||
@@ -18,7 +18,7 @@ Kirigami.Page {
|
||||
required property NeoChatConnection connection
|
||||
padding: 0
|
||||
|
||||
Component.onCompleted: session.camera.start()
|
||||
Component.onCompleted: camera.start()
|
||||
|
||||
Connections {
|
||||
target: root.QQC2.ApplicationWindow.window
|
||||
@@ -66,8 +66,12 @@ Kirigami.Page {
|
||||
CaptureSession {
|
||||
id: session
|
||||
|
||||
camera: Camera {}
|
||||
imageCapture: ImageCapture {}
|
||||
camera: Camera {
|
||||
id: camera
|
||||
}
|
||||
imageCapture: ImageCapture {
|
||||
id: imageCapture
|
||||
}
|
||||
videoOutput: viewFinder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ Kirigami.Page {
|
||||
type: Kirigami.MessageType.Information
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
|
||||
text: xi18nc("@info", "This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
|
||||
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
|
||||
}
|
||||
|
||||
QQC2.TextArea {
|
||||
|
||||
@@ -80,12 +80,6 @@ Kirigami.Page {
|
||||
onHeightChanged: {
|
||||
// HACK: See TimelineView for the hack details.
|
||||
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
|
||||
resetViewSettling();
|
||||
}
|
||||
|
||||
// Resets the view settling of the timeline.
|
||||
// This should be called whenever the apparent height of the timeline changes, or else the view will scroll on its own!
|
||||
function resetViewSettling(): void {
|
||||
(timelineViewLoader.item as TimelineView).resetViewSettling();
|
||||
}
|
||||
|
||||
@@ -110,8 +104,8 @@ Kirigami.Page {
|
||||
enabled: hasExistingMeeting || canStartNewMeeting
|
||||
visible: root.currentRoom && !root.currentRoom.isSpace
|
||||
onTriggered: {
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting }) as MeetingDialog;
|
||||
dialog.accepted.connect(doAction);
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting });
|
||||
dialog.onAccepted.connect(doAction);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -221,7 +215,7 @@ Kirigami.Page {
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
|
||||
onTapped: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
|
||||
room: root.currentRoom
|
||||
}, {
|
||||
title: i18nc("@title", "Pinned Messages")
|
||||
@@ -235,58 +229,6 @@ Kirigami.Page {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
id: selectedMessagesControl
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
showCloseButton: false
|
||||
visible: root.currentRoom?.selectedMessageCount > 0
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
type: Kirigami.MessageType.Positive
|
||||
icon.name: "edit-select-all-symbolic"
|
||||
|
||||
text: i18nc("@info", "Selected Messages: %1", root.currentRoom?.selectedMessageCount)
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Copy Conversation")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: {
|
||||
Clipboard.saveText(root.currentRoom.getFormattedSelectedMessages())
|
||||
showPassiveNotification(i18nc("@info", "Conversation copied to clipboard"));
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Delete Messages")
|
||||
icon.name: "trash-empty-symbolic"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
enabled: root.currentRoom?.canDeleteSelectedMessages
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing these messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.currentRoom.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
}) as ReasonDialog;
|
||||
dialog.accepted.connect(reason => {
|
||||
root.currentRoom.deleteSelectedMessages(reason);
|
||||
});
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "dialog-close"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: root.currentRoom.clearSelectedMessages()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
id: banner
|
||||
|
||||
@@ -362,9 +304,6 @@ Kirigami.Page {
|
||||
width: parent.width
|
||||
currentRoom: root.currentRoom
|
||||
connection: root.currentRoom.connection as NeoChatConnection
|
||||
|
||||
// Creating a reply (or doing anything in the chat bar) can change the height, but this isn't picked up on the root's onHeightChanged.
|
||||
onHeightChanged: root.resetViewSettling()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
@@ -604,7 +604,6 @@ QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||
void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
{
|
||||
if (m_currentRoom != nullptr) {
|
||||
m_currentRoom->clearSelectedMessages();
|
||||
m_currentRoom->disconnect(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,4 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
EmojiDialog.qml
|
||||
EmojiTonesPicker.qml
|
||||
ImageEditorPage.qml
|
||||
VoiceMessageDialog.qml
|
||||
)
|
||||
|
||||
@@ -150,19 +150,6 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
icon.name: "microphone"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
onTriggered: {
|
||||
let dialog = voiceMessageDialog.createObject(root, {
|
||||
room: root.currentRoom
|
||||
}) as VoiceMessageDialog;
|
||||
dialog.open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: sendAction
|
||||
|
||||
@@ -561,11 +548,6 @@ QQC2.Control {
|
||||
NewPollDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: voiceMessageDialog
|
||||
VoiceMessageDialog {}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: documentHandler
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.coreaddons
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
VoiceRecorder {
|
||||
id: voiceRecorder
|
||||
readonly property bool recording: recorder.recorderState == MediaRecorder.RecordingState
|
||||
room: root.room
|
||||
}
|
||||
|
||||
width: Kirigami.Units.gridUnit * 24
|
||||
|
||||
standardButtons: QQC2.DialogButtonBox.Cancel
|
||||
|
||||
title: i18nc("@title:dialog", "Record Voice Message")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.RoundButton {
|
||||
icon.name: voiceRecorder.recording ? "media-playback-stop" : "media-record"
|
||||
text: voiceRecorder.recording ? i18nc("@action:button Stop audio recording", "Stop Recording") : i18nc("@action:button Start audio recording", "Start Recording")
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 4
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
display: QQC2.RoundButton.IconOnly
|
||||
enabled: voiceRecorder.isSupported
|
||||
|
||||
onClicked: voiceRecorder.recording ? voiceRecorder.stopRecording() : voiceRecorder.startRecording()
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
QQC2.Label {
|
||||
text: i18nc("@info Duration being the length of an audio recording", "Duration: %1", Format.formatDuration(voiceRecorder.recorder.duration))
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("@info", "Voice message recording requires a newer Qt version than is currently installed on this system.")
|
||||
visible: !voiceRecorder.isSupported
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button Send the voice message", "Send")
|
||||
icon.name: "document-send"
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: voiceRecorder.send()
|
||||
enabled: !voiceRecorder.recording && voiceRecorder.recorder.duration > 0 && voiceRecorder.isSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,9 @@ ColumnLayout {
|
||||
model: root.connection.getSupportedRoomVersions()
|
||||
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
id: versionDelegate
|
||||
required property var modelData
|
||||
text: modelData.id
|
||||
contentItem.children: QQC2.Label {
|
||||
text: versionDelegate.modelData.status
|
||||
text: modelData.status
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ target_sources(LibNeoChat PRIVATE
|
||||
texthandler.cpp
|
||||
urlhelper.cpp
|
||||
utils.cpp
|
||||
voicerecorder.cpp
|
||||
enums/chatbartype.h
|
||||
enums/messagecomponenttype.h
|
||||
enums/messagetype.h
|
||||
|
||||
@@ -276,11 +276,6 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
if (role == NotificationCountRole) {
|
||||
return room->notificationCount();
|
||||
}
|
||||
if (role == RoomTypeRole) {
|
||||
if (room->creation()) {
|
||||
return room->creation()->contentPart<QString>("type"_L1);
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
@@ -315,7 +310,6 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
roles[IsChildSpaceRole] = "isChildSpace";
|
||||
roles[IsDirectChat] = "isDirectChat";
|
||||
roles[NotificationCountRole] = "notificationCount";
|
||||
roles[RoomTypeRole] = "roomType";
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ public:
|
||||
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
|
||||
IsDirectChat, /**< Whether this room is a direct chat. */
|
||||
NotificationCountRole, /**< Count of all notifications that also include non-notable events like unread messages. */
|
||||
RoomTypeRole, /**< The room's type. */
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
|
||||
@@ -172,10 +172,6 @@ void UserListModel::refreshAllMembers()
|
||||
{
|
||||
beginResetModel();
|
||||
if (m_currentRoom != nullptr) {
|
||||
// Only sort members when this model needs to be refreshed.
|
||||
if (m_currentRoom->sortedMemberIds().isEmpty()) {
|
||||
m_currentRoom->sortAllMembers();
|
||||
}
|
||||
m_members = m_currentRoom->sortedMemberIds();
|
||||
} else {
|
||||
m_members.clear();
|
||||
|
||||
@@ -27,11 +27,6 @@ QString NeoChatDateTime::shortDateTime() const
|
||||
return QLocale().toString(m_dateTime.toLocalTime(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
QString NeoChatDateTime::longDateTime() const
|
||||
{
|
||||
return QLocale().toString(m_dateTime.toLocalTime(), QLocale::LongFormat);
|
||||
}
|
||||
|
||||
QString NeoChatDateTime::relativeDate() const
|
||||
{
|
||||
KFormat formatter;
|
||||
@@ -44,14 +39,6 @@ QString NeoChatDateTime::relativeDateTime() const
|
||||
return formatter.formatRelativeDateTime(m_dateTime.toLocalTime(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
QString NeoChatDateTime::shortRelativeDateTime() const
|
||||
{
|
||||
if (m_dateTime > QDate::currentDate().startOfDay()) {
|
||||
return hourMinuteString();
|
||||
}
|
||||
return relativeDate() + u", "_s + hourMinuteString();
|
||||
}
|
||||
|
||||
bool NeoChatDateTime::isValid() const
|
||||
{
|
||||
return m_dateTime.isValid();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class NeoChatDateTime
|
||||
{
|
||||
Q_GADGET
|
||||
QML_NAMED_ELEMENT(neoChatDateTime)
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The base QDateTime used to generate the other values.
|
||||
@@ -35,21 +35,16 @@ class NeoChatDateTime
|
||||
*/
|
||||
Q_PROPERTY(QString shortDateTime READ shortDateTime CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The date and time formatted as per QLocale::LongFormat for your locale.
|
||||
*/
|
||||
Q_PROPERTY(QString longDateTime READ longDateTime CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The date formatted as relative to now.
|
||||
*
|
||||
* If the date falls within 2 days or after the current date
|
||||
* If the date falls within one week before or after the current date
|
||||
* then a relative date string will be returned, such as:
|
||||
* - Yesterday
|
||||
* - Today
|
||||
* - Tomorrow
|
||||
* - Two days ago
|
||||
* - In Two Days
|
||||
* - Last Tuesday
|
||||
* - Next Wednesday
|
||||
*
|
||||
* If the date falls outside this period then the format QLocale::ShortFormat
|
||||
* for your locale is used.
|
||||
@@ -59,37 +54,21 @@ class NeoChatDateTime
|
||||
/**
|
||||
* @brief The time and date formatted as relative to now.
|
||||
*
|
||||
* The format is "RelativeDate at hh::mm"
|
||||
* The format is "RelativeDate, hh::mm"
|
||||
*
|
||||
* If the date falls within 2 days before or after the current date
|
||||
* If the date falls within one week before or after the current date
|
||||
* then a relative date string will be returned, such as:
|
||||
* - Yesterday
|
||||
* - Today
|
||||
* - Tomorrow
|
||||
* - Two days ago
|
||||
* - In Two Days
|
||||
* - Last Tuesday
|
||||
* - Next Wednesday
|
||||
*
|
||||
* If the date falls outside this period then the format QLocale::ShortFormat
|
||||
* for your locale is used.
|
||||
*/
|
||||
Q_PROPERTY(QString relativeDateTime READ relativeDateTime CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The time and date formatted as relative to now.
|
||||
*
|
||||
* The format is "RelativeDate, hh::mm"
|
||||
*
|
||||
* If the date falls on the same day as current date, the date is skipped.
|
||||
* If the date falls within 2 days before current date
|
||||
* then a relative date string will be returned, such as:
|
||||
* - Yesterday
|
||||
* - Tomorrow
|
||||
*
|
||||
* If the date falls outside this period then the format QLocale::ShortFormat
|
||||
* for your locale is used.
|
||||
*/
|
||||
Q_PROPERTY(QString shortRelativeDateTime READ shortRelativeDateTime CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Whether this object has a valid date time.
|
||||
*/
|
||||
@@ -102,10 +81,8 @@ public:
|
||||
|
||||
QString hourMinuteString() const;
|
||||
QString shortDateTime() const;
|
||||
QString longDateTime() const;
|
||||
QString relativeDate() const;
|
||||
QString relativeDateTime() const;
|
||||
QString shortRelativeDateTime() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@
|
||||
#include <KJobTrackerInterface>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <ranges>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
std::function<bool(const Quotient::RoomEvent *)> NeoChatRoom::m_hiddenFilter = [](const Quotient::RoomEvent *) -> bool {
|
||||
@@ -175,16 +173,9 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
connect(neochatconnection, &NeoChatConnection::globalUrlPreviewEnabledChanged, this, &NeoChatRoom::urlPreviewEnabledChanged);
|
||||
connect(this, &Room::fullyReadMarkerMoved, this, &NeoChatRoom::invalidateLastUnreadHighlightId);
|
||||
|
||||
// This may look weird, but this is actually for performance.
|
||||
// We only want to listen to new member joining *when* the initial member list was loaded.
|
||||
connect(
|
||||
this,
|
||||
&Room::memberListChanged,
|
||||
this,
|
||||
[this] {
|
||||
connect(this, &Room::memberJoined, this, &NeoChatRoom::insertMemberSorted);
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
// Wait until the initial member list is available before sorting
|
||||
connect(this, &Room::memberListChanged, this, &NeoChatRoom::refreshAllMembers, Qt::SingleShotConnection);
|
||||
connect(this, &Room::memberJoined, this, &NeoChatRoom::insertMemberSorted);
|
||||
}
|
||||
|
||||
bool NeoChatRoom::visible() const
|
||||
@@ -639,14 +630,7 @@ bool NeoChatRoom::isUserBanned(const QString &user) const
|
||||
|
||||
void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reason)
|
||||
{
|
||||
QStringList events;
|
||||
for (const auto &event : messageEvents()) {
|
||||
if (event->senderId() == user && !event->isRedacted() && !event.viewAs<RedactionEvent>() && !event->isStateEvent()) {
|
||||
events += event->id();
|
||||
}
|
||||
}
|
||||
|
||||
doDeleteMessageIds(events, reason);
|
||||
doDeleteMessagesByUser(user, reason);
|
||||
}
|
||||
|
||||
QString NeoChatRoom::historyVisibility() const
|
||||
@@ -777,10 +761,16 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
|
||||
}
|
||||
}
|
||||
|
||||
QCoro::Task<void> NeoChatRoom::doDeleteMessageIds(const QStringList eventIds, QString reason)
|
||||
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
|
||||
{
|
||||
for (const auto &eventId : eventIds) {
|
||||
auto job = connection()->callApi<RedactEventJob>(id(), eventId, connection()->generateTxnId(), reason);
|
||||
QStringList events;
|
||||
for (const auto &event : messageEvents()) {
|
||||
if (event->senderId() == user && !event->isRedacted() && !event.viewAs<RedactionEvent>() && !event->isStateEvent()) {
|
||||
events += event->id();
|
||||
}
|
||||
}
|
||||
for (const auto &e : events) {
|
||||
auto job = connection()->callApi<RedactEventJob>(id(), QString::fromLatin1(QUrl::toPercentEncoding(e)), connection()->generateTxnId(), reason);
|
||||
co_await qCoro(job.get(), &BaseJob::finished);
|
||||
if (job->error() != BaseJob::Success) {
|
||||
qWarning() << "Error: \"" << job->error() << "\" while deleting messages. Aborting";
|
||||
@@ -1924,21 +1914,14 @@ void NeoChatRoom::invalidateLastUnreadHighlightId(const QString &fromEventId, co
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::sortAllMembers()
|
||||
void NeoChatRoom::refreshAllMembers()
|
||||
{
|
||||
m_sortedMemberIds = memberIds();
|
||||
|
||||
// Build up a temporary cache, because we may be checking the same member over and over while sorting.
|
||||
QHash<QString, int> effectivePowerLevels;
|
||||
effectivePowerLevels.reserve(m_sortedMemberIds.size());
|
||||
for (const auto &member : m_sortedMemberIds) {
|
||||
effectivePowerLevels[member] = memberEffectivePowerLevel(member);
|
||||
}
|
||||
|
||||
MemberSorter sorter;
|
||||
std::ranges::sort(m_sortedMemberIds, [&sorter, &effectivePowerLevels](const auto &left, const auto &right) {
|
||||
const auto leftPl = effectivePowerLevels[left];
|
||||
const auto rightPl = effectivePowerLevels[right];
|
||||
std::ranges::sort(m_sortedMemberIds, [this, &sorter](const auto &left, const auto &right) {
|
||||
const auto leftPl = memberEffectivePowerLevel(left);
|
||||
const auto rightPl = memberEffectivePowerLevel(right);
|
||||
if (leftPl > rightPl) {
|
||||
return true;
|
||||
}
|
||||
@@ -1980,96 +1963,4 @@ QList<QString> NeoChatRoom::sortedMemberIds() const
|
||||
return m_sortedMemberIds;
|
||||
}
|
||||
|
||||
int NeoChatRoom::selectedMessageCount() const
|
||||
{
|
||||
return m_selectedMessageIds.size();
|
||||
}
|
||||
|
||||
bool NeoChatRoom::canDeleteSelectedMessages() const
|
||||
{
|
||||
if (canSendState("redact"_L1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString localUserId = connection()->userId();
|
||||
return std::ranges::all_of(m_selectedMessageIds, [this, localUserId](const QString &eventId) {
|
||||
const auto eventIt = findInTimeline(eventId);
|
||||
if (eventIt == historyEdge()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RoomEvent *event = eventIt->event();
|
||||
return event && (event->senderId() == localUserId);
|
||||
});
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isMessageSelected(const QString &eventId) const
|
||||
{
|
||||
return m_selectedMessageIds.contains(eventId);
|
||||
}
|
||||
|
||||
void NeoChatRoom::toggleMessageSelection(const QString &eventId)
|
||||
{
|
||||
if (!m_selectedMessageIds.remove(eventId)) {
|
||||
m_selectedMessageIds.insert(eventId);
|
||||
}
|
||||
|
||||
Q_EMIT selectionChanged();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::getFormattedSelectedMessages() const
|
||||
{
|
||||
QVector<const RoomEvent *> events;
|
||||
events.reserve(m_selectedMessageIds.size());
|
||||
|
||||
std::ranges::copy(m_selectedMessageIds | std::views::transform([this](const QString &eventId) -> const RoomEvent * {
|
||||
const auto eventIt = findInTimeline(eventId);
|
||||
return eventIt != historyEdge() ? eventIt->event() : nullptr;
|
||||
}) | std::views::filter([](const RoomEvent *event) {
|
||||
return event != nullptr;
|
||||
}),
|
||||
std::back_inserter(events));
|
||||
|
||||
std::ranges::sort(events, {}, &RoomEvent::originTimestamp);
|
||||
|
||||
QString formattedContent;
|
||||
formattedContent.reserve(events.size() * 256); // estimate an average of 256 characters per message
|
||||
|
||||
for (const RoomEvent *event : events) {
|
||||
formattedContent += EventHandler::authorDisplayName(this, event);
|
||||
formattedContent += u" — "_s;
|
||||
formattedContent += EventHandler::dateTime(this, event).shortDateTime();
|
||||
formattedContent += u'\n';
|
||||
formattedContent += EventHandler::plainBody(this, event);
|
||||
formattedContent += u"\n\n"_s;
|
||||
}
|
||||
|
||||
return formattedContent.trimmed();
|
||||
}
|
||||
|
||||
void NeoChatRoom::deleteSelectedMessages(const QString &reason)
|
||||
{
|
||||
QStringList events;
|
||||
for (const auto &eventId : m_selectedMessageIds) {
|
||||
const auto eventIt = findInTimeline(eventId);
|
||||
if (eventIt == historyEdge()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RoomEvent *event = eventIt->event();
|
||||
if (event && !event->isRedacted() && !is<RedactionEvent>(*event)) {
|
||||
events += eventId;
|
||||
}
|
||||
}
|
||||
|
||||
doDeleteMessageIds(events, reason);
|
||||
clearSelectedMessages();
|
||||
}
|
||||
|
||||
void NeoChatRoom::clearSelectedMessages()
|
||||
{
|
||||
m_selectedMessageIds.clear();
|
||||
Q_EMIT selectionChanged();
|
||||
}
|
||||
|
||||
#include "moc_neochatroom.cpp"
|
||||
|
||||
@@ -220,16 +220,6 @@ class NeoChatRoom : public Quotient::Room
|
||||
*/
|
||||
Q_PROPERTY(bool spaceHasUnreadMessages READ spaceHasUnreadMessages NOTIFY spaceHasUnreadMessagesChanged)
|
||||
|
||||
/**
|
||||
* @brief The number of selected messages in the room.
|
||||
*/
|
||||
Q_PROPERTY(int selectedMessageCount READ selectedMessageCount NOTIFY selectionChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the user can delete the selected messages.
|
||||
*/
|
||||
Q_PROPERTY(bool canDeleteSelectedMessages READ canDeleteSelectedMessages NOTIFY selectionChanged)
|
||||
|
||||
public:
|
||||
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
||||
|
||||
@@ -683,53 +673,9 @@ public:
|
||||
|
||||
/**
|
||||
* @return List of members in this room, sorted by power level and then by name.
|
||||
*
|
||||
* This list is only populated after sortAllMembers() is called.
|
||||
*/
|
||||
QList<QString> sortedMemberIds() const;
|
||||
|
||||
/**
|
||||
* @brief The number of selected messages in the room.
|
||||
*/
|
||||
int selectedMessageCount() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the user can delete the selected messages.
|
||||
*/
|
||||
bool canDeleteSelectedMessages() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the given message is selected.
|
||||
*/
|
||||
Q_INVOKABLE bool isMessageSelected(const QString &eventId) const;
|
||||
|
||||
/**
|
||||
* @brief Toggle the selection state of the given message.
|
||||
*/
|
||||
Q_INVOKABLE void toggleMessageSelection(const QString &eventId);
|
||||
|
||||
/**
|
||||
* @brief Get the content of the selected messages formatted as a single string.
|
||||
*/
|
||||
Q_INVOKABLE QString getFormattedSelectedMessages() const;
|
||||
|
||||
/**
|
||||
* @brief Delete the selected messages with an optional reason.
|
||||
*/
|
||||
Q_INVOKABLE void deleteSelectedMessages(const QString &reason = QString());
|
||||
|
||||
/**
|
||||
* @brief Clear the selection of messages.
|
||||
*/
|
||||
Q_INVOKABLE void clearSelectedMessages();
|
||||
|
||||
/**
|
||||
* @brief Sort all members based on their display name, and power level.
|
||||
*
|
||||
* @note This is a very expensive operation, and should only be done when truly needed.
|
||||
*/
|
||||
void sortAllMembers();
|
||||
|
||||
private:
|
||||
bool m_visible = false;
|
||||
|
||||
@@ -747,7 +693,7 @@ private:
|
||||
void onAddHistoricalTimelineEvents(rev_iter_t from) override;
|
||||
void onRedaction(const Quotient::RoomEvent &prevEvent, const Quotient::RoomEvent &after) override;
|
||||
|
||||
QCoro::Task<void> doDeleteMessageIds(const QStringList eventIds, QString reason);
|
||||
QCoro::Task<void> doDeleteMessagesByUser(const QString &user, QString reason);
|
||||
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString(), std::optional<Quotient::EventRelation> relatesTo = std::nullopt);
|
||||
|
||||
std::unique_ptr<Quotient::RoomEvent> m_cachedEvent;
|
||||
@@ -767,7 +713,6 @@ private:
|
||||
|
||||
QString m_lastUnreadHighlightId;
|
||||
QList<QString> m_sortedMemberIds;
|
||||
QSet<QString> m_selectedMessageIds;
|
||||
|
||||
private Q_SLOTS:
|
||||
void updatePushNotificationState(QString type);
|
||||
@@ -776,6 +721,8 @@ private Q_SLOTS:
|
||||
|
||||
void invalidateLastUnreadHighlightId(const QString &fromEventId, const QString &toEventId);
|
||||
|
||||
void refreshAllMembers();
|
||||
|
||||
void insertMemberSorted(Quotient::RoomMember member);
|
||||
|
||||
Q_SIGNALS:
|
||||
@@ -805,7 +752,6 @@ Q_SIGNALS:
|
||||
void pinnedMessageChanged();
|
||||
void highlightCycleStartedChanged();
|
||||
void spaceHasUnreadMessagesChanged();
|
||||
void selectionChanged();
|
||||
|
||||
/**
|
||||
* @brief Request a message be shown to the user of the given type.
|
||||
|
||||
@@ -51,16 +51,8 @@ SearchPage {
|
||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||
|
||||
title: i18nc("@action:title Explore public rooms and spaces", "Explore")
|
||||
customPlaceholderText: {
|
||||
if (publicRoomListModel.redirectedText.length > 0)
|
||||
return publicRoomListModel.redirectedText;
|
||||
if (publicRoomListModel.errorText.length > 0)
|
||||
return publicRoomListModel.errorText;
|
||||
|
||||
return "";
|
||||
}
|
||||
customPlaceholderText: publicRoomListModel.redirectedText
|
||||
customPlaceholderIcon: "data-warning"
|
||||
enableSearch: publicRoomListModel.errorText.length === 0
|
||||
|
||||
Component.onCompleted: focusSearch()
|
||||
|
||||
@@ -71,7 +63,6 @@ SearchPage {
|
||||
display: QQC2.Button.IconOnly
|
||||
checkable: true
|
||||
text: i18nc("@action:button", "Only show spaces")
|
||||
enabled: root.enableSearch
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
@@ -105,7 +96,7 @@ SearchPage {
|
||||
|
||||
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: root.customPlaceholderText.length === 0
|
||||
visible: publicRoomListModel.redirectedText.length === 0
|
||||
icon.name: "compass"
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
@@ -108,7 +108,7 @@ SearchPage {
|
||||
id: _private
|
||||
function openManualUserDialog(): void {
|
||||
let dialog = manualUserDialog.createObject(this, {
|
||||
connection: root.room.connection
|
||||
connection: root.connection
|
||||
}) as ManualUserDialog;
|
||||
dialog.parent = root.Window.window.overlay;
|
||||
dialog.accepted.connect(() => {
|
||||
|
||||
@@ -80,11 +80,6 @@ Kirigami.ScrollablePage {
|
||||
*/
|
||||
property bool showSearchButton: true
|
||||
|
||||
/**
|
||||
* @brief Enable the search controls like the text field and button.
|
||||
*/
|
||||
property bool enableSearch: true
|
||||
|
||||
/**
|
||||
* @brief Message to be shown in a custom placeholder.
|
||||
* The custom placeholder will be shown if the text is not empty
|
||||
@@ -144,7 +139,6 @@ Kirigami.ScrollablePage {
|
||||
Kirigami.SearchField {
|
||||
id: searchField
|
||||
focus: true
|
||||
enabled: root.enableSearch
|
||||
Layout.fillWidth: true
|
||||
Keys.onEnterPressed: searchButton.clicked()
|
||||
Keys.onReturnPressed: searchButton.clicked()
|
||||
@@ -164,7 +158,6 @@ Kirigami.ScrollablePage {
|
||||
display: QQC2.Button.IconOnly
|
||||
visible: root.showSearchButton
|
||||
text: i18nc("@action:button", "Search")
|
||||
enabled: root.enableSearch
|
||||
|
||||
onClicked: {
|
||||
if (typeof root.model.search === 'function') {
|
||||
@@ -180,6 +173,7 @@ Kirigami.ScrollablePage {
|
||||
Timer {
|
||||
id: searchTimer
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: if (typeof root.model.search === 'function') {
|
||||
root.model.search();
|
||||
}
|
||||
@@ -199,7 +193,7 @@ Kirigami.ScrollablePage {
|
||||
id: noSearchMessage
|
||||
icon.name: "search"
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length === 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
|
||||
visible: searchField.text.length === 0 && listView.count === 0 && customPlaceholder.text.length === 0
|
||||
helpfulAction: root.noSearchHelpfulAction
|
||||
}
|
||||
|
||||
@@ -214,13 +208,13 @@ Kirigami.ScrollablePage {
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: customPlaceholder
|
||||
anchors.centerIn: parent
|
||||
visible: listView.count === 0 && !root.model.searching && text.length > 0
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && text.length > 0
|
||||
icon.name: root.customPlaceholderIcon
|
||||
}
|
||||
|
||||
Kirigami.LoadingPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
visible: listView.count === 0 && (root.model.searching || searchTimer.running)
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && (root.model.searching || searchTimer.running) && customPlaceholder.text.length === 0
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "voicerecorder.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include <Quotient/events/filesourceinfo.h>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
VoiceRecorder::VoiceRecorder(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_buffer(new QBuffer)
|
||||
, m_format(QMediaFormat::FileFormat::Ogg)
|
||||
{
|
||||
m_session.setAudioInput(&m_input);
|
||||
m_recorder.setAudioBitRate(24000);
|
||||
m_recorder.setAudioSampleRate(48000);
|
||||
m_format.setAudioCodec(QMediaFormat::AudioCodec::Opus);
|
||||
m_recorder.setAudioChannelCount(1);
|
||||
m_recorder.setMediaFormat(m_format);
|
||||
m_buffer->open(QIODevice::ReadWrite);
|
||||
m_recorder.setOutputDevice(m_buffer);
|
||||
m_session.setRecorder(&m_recorder);
|
||||
}
|
||||
|
||||
VoiceRecorder::~VoiceRecorder()
|
||||
{
|
||||
delete m_buffer;
|
||||
}
|
||||
|
||||
void VoiceRecorder::startRecording()
|
||||
{
|
||||
m_buffer->setData({});
|
||||
m_recorder.record();
|
||||
}
|
||||
|
||||
void VoiceRecorder::stopRecording()
|
||||
{
|
||||
m_recorder.stop();
|
||||
}
|
||||
|
||||
QMediaRecorder *VoiceRecorder::recorder()
|
||||
{
|
||||
return &m_recorder;
|
||||
}
|
||||
|
||||
void VoiceRecorder::send()
|
||||
{
|
||||
Quotient::FileSourceInfo fileMetadata;
|
||||
QByteArray data;
|
||||
m_buffer->seek(0);
|
||||
|
||||
if (m_room->usesEncryption()) {
|
||||
std::tie(fileMetadata, data) = Quotient::encryptFile(m_buffer->data());
|
||||
m_buffer->close();
|
||||
m_buffer->setData(data);
|
||||
m_buffer->open(QIODevice::ReadOnly);
|
||||
}
|
||||
|
||||
auto room = m_room;
|
||||
auto buffer = m_buffer;
|
||||
auto duration = m_recorder.duration();
|
||||
m_buffer = nullptr;
|
||||
m_room->connection()->uploadContent(buffer, {}, u"audio/ogg"_s).then([fileMetadata, room, buffer, duration](const auto &job) mutable {
|
||||
QJsonObject mscFile{
|
||||
{u"mimetype"_s, u"audio/ogg"_s},
|
||||
{u"name"_s, u"Voice Message"_s},
|
||||
{u"size"_s, buffer->size()},
|
||||
};
|
||||
|
||||
if (room->usesEncryption()) {
|
||||
mscFile[u"file"_s] = toJson(fileMetadata);
|
||||
} else {
|
||||
mscFile[u"url"_s] = job->contentUri().toString();
|
||||
}
|
||||
|
||||
Quotient::setUrlInSourceInfo(fileMetadata, job->contentUri());
|
||||
QJsonObject content{
|
||||
{u"body"_s, u"Voice message"_s},
|
||||
{u"msgtype"_s, u"m.audio"_s},
|
||||
{u"org.matrix.msc1767.text"_s,
|
||||
QJsonObject{{u"body"_s, u"Voice Message (%1, %2)"_s.arg(KFormat().formatDuration(duration), KFormat().formatByteSize(buffer->size()))}}},
|
||||
{u"org.matrix.msc1767.file"_s, mscFile},
|
||||
{u"info"_s,
|
||||
QJsonObject{
|
||||
{u"mimetype"_s, u"audio/ogg"_s},
|
||||
{u"size"_s, buffer->size()},
|
||||
{u"duration"_s, duration},
|
||||
}},
|
||||
{u"org.matrix.msc1767.audio"_s,
|
||||
QJsonObject{
|
||||
{u"duration"_s, duration},
|
||||
{u"waveform"_s, QJsonArray{}}, // TODO
|
||||
}},
|
||||
{u"org.matrix.msc3245.voice"_s, QJsonObject{}}};
|
||||
if (room->usesEncryption()) {
|
||||
content[u"file"_s] = toJson(fileMetadata);
|
||||
} else {
|
||||
content[u"url"_s] = job->contentUri().toString();
|
||||
}
|
||||
room->postJson(u"m.room.message"_s, content);
|
||||
});
|
||||
}
|
||||
|
||||
void VoiceRecorder::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
NeoChatRoom *VoiceRecorder::room() const
|
||||
{
|
||||
return m_room.get();
|
||||
}
|
||||
|
||||
bool VoiceRecorder::isSupported() const
|
||||
{
|
||||
return m_format.isSupported(QMediaFormat::Encode);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include <QAudioInput>
|
||||
#include <QBuffer>
|
||||
#include <QMediaCaptureSession>
|
||||
#include <QMediaFormat>
|
||||
#include <QMediaRecorder>
|
||||
|
||||
class VoiceRecorder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QMediaRecorder *recorder READ recorder CONSTANT)
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED)
|
||||
// TODO: Remove once no longer required
|
||||
Q_PROPERTY(bool isSupported READ isSupported CONSTANT)
|
||||
|
||||
public:
|
||||
explicit VoiceRecorder(QObject *parent = nullptr);
|
||||
~VoiceRecorder() override;
|
||||
|
||||
Q_INVOKABLE void startRecording();
|
||||
Q_INVOKABLE void stopRecording();
|
||||
Q_INVOKABLE void send();
|
||||
|
||||
QMediaRecorder *recorder();
|
||||
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
bool isSupported() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
|
||||
private:
|
||||
QAudioInput m_input;
|
||||
QMediaCaptureSession m_session;
|
||||
QMediaRecorder m_recorder;
|
||||
QBuffer *m_buffer;
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QMediaFormat m_format;
|
||||
};
|
||||
@@ -27,15 +27,14 @@ RowLayout {
|
||||
required property var author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the event as a neoChatDateTime.
|
||||
* @brief The timestamp of the event as a NeoChatDateTime.
|
||||
*/
|
||||
required property neoChatDateTime dateTime
|
||||
required property NeoChatDateTime dateTime
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Label {
|
||||
id: nameButton
|
||||
@@ -46,15 +45,11 @@ RowLayout {
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
clip: true // Intentional to limit insane Unicode in display names
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: nameButton.implicitWidth + Kirigami.Units.smallSpacing
|
||||
|
||||
|
||||
function openUserMenu(): void {
|
||||
const menu = Qt.createComponent("org.kde.neochat", "UserMenu").createObject(root, {
|
||||
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow,
|
||||
author: root.author
|
||||
author: root.author,
|
||||
});
|
||||
menu.popup(root.QQC2.Overlay.overlay);
|
||||
}
|
||||
@@ -78,24 +73,20 @@ RowLayout {
|
||||
onTapped: nameButton.openUserMenu()
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
id: timeLabel
|
||||
|
||||
text: root.dateTime.shortRelativeDateTime
|
||||
text: root.dateTime.hourMinuteString
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
QQC2.ToolTip.visible: timeHoverHandler.hovered
|
||||
QQC2.ToolTip.text: root.dateTime.longDateTime
|
||||
QQC2.ToolTip.text: root.dateTime.shortDateTime
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
HoverHandler {
|
||||
id: timeHoverHandler
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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 QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
@@ -36,7 +34,7 @@ QQC2.Control {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
||||
Layout.preferredHeight: active ? item.implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0
|
||||
@@ -73,7 +71,7 @@ QQC2.Control {
|
||||
root.chatBarCache.text = text;
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: event => {
|
||||
Keys.onEnterPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
@@ -82,7 +80,7 @@ QQC2.Control {
|
||||
_private.post();
|
||||
}
|
||||
}
|
||||
Keys.onReturnPressed: event => {
|
||||
Keys.onReturnPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
@@ -110,7 +108,7 @@ QQC2.Control {
|
||||
height: implicitHeight
|
||||
y: -height - 5
|
||||
z: 10
|
||||
connection: root.Message.room.connection as NeoChatConnection
|
||||
connection: root.Message.room.connection
|
||||
chatDocumentHandler: documentHandler
|
||||
margins: 0
|
||||
Behavior on height {
|
||||
@@ -167,7 +165,7 @@ QQC2.Control {
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
icon.name: "mail-attachment"
|
||||
onClicked: {
|
||||
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay) as QQC2.Dialog;
|
||||
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
@@ -227,6 +225,8 @@ QQC2.Control {
|
||||
Message.maxContentWidth: paneLoader.item.width
|
||||
}
|
||||
QQC2.Button {
|
||||
id: cancelButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
@@ -261,9 +261,9 @@ QQC2.Control {
|
||||
function updateText() {
|
||||
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
|
||||
documentHandler.document;
|
||||
if (root.chatBarCache?.isEditing && root.chatBarCache.relationMessage.length > 0) {
|
||||
textArea.text = root.chatBarCache.relationMessage;
|
||||
documentHandler.updateMentions(root.chatBarCache.editId);
|
||||
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
|
||||
textArea.text = chatBarCache.relationMessage;
|
||||
documentHandler.updateMentions(chatBarCache.editId);
|
||||
textArea.forceActiveFocus();
|
||||
textArea.cursorPosition = textArea.text.length;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ QQC2.Control {
|
||||
required property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the event as a neoChatDateTime.
|
||||
* @brief The timestamp of the event as a NeoChatDateTime.
|
||||
*/
|
||||
required property neoChatDateTime dateTime
|
||||
required property NeoChatDateTime dateTime
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore as Core
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs as Dialogs
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -52,7 +51,7 @@ ColumnLayout {
|
||||
property bool autoOpenFile: false
|
||||
|
||||
function saveFileAs() {
|
||||
const dialog = fileDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
|
||||
const dialog = fileDialog.createObject(QQC2.Overlay.overlay);
|
||||
dialog.selectedFile = Message.room.fileNameToDownload(root.eventId);
|
||||
dialog.open();
|
||||
}
|
||||
@@ -71,7 +70,7 @@ ColumnLayout {
|
||||
states: [
|
||||
State {
|
||||
name: "downloadedInstant"
|
||||
when: root.fileTransferInfo.completed && root.autoOpenFile
|
||||
when: root.fileTransferInfo.completed && autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
openButton.icon.name: "document-open"
|
||||
@@ -85,7 +84,7 @@ ColumnLayout {
|
||||
},
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: root.fileTransferInfo.completed && !root.autoOpenFile
|
||||
when: root.fileTransferInfo.completed && !autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
openButton.visible: false
|
||||
@@ -139,7 +138,7 @@ ColumnLayout {
|
||||
id: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: {
|
||||
root.autoOpenFile = true;
|
||||
autoOpenFile = true;
|
||||
root.Message.room.downloadTempFile(root.eventId);
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ ColumnLayout {
|
||||
onAccepted: {
|
||||
NeoChatConfig.lastSaveDirectory = currentFolder;
|
||||
NeoChatConfig.save();
|
||||
if (root.autoOpenFile) {
|
||||
if (autoOpenFile) {
|
||||
UrlHelper.copyTo(root.fileTransferInfo.localPath, selectedFile);
|
||||
} else {
|
||||
root.Message.room.download(root.eventId, selectedFile);
|
||||
|
||||
@@ -40,7 +40,7 @@ ColumnLayout {
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
LiveLocationsModel {
|
||||
id: locationModel
|
||||
id: liveLocationModel
|
||||
eventId: root.eventId
|
||||
room: Message.room
|
||||
}
|
||||
@@ -50,13 +50,13 @@ ColumnLayout {
|
||||
Layout.preferredWidth: root.Message.maxContentWidth
|
||||
Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(locationModel.boundingBox.y, locationModel.boundingBox.x)
|
||||
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MapItemView {
|
||||
model: locationModel
|
||||
model: liveLocationModel
|
||||
delegate: LocationMapItem {}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ ColumnLayout {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
fullScreenMap.createObject(parent, {
|
||||
liveLocationModel: locationModel
|
||||
liveLocationModel: liveLocationModel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// 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
|
||||
|
||||
@@ -103,7 +102,7 @@ Flow {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var dialog = emojiDialog.createObject(reactButton) as EmojiDialog;
|
||||
var dialog = emojiDialog.createObject(reactButton);
|
||||
dialog.showStickers = false;
|
||||
dialog.chosen.connect(emoji => {
|
||||
root.Message.room.toggleReaction(root.eventId, emoji);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
@@ -119,7 +117,6 @@ DelegateChooser {
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Location
|
||||
delegate: MimeComponent {
|
||||
required property string display
|
||||
mimeIconSource: "mark-location"
|
||||
label: display
|
||||
}
|
||||
@@ -128,7 +125,6 @@ DelegateChooser {
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LiveLocation
|
||||
delegate: MimeComponent {
|
||||
required property string display
|
||||
mimeIconSource: "mark-location"
|
||||
label: display
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ void EventMessageContentModel::initializeModel()
|
||||
}
|
||||
});
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
connect(m_room, &Room::newThread, this, [this](const auto &newThread) {
|
||||
if (newThread == m_eventId) {
|
||||
connect(m_room, &Room::newThread, this, [this](const Thread &newThread) {
|
||||
if (newThread.threadRootId == m_eventId) {
|
||||
resetContent();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
@@ -61,7 +59,7 @@ Kirigami.OverlayDrawer {
|
||||
if (_lastX === -1) {
|
||||
return;
|
||||
}
|
||||
if (Application.layoutDirection === Qt.RightToLeft) {
|
||||
if (Qt.application.layoutDirection === Qt.RightToLeft) {
|
||||
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, root.roomDrawerWidth - _lastX + mapToGlobal(mouseX, mouseY).x));
|
||||
} else {
|
||||
root.actualWidth = Math.min(root.maxWidth, Math.max(root.minWidth, root.roomDrawerWidth + _lastX - mapToGlobal(mouseX, mouseY).x));
|
||||
@@ -70,7 +68,7 @@ Kirigami.OverlayDrawer {
|
||||
}
|
||||
enabled: true
|
||||
|
||||
edge: Application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
|
||||
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.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// 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.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
@@ -104,7 +103,7 @@ Kirigami.Page {
|
||||
|
||||
Connections {
|
||||
target: root.Kirigami.PageStack.pageStack
|
||||
function onWideModeChanged(): void {
|
||||
onWideModeChanged: {
|
||||
if ((root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode) {
|
||||
root.Kirigami.PageStack.pop();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.kde.neochat
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
property bool collapsed: false
|
||||
|
||||
signal search
|
||||
@@ -37,7 +36,7 @@ RowLayout {
|
||||
} else if(RoomManager.currentSpace === 'DM') {
|
||||
return i18nc("@title", "Direct Messages");
|
||||
}
|
||||
return root.connection.room(RoomManager.currentSpace)?.displayName;
|
||||
return root.connection.room(RoomManager.currentSpace).displayName;
|
||||
}
|
||||
return i18nc("@title List of rooms", "Rooms");
|
||||
}
|
||||
@@ -58,16 +57,4 @@ RowLayout {
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.Button.IconOnly
|
||||
visible: Kirigami.Settings.isMobile
|
||||
text: i18nc("@action:button", "Open Settings")
|
||||
icon.name: "settings-configure-symbolic"
|
||||
onClicked: NeoChatSettingsView.open()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ Kirigami.Page {
|
||||
contentItem: TreeView {
|
||||
id: treeView
|
||||
topMargin: Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
bottomMargin: Math.round(Kirigami.Units.smallSpacing / 2)
|
||||
|
||||
clip: true
|
||||
reuseItems: false
|
||||
@@ -148,17 +147,6 @@ Kirigami.Page {
|
||||
|
||||
selectionModel: ItemSelectionModel {}
|
||||
|
||||
// This is somewhat of a hack.
|
||||
// If we don't calculate this for TableView, then the content area doesn't quite scroll down far enough to cover the margins.
|
||||
rowHeightProvider: row => {
|
||||
// NOTE: This padding value should be kept in sync with the padding value of our delegates.
|
||||
const padding = Kirigami.Units.mediumSpacing;
|
||||
// NOTE: This calculation should be kept in sync with the height value of our delegates.
|
||||
return Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2) + padding * 2;
|
||||
}
|
||||
|
||||
// NOTE: For any future delegate spelunkers, please *keep the delegate heights in sync*.
|
||||
// If you fail to do so, weird scrolling behavior begins to manifest.
|
||||
delegate: DelegateChooser {
|
||||
role: "delegateType"
|
||||
|
||||
@@ -193,8 +181,8 @@ Kirigami.Page {
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
text: i18nc("@action:button", "Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
icon.width: Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2)
|
||||
icon.height: Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2)
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
onClicked: (Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
@@ -282,7 +270,6 @@ Kirigami.Page {
|
||||
Component {
|
||||
id: exploreComponent
|
||||
ExploreComponent {
|
||||
connection: root.connection
|
||||
collapsed: root.collapsed
|
||||
|
||||
onSearch: root.search()
|
||||
|
||||
@@ -6,11 +6,8 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
QQC2.ItemDelegate {
|
||||
id: root
|
||||
|
||||
required property TreeView treeView
|
||||
@@ -34,58 +31,43 @@ Delegates.RoundedItemDelegate {
|
||||
activeFocusOnTab: false
|
||||
background: null
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
|
||||
Keys.onEnterPressed: root.treeView.toggleExpanded(row)
|
||||
Keys.onReturnPressed: root.treeView.toggleExpanded(row)
|
||||
Keys.onSpacePressed: root.treeView.toggleExpanded(row)
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: Math.max(layout.implicitHeight, Kirigami.Units.gridUnit + (NeoChatConfig.compactRoomList ? 0 : Kirigami.Units.largeSpacing * 2))
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
Kirigami.ListSectionHeader {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.collapsed
|
||||
horizontalPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
text: root.collapsed ? "" : root.displayName
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.ListSectionHeader {
|
||||
visible: !root.collapsed
|
||||
horizontalPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
text: root.collapsed ? "" : root.displayName
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(root.row)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: collapseButton
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
icon {
|
||||
name: root.expanded ? "go-up" : "go-down"
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
height: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
text: root.expanded ? i18nc("Collapse <section name>", "Collapse %1", root.displayName) : i18nc("Expand <section name", "Expand %1", root.displayName)
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
activeFocusOnTab: false
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(root.row)
|
||||
}
|
||||
onClicked: root.treeView.toggleExpanded(row)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: collapseButton
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
// Reduce the size of the tap target, this is smaller the ginormous section header ItemDelegate
|
||||
TapHandler {
|
||||
onTapped: root.treeView.toggleExpanded(root.row)
|
||||
icon {
|
||||
name: root.expanded ? "go-up" : "go-down"
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
height: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
text: root.expanded ? i18nc("Collapse <section name>", "Collapse %1", root.displayName) : i18nc("Expand <section name", "Expand %1", root.displayName)
|
||||
display: QQC2.Button.IconOnly
|
||||
|
||||
activeFocusOnTab: false
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: root.treeView.toggleExpanded(root.row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ QQC2.Control {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
onActiveFocusChanged: if (activeFocus) {
|
||||
notificationsButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
@@ -42,9 +46,42 @@ QQC2.Control {
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
width: scrollView.width
|
||||
spacing: 0
|
||||
|
||||
AvatarTabButton {
|
||||
id: notificationsButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing / 2
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing / 2
|
||||
text: i18n("View notifications")
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "notifications"
|
||||
}
|
||||
visible: !Kirigami.Settings.isMobile // Shows up in the mobile bar instead
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
onSelected: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'NotificationsView'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Notifications"),
|
||||
modality: Qt.NonModal
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
visible: notificationsButton.visible
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
AvatarTabButton {
|
||||
id: allRoomButton
|
||||
|
||||
|
||||
@@ -89,23 +89,6 @@ RowLayout {
|
||||
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.Button.IconOnly
|
||||
text: i18n("View notifications")
|
||||
icon.name: "notifications"
|
||||
onClicked: (Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'NotificationsView'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Notifications"),
|
||||
modality: Qt.NonModal
|
||||
})
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.Button.IconOnly
|
||||
text: i18nc("@action:button", "Open Settings")
|
||||
|
||||
@@ -172,8 +172,6 @@ void PublicRoomListModel::next(int limit)
|
||||
}
|
||||
m_redirectedText.clear();
|
||||
Q_EMIT redirectedChanged();
|
||||
m_errorText.clear();
|
||||
Q_EMIT errorTextChanged();
|
||||
|
||||
if (job) {
|
||||
qCDebug(PublicRoomList) << "Other job running, ignore";
|
||||
@@ -202,12 +200,9 @@ void PublicRoomListModel::next(int limit)
|
||||
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
|
||||
rooms.append(job->chunk());
|
||||
this->endInsertRows();
|
||||
} else if (job->error() == BaseJob::ContentAccessError && !m_searchText.isEmpty()) {
|
||||
} else if (job->error() == BaseJob::ContentAccessError) {
|
||||
m_redirectedText = job->jsonData()[u"error"_s].toString();
|
||||
Q_EMIT redirectedChanged();
|
||||
} else {
|
||||
m_errorText = job->jsonData()[u"error"_s].toString();
|
||||
Q_EMIT errorTextChanged();
|
||||
}
|
||||
|
||||
this->job = nullptr;
|
||||
@@ -334,9 +329,4 @@ QString PublicRoomListModel::redirectedText() const
|
||||
return m_redirectedText;
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::errorText() const
|
||||
{
|
||||
return m_errorText;
|
||||
}
|
||||
|
||||
#include "moc_publicroomlistmodel.cpp"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user