Compare commits

..

1 Commits

Author SHA1 Message Date
Tobias Fella
092e092e18 WIP 2026-02-06 05:02:58 -05:00
129 changed files with 12268 additions and 16136 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -4,6 +4,9 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.neochat

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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()
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -16,5 +16,4 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
EmojiDialog.qml
EmojiTonesPicker.qml
ImageEditorPage.qml
VoiceMessageDialog.qml
)

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -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: {

View File

@@ -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);
}

View File

@@ -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;
};

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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
})
}
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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();
}
});

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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