Compare commits

..

1 Commits

Author SHA1 Message Date
Tobias Fella
cd70e2b47f Annotate functions in connections and port away from implicit onFoo 2025-08-08 11:16:27 +02:00
117 changed files with 14497 additions and 16464 deletions

View File

@@ -1,2 +0,0 @@
[General]
disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp"

View File

@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.16")
set(KF_MIN_VERSION "6.12")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)

View File

@@ -88,9 +88,3 @@ path = "memorytests/memtest-sync.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".contextProperties.ini"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"

View File

@@ -92,9 +92,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME actionstest
)
ecm_add_test(
roommanagertest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)

View File

@@ -1,132 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <QVariantList>
#include "accountmanager.h"
#include "models/actionsmodel.h"
#include "roommanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
class RoomManagerTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
void testMaximizeMedia();
};
void RoomManagerTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
QVERIFY(connection);
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
RoomManager::instance().setConnection(connection);
QSignalSpy roomSpy(&RoomManager::instance(), &RoomManager::currentRoomChanged);
RoomManager::instance().resolveResource(room->id());
QVERIFY(roomSpy.size() > 0);
}
void RoomManagerTest::testMaximizeMedia()
{
QSignalSpy spy(&RoomManager::instance(), &RoomManager::showMaximizedMedia);
QSignalSpy syncSpy(connection, &Connection::syncDone);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for empty event id");
RoomManager::instance().maximizeMedia(QString());
QVERIFY(!spy.wait(10));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for unknown event id \"Doesn't exist\"");
RoomManager::instance().maximizeMedia(u"Doesn't exist"_s);
QVERIFY(!spy.wait(10));
const auto eventWithoutMedia = server.sendEvent(room->id(),
u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"format"_s, u"org.matrix.custom.html"_s},
{u"formatted_body"_s, u"Foo"_s},
{u"msgtype"_s, u"m.text"_s},
}));
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(eventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(eventWithoutMedia);
QVERIFY(!spy.wait(10));
// NOTE: This is supposed to test that maximizing pending media works correctly. This probably doesn't work in the UI yet, but at least the backend supports
// it. If the server ever learns how to process events, this becomes pointless and we need to find a way of preventing *these* events from arriving
auto pendingEventWithoutMedia = room->postText(u"Hello"_s);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(pendingEventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(pendingEventWithoutMedia);
QVERIFY(!spy.wait(10));
const auto eventWithMedia = server.sendEvent(room->id(),
u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
RoomManager::instance().maximizeMedia(eventWithMedia);
QVERIFY(spy.size() == 1);
QVERIFY(spy[0][0] == 0);
auto pendingEventWithMedia = room->postJson(u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
RoomManager::instance().maximizeMedia(pendingEventWithMedia);
QVERIFY(spy.size() == 2);
QVERIFY(spy[1][0] == 0);
}
QTEST_MAIN(RoomManagerTest)
#include "roommanagertest.moc"

View File

@@ -115,36 +115,28 @@ void Server::start()
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
QMap<QString, QJsonArray> stateEvents;
QMap<QString, QJsonArray> roomAccountData;
for (const auto &roomData : m_roomsToCreate) {
stateEvents[roomData.id] += QJsonObject{
for (const auto &[roomId, matrixId] : m_roomsToCreate) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomData.id},
{u"sender"_s, roomData.members[0]},
{u"room_id"_s, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : roomData.members) {
stateEvents[roomData.id] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomData.id},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
QJsonObject tags;
for (const auto &tag : roomData.tags) {
tags[tag] = QJsonObject();
}
roomAccountData[roomData.id] += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, matrixId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
m_roomsToCreate.clear();
for (const auto &roomId : m_invitedUsers.keys()) {
@@ -199,18 +191,11 @@ void Server::start()
m_joinedUsers.clear();
QJsonObject rooms;
auto keys = stateEvents.keys() + m_events.keys();
for (const auto &roomId : QSet(keys.begin(), keys.end())) {
rooms[roomId] = QJsonObject{
{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}},
{u"account_data"_s, QJsonObject{{u"events"_s, roomAccountData[roomId]}}},
{u"timeline"_s, QJsonObject{{u"events"_s, m_events[roomId]}}},
};
for (const auto &roomId : stateEvents.keys()) {
rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}};
}
m_events.clear();
auto json = QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}};
responder.write(QJsonDocument(json), QHttpServerResponder::StatusCode::Ok);
responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok);
});
QSslConfiguration config;
@@ -230,11 +215,7 @@ void Server::start()
QString Server::createRoom(const QString &matrixId)
{
auto roomId = generateRoomId();
m_roomsToCreate += RoomData{
.members = {matrixId},
.id = roomId,
.tags = {},
};
m_roomsToCreate += {roomId, matrixId};
return roomId;
}
@@ -252,23 +233,3 @@ void Server::joinUser(const QString &roomId, const QString &matrixId)
{
m_joinedUsers[roomId] += matrixId;
}
QString Server::createServerNoticesRoom(const QString &matrixId)
{
auto roomId = createRoom(matrixId);
m_roomsToCreate.last().tags = {u"m.server_notice"_s};
return roomId;
}
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
{
const auto eventId = generateEventId();
m_events[roomId] += QJsonObject{
{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
};
return eventId;
}

View File

@@ -4,12 +4,6 @@
#include <QHttpServer>
#include <QSslServer>
struct RoomData {
QStringList members;
QString id;
QStringList tags;
};
class Server
{
public:
@@ -27,12 +21,6 @@ public:
void banUser(const QString &roomId, const QString &matrixId);
void joinUser(const QString &roomId, const QString &matrixId);
/**
* Create a server notices room.
*/
QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
private:
QHttpServer m_server;
QSslServer m_sslServer;
@@ -41,6 +29,5 @@ private:
QHash<QString, QList<QString>> m_bannedUsers;
QHash<QString, QList<QString>> m_joinedUsers;
QList<RoomData> m_roomsToCreate;
QMap<QString, QJsonArray> m_events;
QList<std::pair<QString, QString>> m_roomsToCreate;
};

View File

@@ -120,7 +120,7 @@
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
<p xml:lang="zh-TW">NeoChat 是一個讓您能夠完全利用 Matrix 網路的聊天應用程式。它讓您安全地傳送文字訊息、影片或音訊檔給家人、同事或朋友等等。</p>
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. يوفر نيوتشات كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP و تعدد الخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="de">NeoChat versucht eine vollumfängliche Anwendung für die Spezifikation von Matrix zu sein. Damit wird alles der aktuellen stabilen Spezifikation mit den erwähnenswerten Ausnahmen von VoIP, Diskussionsfäden und ein paar Teilen der Ende-zu-Ende-Verschlüsselung unterstützt. Zudem sind andere kleinere Auslassungen vorhanden, da sich die Matrixspezifikation ständig weiterentwickelt. Nichtsdestotrotz soll letztendlich die gesamte Spezifikation unterstützt werden.</p>
@@ -306,8 +306,8 @@
<keyword>Matrix</keyword>
<keyword>Kirigami</keyword>
</keywords>
<developer id="org.kde">
<name translate="no">KDE</name>
<developer id="kde.org">
<name>The KDE Community</name>
<url>https://kde.org</url>
</developer>
<metadata_license>CC0-1.0</metadata_license>

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

@@ -104,7 +104,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
DEPENDENCIES
QtCore
QtQuick
com.github.quotient_im.libquotient
IMPORTS
org.kde.neochat.libneochat
org.kde.neochat.rooms
@@ -116,15 +115,13 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.devtools
org.kde.neochat.login
org.kde.neochat.chatbar
org.kde.config
org.kde.purpose
org.kde.syntaxhighlighting
)
if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES
qml/ShareAction.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
)
else()
qt_target_qml_sources(neochat QML_FILES
@@ -341,6 +338,10 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)

View File

@@ -433,7 +433,7 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
if (icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);

View File

@@ -11,6 +11,7 @@ import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.settings
import org.kde.neochat.devtools
KirigamiComponents.ConvergentContextMenu {
id: root

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -55,7 +53,7 @@ Kirigami.Dialog {
text: i18nc("@button: login to or register a new account.", "Add Account")
contentItem: Delegates.SubtitleContentItem {
itemDelegate: addDelegate
subtitle: i18nc("@info", "Log in or create a new account")
subtitle: i18n("Log in or create a new account")
labelItem.textFormat: Text.PlainText
subtitleItem.textFormat: Text.PlainText
}
@@ -95,8 +93,8 @@ Kirigami.Dialog {
accountView.decrementCurrentIndex();
}
}
Keys.onEnterPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
Keys.onReturnPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
Keys.onEnterPressed: accountView.currentItem.clicked()
Keys.onReturnPressed: accountView.currentItem.clicked()
onVisibleChanged: {
for (let i = 0; i < accountView.count; i++) {

View File

@@ -6,6 +6,8 @@ import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Dialog {
id: root
@@ -18,7 +20,7 @@ Kirigami.Dialog {
title: i18nc("@title:dialog", "Start a chat")
contentItem: QQC2.Label {
text: i18nc("@info", "Do you want to start a chat with %1?", root.user.displayName)
text: i18n("Do you want to start a chat with %1?", root.user.displayName)
textFormat: Text.PlainText
wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter

View File

@@ -58,18 +58,14 @@ ColumnLayout {
QQC2.ToolButton {
id: cancelAttachmentButton
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel sending attachment")
icon.name: "dialog-close"
onClicked: root.attachmentCancelled()
action: Kirigami.Action {
text: i18n("Cancel sending attachment")
icon.name: "dialog-close"
onTriggered: root.attachmentCancelled()
shortcut: "Escape"
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Kirigami.Action {
shortcut: "Escape"
onTriggered: cancelAttachmentButton.clicked()
}
}
}

86
src/app/qml/EditMenu.qml Normal file
View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import Qt.labs.platform as Labs
import QtQuick
import QtQuick.Layouts
Labs.Menu {
id: root
required property Item field
Labs.MenuItem {
enabled: root.field !== null && root.field.canUndo
text: i18nc("text editing menu action", "Undo")
shortcut: StandardKey.Undo
onTriggered: {
root.field.undo();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.canRedo
text: i18nc("text editing menu action", "Redo")
shortcut: StandardKey.Redo
onTriggered: {
root.field.undo();
root.close();
}
}
Labs.MenuSeparator {}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText
text: i18nc("text editing menu action", "Cut")
shortcut: StandardKey.Cut
onTriggered: {
root.field.cut();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText
text: i18nc("text editing menu action", "Copy")
shortcut: StandardKey.Copy
onTriggered: {
root.field.copy();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.canPaste
text: i18nc("text editing menu action", "Paste")
shortcut: StandardKey.Paste
onTriggered: {
root.field.paste();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText !== ""
text: i18nc("text editing menu action", "Delete")
shortcut: ""
onTriggered: {
root.field.remove(root.field.selectionStart, root.field.selectionEnd);
root.close();
}
}
Labs.MenuSeparator {}
Labs.MenuItem {
enabled: root.field !== null
text: i18nc("text editing menu action", "Select All")
shortcut: StandardKey.SelectAll
onTriggered: {
root.field.selectAll();
root.close();
}
}
}

View File

@@ -59,7 +59,7 @@ ApplicationWindow {
Connections {
target: mapView.map
function onCopyrightLinkActivated() {
function onCopyrightLinkActivated(link: string): void {
Qt.openUrlExternally(link);
}
}

View File

@@ -5,6 +5,7 @@ import Qt.labs.platform as Labs
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
@@ -15,7 +16,6 @@ Labs.MenuBar {
id: root
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow appWindow
Labs.Menu {
title: i18nc("menu", "File")
@@ -23,8 +23,8 @@ Labs.MenuBar {
Labs.MenuItem {
icon.name: "list-add-user"
text: i18nc("@action:inmenu", "Find your Friends")
enabled: root.connection
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
@@ -33,22 +33,21 @@ Labs.MenuBar {
Labs.MenuItem {
icon.name: "system-users-symbolic"
text: i18nc("@action:inmenu", "Create a Room…")
enabled: root.connection
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
shortcut: StandardKey.New
onTriggered: {
Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root.appWindow, {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Create a Room")
}).open();
});
}
}
Labs.MenuItem {
icon.name: "compass-symbolic"
text: i18nc("@action:inmenu", "Explore Rooms")
enabled: root.connection
onTriggered: {
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
@@ -59,6 +58,7 @@ Labs.MenuBar {
}
}
Labs.MenuItem {
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat…")
text: i18nc("menu", "Configure NeoChat…")
shortcut: StandardKey.Preferences
@@ -71,15 +71,17 @@ Labs.MenuBar {
onTriggered: Qt.quit()
}
}
EditMenu {
title: i18nc("menu", "Edit")
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
}
Labs.Menu {
title: i18nc("menu", "View")
Labs.MenuItem {
icon.name: "search-symbolic"
enabled: root.connection
text: i18nc("@action:inmenu opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Search Rooms")
onTriggered: (root.appWindow as Main).quickSwitcher.open()
onTriggered: quickSwitcher.open()
}
}
Labs.Menu {
@@ -87,8 +89,8 @@ Labs.MenuBar {
Labs.MenuItem {
icon.name: "view-fullscreen-symbolic"
text: root.appWindow.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
onTriggered: root.appWindow.visibility === Window.FullScreen ? root.appWindow.showNormal() : root.appWindow.showFullScreen()
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
}
}
Labs.Menu {
@@ -97,12 +99,12 @@ Labs.MenuBar {
Labs.MenuItem {
icon.name: "help-about-symbolic"
text: i18nc("menu", "About NeoChat")
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
}
Labs.MenuItem {
icon.name: "kde-symbolic"
text: i18nc("menu", "About KDE")
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
}
}
}

View File

@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.config as KConfig
@@ -19,11 +20,6 @@ Kirigami.ApplicationWindow {
property bool initialized: false
readonly property QuickSwitcher quickSwitcher: QuickSwitcher {
connection: root.connection
window: root
}
title: {
if (NeoChatConfig.windowTitleFocus) {
return activeFocusItem + " " + (activeFocusItem ? activeFocusItem.Accessible.name : "");
@@ -64,21 +60,21 @@ Kirigami.ApplicationWindow {
Connections {
target: LoginHelper
function onLoaded() {
function onLoaded(): void {
root.load();
}
}
Connections {
target: Registration
function onLoaded() {
function onLoaded(): void {
root.load();
}
}
Connections {
target: root.quitAction
function onTriggered() {
function onTriggered(): void {
Qt.quit();
}
}
@@ -87,7 +83,6 @@ Kirigami.ApplicationWindow {
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
sourceComponent: GlobalMenu {
connection: root.connection
appWindow: root
}
}
@@ -95,39 +90,51 @@ Kirigami.ApplicationWindow {
configGroupName: "MainWindow"
}
QuickSwitcher {
id: quickSwitcher
connection: root.connection
}
Connections {
target: RoomManager
function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = root.pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
function onCurrentRoomChanged(): void {
if (RoomManager.currentRoom && pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom();
});
}
}
function onAskJoinRoom(room) {
(Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
function onAskJoinRoom(room: NeoChatRoom): void {
Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
room: room,
connection: root.connection
}) as JoinRoomDialog).open();
}).open();
}
function onShowUserDetail(user, room) {
function onShowUserDetail(user, room: NeoChatRoom): void {
root.showUserDetail(user, room);
}
function onAskDirectChatConfirmation(user) {
(Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
user: user
}) as AskDirectChatConfirmation).open();
function goToEvent(event: string): void {
if (event.length > 0) {
roomItem.goToEvent(event);
}
roomItem.forceActiveFocus();
}
function onExternalUrl(url) {
(Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this, {
link: url
}) as ConfirmUrlDialog).open();
function onAskDirectChatConfirmation(user): void {
Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
user: user
}).open();
}
function onExternalUrl(url): void {
let dialog = Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this);
dialog.link = url;
dialog.open();
}
}
@@ -191,7 +198,7 @@ Kirigami.ApplicationWindow {
dim = false;
}
}
enabled: RoomManager.hasOpenRoom && root.pageStack.layers.depth < 2 && root.pageStack.depth < 3 && (root.pageStack.visibleItems.length > 1 || root.pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 && (pageStack.visibleItems.length > 1 || pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
handleVisible: enabled
}
@@ -213,10 +220,10 @@ Kirigami.ApplicationWindow {
Connections {
target: NeoChatConfig
function onBlurChanged() {
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
}
function onCompactLayoutChanged() {
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
}
}
@@ -271,8 +278,8 @@ Kirigami.ApplicationWindow {
target: AccountRegistry
function onRowsRemoved() {
if (AccountRegistry.rowCount() === 0) {
root.pageStack.clear();
root.pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
pageStack.clear();
pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
}
}
}
@@ -281,7 +288,7 @@ Kirigami.ApplicationWindow {
target: Controller
function onErrorOccured(error) {
root.showPassiveNotification(error, "short");
showPassiveNotification(error, "short");
}
}
@@ -296,9 +303,9 @@ Kirigami.ApplicationWindow {
});
}
function onUserConsentRequired(url) {
(Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
url: url
}) as ConsentDialog).open();
}).open();
}
}
@@ -346,7 +353,7 @@ Kirigami.ApplicationWindow {
room: room,
user: user,
connection: root.connection,
}) as UserDetailDialog;
});
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
dialog.open();
}

View File

@@ -72,7 +72,7 @@ Components.AlbumMaximizeComponent {
Connections {
target: MediaManager
function onPlaybackStarted() {
function onPlaybackStarted(): void {
if (currentItem.playbackState === MediaPlayer.PlayingState) {
currentItem.pause();
}
@@ -82,7 +82,7 @@ Components.AlbumMaximizeComponent {
Connections {
target: currentRoom
function onFileTransferProgress(id, progress, total) {
function onFileTransferProgress(id: string, progress: int, total: int): void {
if (id == root.currentEventId) {
root.downloadAction.progress = progress / total * 100.0;
}
@@ -130,7 +130,7 @@ Components.AlbumMaximizeComponent {
Connections {
target: RoomManager
function onCloseFullScreen() {
function onCloseFullScreen(): void {
root.close();
}
}

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -10,8 +8,11 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.kirigamiaddons.delegates as Delegates
import Quotient
import org.kde.neochat
Kirigami.Dialog {
@@ -42,22 +43,22 @@ Kirigami.Dialog {
FormCard.FormComboBoxDelegate {
id: pollTypeCombo
text: i18nc("@label", "Poll type:")
text: i18n("Poll type:")
currentIndex: 0
textRole: "text"
valueRole: "value"
model: [
{ value: PollKind.Disclosed, text: i18nc("@item:inlistbox", "Open poll") },
{ value: PollKind.Undisclosed, text: i18nc("@item:inlistbox", "Closed poll") }
{ value: PollKind.Disclosed, text: i18n("Open poll") },
{ value: PollKind.Undisclosed, text: i18n("Closed poll") }
]
}
FormCard.FormTextDelegate {
verticalPadding: 0
text: pollTypeCombo.currentValue == 0 ? i18nc("@info", "Voters can see the result as soon as they have voted") : i18nc("@info", "Results are revealed only after the poll has closed")
text: pollTypeCombo.currentValue == 0 ? i18n("Voters can see the result as soon as they have voted") : i18n("Results are revealed only after the poll has closed")
}
FormCard.FormTextFieldDelegate {
id: questionTextField
label: i18nc("@label", "Question:")
label: i18n("Question:")
}
Repeater {
id: optionRepeater
@@ -120,12 +121,16 @@ Kirigami.Dialog {
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Remove option")
icon.name: "edit-delete-remove"
onClicked: optionModel.remove(optionDelegate.index)
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
action: Kirigami.Action {
id: removeOptionAction
text: i18nc("@action:button", "Remove option")
icon.name: "edit-delete-remove"
onTriggered: optionModel.remove(optionDelegate.index)
}
QQC2.ToolTip {
text: removeOptionAction.text
delay: Kirigami.Units.toolTipDelay
}
}
}
}

View File

@@ -9,6 +9,6 @@ FileDialog {
signal chosen(string path)
title: i18nc("@title:dialog", "Select a File")
title: i18n("Select a File")
onAccepted: root.chosen(selectedFile)
}

View File

@@ -22,7 +22,7 @@ Kirigami.Page {
Connections {
target: root.QQC2.ApplicationWindow.window
function onClosing() {
function onClosing(): void {
root.destroy();
}
}

View File

@@ -3,6 +3,7 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
@@ -19,7 +20,7 @@ QQC2.Popup {
contentItem: Flow {
QQC2.ToolButton {
icon.name: "format-text-bold"
text: i18nc("@action:button", "Bold")
text: i18n("Bold")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -28,7 +29,7 @@ QQC2.Popup {
end: "**",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -38,7 +39,7 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "format-text-italic"
text: i18nc("@action:button", "Italic")
text: i18n("Italic")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -47,7 +48,7 @@ QQC2.Popup {
end: "*",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -57,7 +58,7 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "format-text-strikethrough"
text: i18nc("@action:button", "Strikethrough")
text: i18n("Strikethrough")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -66,7 +67,7 @@ QQC2.Popup {
end: "~~",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -76,7 +77,7 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "view-hidden-symbolic"
text: i18nc("@action:button", "Spoiler")
text: i18n("Spoiler")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -85,7 +86,7 @@ QQC2.Popup {
end: "||",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -95,7 +96,7 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "format-text-code"
text: i18nc("@action:button", "Code block")
text: i18n("Code block")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -104,7 +105,7 @@ QQC2.Popup {
end: "`",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -114,16 +115,16 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "format-text-blockquote"
text: i18nc("@action:button", "Quote")
text: i18n("Quote")
display: QQC2.AbstractButton.IconOnly
onClicked: {
const format = {
start: root.selectionStart == 0 ? ">" : "\n>",
start: selectionStart == 0 ? ">" : "\n>",
end: "\n\n",
extra: ""
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}
@@ -133,7 +134,7 @@ QQC2.Popup {
}
QQC2.ToolButton {
icon.name: "link"
text: i18nc("@action:button", "Insert link")
text: i18n("Insert link")
display: QQC2.AbstractButton.IconOnly
onClicked: {
@@ -142,7 +143,7 @@ QQC2.Popup {
end: "](",
extra: ")"
};
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
formattingSelected(format, selectionStart, selectionEnd);
root.close();
}

View File

@@ -1,10 +1,9 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
@@ -14,15 +13,14 @@ Kirigami.SearchDialog {
id: root
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window
Shortcut {
sequence: "Ctrl+K"
onActivated: if (root.connection) root.open()
onActivated: root.open()
}
onAccepted: if (currentItem) {
(currentItem as QQC2.ItemDelegate).clicked();
currentItem.clicked();
}
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
@@ -34,7 +32,7 @@ Kirigami.SearchDialog {
icon.name: "compass"
onTriggered: {
root.close()
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")

View File

@@ -7,6 +7,8 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root

View File

@@ -4,6 +4,7 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami
@@ -62,24 +63,24 @@ Kirigami.Page {
actions: [
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
visible: Kirigami.Settings.isMobile || !root.Kirigami.PageStack.pageStack.wideMode
icon.name: "view-right-new"
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
}
]
KeyNavigation.left: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).get(0)
KeyNavigation.left: pageStack.get(0)
onCurrentRoomChanged: {
banner.visible = false;
if (!Kirigami.Settings.isMobile && chatBarLoader.item) {
(chatBarLoader.item as ChatBar).forceActiveFocus();
chatBarLoader.item.forceActiveFocus();
}
}
Connections {
target: root.currentRoom.connection
function onIsOnlineChanged() {
function onIsOnlineChanged(): void {
if (!root.currentRoom.connection.isOnline) {
banner.text = i18nc("@info:status", "NeoChat is offline. Please check your network connection.");
banner.visible = true;
@@ -161,20 +162,20 @@ Kirigami.Page {
Connections {
target: RoomManager
function onCurrentRoomChanged() {
function onCurrentRoomChanged(): void {
if (root.currentRoom && root.currentRoom.isInvite) {
Controller.clearInvitationNotification(root.currentRoom.id);
}
}
function onGoToEvent(eventId) {
function onGoToEvent(eventId: string): void {
(timelineViewLoader.item as TimelineView).goToEvent(eventId);
}
}
Connections {
target: root.currentRoom.connection
function onJoinedRoom(room, invited) {
function onJoinedRoom(room: NeoChatRoom, invited: NeoChatRoom): void {
if (root.currentRoom.id === invited.id) {
RoomManager.resolveResource(room.id);
}
@@ -184,23 +185,23 @@ Kirigami.Page {
Keys.onPressed: event => {
if (event.key === Qt.Key_PageUp) {
event.accepted = true;
(timelineViewLoader.item as TimelineView).pageUp();
timelineViewLoader.item.pageUp();
} else if (event.key === Qt.Key_PageDown) {
event.accepted = true;
(timelineViewLoader.item as TimelineView).pageDown();
timelineViewLoader.item.pageDown();
}
}
Connections {
target: RoomManager
function onShowMessage(messageType, message) {
function onShowMessage(messageType: Kirigami.MessageType, message: string): void {
banner.text = message;
banner.type = messageType;
banner.visible = true;
}
function onShowEventSource(eventId) {
function onShowEventSource(eventId: string): void {
(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.currentRoom.getEventJsonSource(eventId)
}, {
@@ -209,7 +210,7 @@ Kirigami.Page {
});
}
function onShowMessageMenu(eventId, author, messageComponentType, plainText, htmlText, selectedText, hoveredLink, isThread) {
function onShowMessageMenu(eventId: string, author, messageComponentType, plainText: string, htmlText: string, selectedText: string, hoveredLink: string, isThread: bool): void {
const contextMenu = messageDelegateContextMenu.createObject(root, {
selectedText: selectedText,
hoveredLink: hoveredLink,

View File

@@ -177,7 +177,7 @@ QQC2.ComboBox {
Connections {
target: serverListModel
function onServerCheckComplete(url, valid) {
function onServerCheckComplete(url: string, valid: bool): void {
if (url == serverUrlField.text && valid) {
serverUrlField.isValidServer = true;
}

View File

@@ -1,9 +1,8 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.purpose as Purpose
@@ -21,8 +20,8 @@ Kirigami.Action {
id: root
icon.name: "emblem-shared-symbolic"
text: i18nc("@action:button", "Share")
tooltip: i18nc("@info:tooltip", "Share the selected media")
text: i18n("Share")
tooltip: i18n("Share the selected media")
/**
* This property holds the input data for purpose.
@@ -48,17 +47,14 @@ Kirigami.Action {
}
delegate: Kirigami.Action {
required property int index
required property string display
required property string iconName
text: display
icon.name: iconName
property int index
text: model.display
icon.name: model.iconName
onTriggered: {
root.room.download(root.eventId, root.inputData.urls[0]);
root.room.fileTransferCompleted.connect(share);
}
function share(id: string): void {
function share(id) {
if (id != root.eventId) {
return;
}

View File

@@ -6,6 +6,7 @@
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.purpose as Purpose
@@ -23,7 +24,7 @@ Kirigami.Page {
bottomPadding: 0
property alias index: jobView.index
required property var model
property alias model: jobView.model
QQC2.Action {
shortcut: 'Escape'
@@ -33,7 +34,7 @@ Kirigami.Page {
Notification {
id: sharingFailed
eventId: "Share"
text: i18nc("@info:status", "Sharing failed")
text: i18n("Sharing failed")
urgency: Notification.NormalUrgency
}
@@ -50,12 +51,11 @@ Kirigami.Page {
Purpose.JobView {
id: jobView
model: root.model
anchors.fill: parent
onStateChanged: {
if (state === Purpose.PurposeJobController.Finished) {
if (jobView.job?.output?.url?.length > 0) {
sharingSuccess.text = i18nc("@info", "Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
sharingSuccess.text = i18n("Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
sharingSuccess.sendEvent();
Clipboard.saveText(jobView.job.output.url);
}

View File

@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard

View File

@@ -142,95 +142,107 @@ Kirigami.Dialog {
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId && !!root.user
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
icon.name: "im-invisible-user"
onClicked: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
action: Kirigami.Action {
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
icon.name: "im-invisible-user"
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("kick") && room.containsUser(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Kick this user")
icon.name: "im-kick-user"
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
action: Kirigami.Action {
text: i18n("Kick this user")
icon.name: "im-kick-user"
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("invite") && !room.containsUser(root.user.id)
enabled: root.room && !root.room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Invite this user")
icon.name: "list-add-user"
onClicked: {
root.room.inviteToRoom(root.user.id);
root.close();
action: Kirigami.Action {
enabled: root.room && !root.room.isUserBanned(root.user.id)
text: i18n("Invite this user")
icon.name: "list-add-user"
onTriggered: {
root.room.inviteToRoom(root.user.id);
root.close();
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && !room.isUserBanned(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Ban this user")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
action: Kirigami.Action {
text: i18n("Ban this user")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
root.room.unban(root.user.id);
root.close();
action: Kirigami.Action {
text: i18n("Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
root.room.unban(root.user.id);
root.close();
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.room.canSendState("m.room.power_levels")
text: i18nc("@action:button", "Set user power level")
icon.name: "visibility"
onClicked: {
let dialog = powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
});
dialog.open();
root.close();
action: Kirigami.Action {
text: i18n("Set user power level")
icon.name: "visibility"
onTriggered: {
let dialog = powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
});
dialog.open();
root.close();
}
}
Component {
@@ -244,40 +256,48 @@ Kirigami.Dialog {
FormCard.FormButtonDelegate {
visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact"))
text: i18nc("@action:button", "Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
action: Kirigami.Action {
text: i18nc("@action:button", "Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
}
}
}
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
icon.name: "document-send"
onClicked: {
root.connection.requestDirectChat(root.user.id);
root.close();
action: Kirigami.Action {
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
icon.name: "document-send"
onTriggered: {
root.connection.requestDirectChat(root.user.id);
root.close();
}
}
}
FormCard.FormButtonDelegate {
text: i18n("Copy link")
icon.name: "username-copy"
onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id)
action: Kirigami.Action {
text: i18n("Copy link")
icon.name: "username-copy"
onTriggered: {
Clipboard.saveText("https://matrix.to/#/" + root.user.id);
}
}
}
}
}

View File

@@ -245,7 +245,6 @@ void RoomManager::maximizeMedia(const QString &eventId)
const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId);
if (index == -1) {
qWarning() << "Tried to open media for unknown event id" << eventId;
return;
}

View File

@@ -26,11 +26,11 @@ QQC2.Popup {
icon.name: 'mail-attachment'
text: i18nc("@action:button", "Choose local file")
text: i18n("Choose local file")
onClicked: {
root.close();
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay) as OpenFileDialog;
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay);
fileDialog.chosen.connect(path => root.chosen(path));
fileDialog.open();
}
@@ -42,7 +42,7 @@ QQC2.Popup {
Layout.fillHeight: true
icon.name: 'insert-image'
text: i18nc("@action:button", "Clipboard image")
text: i18n("Clipboard image")
onClicked: {
const path = StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + "/screenshots/" + (new Date()).getTime() + ".png";
if (!Clipboard.saveImage(path)) {

View File

@@ -56,7 +56,7 @@ QQC2.Control {
}
Connections {
target: root.currentRoom.mainCache
target: currentRoom.mainCache
function onMentionAdded(mention: string): void {
// add mention text
@@ -74,16 +74,16 @@ QQC2.Control {
* Each of these will be visualised in the ChatBar so new actions can be added
* by appending to this list.
*/
property list<BusyAction> actions: [
BusyAction {
property list<Kirigami.Action> actions: [
Kirigami.Action {
id: attachmentAction
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
// Matrix does not allow sending attachments in replies
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18nc("@action:button", "Attach an image or file")
text: i18n("Attach an image or file")
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: {
@@ -94,14 +94,14 @@ QQC2.Control {
tooltip: text
},
BusyAction {
Kirigami.Action {
id: emojiAction
isBusy: false
property bool isBusy: false
visible: !Kirigami.Settings.isMobile
icon.name: "smiley"
text: i18nc("@action:button", "Emojis & Stickers")
text: i18n("Emojis & Stickers")
displayHint: Kirigami.DisplayHint.IconOnly
checkable: true
@@ -114,11 +114,11 @@ QQC2.Control {
}
tooltip: text
},
BusyAction {
Kirigami.Action {
id: mapButton
icon.name: "mark-location-symbolic"
isBusy: false
text: i18nc("@action:button", "Send a Location")
property bool isBusy: false
text: i18n("Send a Location")
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
@@ -128,10 +128,10 @@ QQC2.Control {
}
tooltip: text
},
BusyAction {
Kirigami.Action {
id: pollButton
icon.name: "amarok_playcount"
isBusy: false
property bool isBusy: false
text: i18nc("@action:button", "Create a Poll")
displayHint: QQC2.AbstractButton.IconOnly
@@ -142,13 +142,13 @@ QQC2.Control {
}
tooltip: text
},
BusyAction {
Kirigami.Action {
id: sendAction
isBusy: false
property bool isBusy: false
icon.name: "document-send"
text: i18nc("@action:button", "Send message")
text: i18n("Send message")
displayHint: Kirigami.DisplayHint.IconOnly
checkable: true
@@ -191,7 +191,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.currentRoom.mainCache.replyId.length > 0
@@ -218,7 +218,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.currentRoom.mainCache.attachmentPath.length > 0
@@ -250,10 +250,9 @@ QQC2.Control {
QQC2.TextArea {
id: textField
placeholderText: root.currentRoom.usesEncryption ? i18nc("@placeholder", "Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18nc("@placeholder", "Set an attachment caption…") : i18nc("@placeholder", "Send a message…")
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18n("Set an attachment caption…") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
textFormat: TextEdit.MarkdownText
Accessible.description: placeholderText
@@ -270,6 +269,7 @@ QQC2.Control {
root.currentRoom.sendTypingNotification(textExists);
textExists ? repeatTimer.start() : repeatTimer.stop();
}
_private.chatBarCache.text = text;
}
onSelectedTextChanged: {
if (selectedText.length > 0) {
@@ -285,7 +285,7 @@ QQC2.Control {
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: (format, selectionStart, selectionEnd) => _private.formatText(format, selectionStart, selectionEnd)
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd)
}
Keys.onEnterPressed: event => {
@@ -363,8 +363,6 @@ QQC2.Control {
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
id: actionDelegate
required property BusyAction modelData
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
@@ -375,7 +373,7 @@ QQC2.Control {
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
contentItem: PieProgressBar {
visible: actionDelegate.modelData.isBusy
visible: modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
}
@@ -502,8 +500,13 @@ QQC2.Control {
ChatDocumentHandler {
id: documentHandler
type: ChatBarType.Room
textItem: textField
room: root.currentRoom
document: textField.textDocument
cursorPosition: textField.cursorPosition
selectionStart: textField.selectionStart
selectionEnd: textField.selectionEnd
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
Component {
@@ -562,7 +565,7 @@ QQC2.Control {
currentRoom: root.currentRoom
onChosen: emoji => root.insertText(emoji)
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) {
emojiAction.checked = false;
}
@@ -573,8 +576,4 @@ QQC2.Control {
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition);
textField.cursorPosition = initialCursorPosition + text.length;
}
component BusyAction : Kirigami.Action {
required property bool isBusy
}
}

View File

@@ -25,7 +25,7 @@ QQC2.Popup {
Connections {
target: RoomManager
function onCurrentRoomChanged() {
function onCurrentRoomChanged(): void {
root.close();
}
}

View File

@@ -100,7 +100,7 @@ Kirigami.Page {
}
Connections {
target: selectionTool.selectionArea
function onDoubleClicked() {
function onDoubleClicked(): void {
rootEditorView.crop();
}
}

View File

@@ -47,10 +47,6 @@ target_sources(LibNeoChat PRIVATE
models/userlistmodel.cpp
)
if (TARGET KF6::KIOWidgets)
target_compile_definitions(LibNeoChat PUBLIC -DHAVE_KIO)
endif()
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.libneochat
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
@@ -62,8 +58,6 @@ ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
qml/SearchPage.qml
qml/CreateRoomDialog.qml
qml/CreateSpaceDialog.qml
DEPENDENCIES
com.github.quotient_im.libquotient
)
ecm_qt_declare_logging_category(LibNeoChat

View File

@@ -1,19 +1,16 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "chatdocumenthandler.h"
#include <QQmlFile>
#include <QQmlFileSelector>
#include <QQuickTextDocument>
#include <QStringBuilder>
#include <QSyntaxHighlighter>
#include <QTextBlock>
#include <QTextDocument>
#include <QTimer>
#include <Kirigami/Platform/PlatformTheme>
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
@@ -36,16 +33,10 @@ public:
SyntaxHighlighter(QObject *parent)
: QSyntaxHighlighter(parent)
{
m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, [this]() {
mentionFormat.setForeground(m_theme->linkColor());
errorFormat.setForeground(m_theme->negativeTextColor());
});
mentionFormat.setFontWeight(QFont::Bold);
mentionFormat.setForeground(m_theme->linkColor());
mentionFormat.setForeground(Qt::blue);
errorFormat.setForeground(m_theme->negativeTextColor());
errorFormat.setForeground(Qt::red);
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
@@ -110,22 +101,29 @@ public:
}),
mentions->end());
}
private:
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
};
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
: QObject(parent)
, m_document(nullptr)
, m_cursorPosition(-1)
, m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel(this))
{
}
void ChatDocumentHandler::updateCompletion() const
{
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
if (!m_document) {
m_highlighter->setDocument(nullptr);
return;
}
m_highlighter->setDocument(m_document->textDocument());
});
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
if (!m_room) {
return;
}
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
}
int ChatDocumentHandler::completionStartIndex() const
@@ -162,58 +160,38 @@ void ChatDocumentHandler::setType(ChatBarType::Type type)
Q_EMIT typeChanged();
}
QQuickItem *ChatDocumentHandler::textItem() const
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_textItem;
return m_document;
}
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
{
if (textItem == m_textItem) {
if (document == m_document) {
return;
}
if (m_textItem) {
m_textItem->disconnect(this);
if (const auto textDoc = document()) {
textDoc->disconnect(this);
}
if (m_document) {
m_document->textDocument()->disconnect(this);
}
m_textItem = textItem;
m_highlighter->setDocument(document());
if (m_textItem) {
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCompletion()));
if (document()) {
connect(document(), &QTextDocument::contentsChanged, this, [this]() {
if (m_room) {
m_room->cacheForType(m_type)->setText(getText());
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
}
});
}
}
Q_EMIT textItemChanged();
}
QTextDocument *ChatDocumentHandler::document() const
{
if (!m_textItem) {
return nullptr;
}
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(m_textItem->property("textDocument"));
return quickDocument ? quickDocument->textDocument() : nullptr;
m_document = document;
Q_EMIT documentChanged();
}
int ChatDocumentHandler::cursorPosition() const
{
if (!m_textItem) {
return -1;
return m_cursorPosition;
}
void ChatDocumentHandler::setCursorPosition(int position)
{
if (position == m_cursorPosition) {
return;
}
return m_textItem->property("cursorPosition").toInt();
if (m_room) {
m_cursorPosition = position;
}
Q_EMIT cursorPositionChanged();
}
NeoChatRoom *ChatDocumentHandler::room() const
@@ -229,8 +207,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
if (m_room && m_type != ChatBarType::None) {
m_room->cacheForType(m_type)->disconnect(this);
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
m_room->mainCache()->setSavedText(document()->toPlainText());
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) {
m_room->mainCache()->setSavedText(document()->textDocument()->toPlainText());
}
}
@@ -242,8 +220,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
document()->setPlainText(room->mainCache()->savedText());
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) {
document()->textDocument()->setPlainText(room->mainCache()->savedText());
m_room->mainCache()->setText(room->mainCache()->savedText());
}
}
@@ -261,7 +239,7 @@ ChatBarCache *ChatDocumentHandler::chatBarCache() const
void ChatDocumentHandler::complete(int index)
{
if (document() == nullptr) {
if (m_document == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return;
}
@@ -278,7 +256,7 @@ void ChatDocumentHandler::complete(int index)
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('@'), fromIndex);
QTextCursor cursor(document());
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(name + u" "_s);
@@ -291,7 +269,7 @@ void ChatDocumentHandler::complete(int index)
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('/'), fromIndex);
QTextCursor cursor(document());
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(u"/%1 "_s.arg(command));
@@ -299,7 +277,7 @@ void ChatDocumentHandler::complete(int index)
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char('#'), fromIndex);
QTextCursor cursor(document());
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(alias + u" "_s);
@@ -312,7 +290,7 @@ void ChatDocumentHandler::complete(int index)
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.indexOf(QLatin1Char(':'), fromIndex);
QTextCursor cursor(document());
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(shortcode);
@@ -324,13 +302,43 @@ CompletionModel *ChatDocumentHandler::completionModel() const
return m_completionModel;
}
int ChatDocumentHandler::selectionStart() const
{
return m_selectionStart;
}
void ChatDocumentHandler::setSelectionStart(int position)
{
if (position == m_selectionStart) {
return;
}
m_selectionStart = position;
Q_EMIT selectionStartChanged();
}
int ChatDocumentHandler::selectionEnd() const
{
return m_selectionEnd;
}
void ChatDocumentHandler::setSelectionEnd(int position)
{
if (position == m_selectionEnd) {
return;
}
m_selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QString ChatDocumentHandler::getText() const
{
if (!document()) {
qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available.";
if (!m_room || m_type == ChatBarType::None) {
qCWarning(ChatDocumentHandling) << "getText called with no ChatBarCache available. ChatBarType: " << m_type << " Room: " << m_room;
return {};
}
return document()->toRawText();
return m_room->cacheForType(m_type)->text();
}
void ChatDocumentHandler::pushMention(const Mention mention) const
@@ -342,8 +350,42 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
m_room->cacheForType(m_type)->mentions()->push_back(mention);
}
void ChatDocumentHandler::updateMentions(const QString &editId)
QColor ChatDocumentHandler::mentionColor() const
{
return m_mentionColor;
}
void ChatDocumentHandler::setMentionColor(const QColor &color)
{
if (m_mentionColor == color) {
return;
}
m_mentionColor = color;
m_highlighter->mentionFormat.setForeground(m_mentionColor);
m_highlighter->rehighlight();
Q_EMIT mentionColorChanged();
}
QColor ChatDocumentHandler::errorColor() const
{
return m_errorColor;
}
void ChatDocumentHandler::setErrorColor(const QColor &color)
{
if (m_errorColor == color) {
return;
}
m_errorColor = color;
m_highlighter->errorFormat.setForeground(m_errorColor);
m_highlighter->rehighlight();
Q_EMIT errorColorChanged();
}
void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QString &editId)
{
setDocument(document);
if (editId.isEmpty() || m_type == ChatBarType::None || !m_room) {
return;
}
@@ -367,7 +409,7 @@ void ChatDocumentHandler::updateMentions(const QString &editId)
const int end = position + name.length();
linkSize += match.capturedLength(0) - name.length();
QTextCursor cursor(document());
QTextCursor cursor(this->document()->textDocument());
cursor.setPosition(position);
cursor.setPosition(end, QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);

View File

@@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor>
#include "chatbarcache.h"
@@ -13,8 +13,6 @@
#include "models/completionmodel.h"
#include "neochatroom.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;
@@ -71,9 +69,24 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
/**
* @brief The QML text Item the ChatDocumentHandler is handling.
* @brief The QQuickTextDocument that is being handled.
*/
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
/**
* @brief The current saved cursor position.
*/
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
/**
* @brief The start position of any currently selected text.
*/
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
/**
* @brief The end position of any currently selected text.
*/
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
/**
* @brief The current CompletionModel.
@@ -88,14 +101,33 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The color to highlight user mentions.
*/
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
/**
* @brief The color to highlight spelling errors.
*/
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
ChatBarType::Type type() const;
void setType(ChatBarType::Type type);
QQuickItem *textItem() const;
void setTextItem(QQuickItem *textItem);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
[[nodiscard]] int cursorPosition() const;
void setCursorPosition(int position);
[[nodiscard]] int selectionStart() const;
void setSelectionStart(int position);
[[nodiscard]] int selectionEnd() const;
void setSelectionEnd(int position);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
@@ -106,27 +138,41 @@ public:
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
void setMentionColor(const QColor &color);
[[nodiscard]] QColor errorColor() const;
void setErrorColor(const QColor &color);
/**
* @brief Update the mentions in @p document when editing a message.
*/
Q_INVOKABLE void updateMentions(const QString &editId);
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, const QString &editId);
Q_SIGNALS:
void typeChanged();
void textItemChanged();
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private:
ChatBarType::Type m_type = ChatBarType::None;
QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
void updateCompletion() const;
int completionStartIndex() const;
ChatBarType::Type m_type = ChatBarType::None;
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;
int cursorPosition() const;
QColor m_mentionColor;
QColor m_errorColor;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;
QString getText() const;
void pushMention(const Mention mention) const;

View File

@@ -1269,7 +1269,7 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
return;
}
downloadFile(eventId,
QUrl(u"file:"_s + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ evtIt->event()->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(
this,
@@ -1290,36 +1290,33 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
void NeoChatRoom::copyEventMedia(const QString &eventId)
{
const auto evtIt = findInTimeline(eventId);
if (evtIt == messageEvents().rend() || !is<RoomMessageEvent>(**evtIt)) {
return;
}
const auto event = evtIt->viewAs<RoomMessageEvent>();
if (!event->has<EventContent::FileContentBase>()) {
return;
}
const auto transferInfo = fileTransferInfo(eventId);
if (transferInfo.completed()) {
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
} else {
downloadFile(eventId,
QUrl(u"file:"_s + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(
this,
&Room::fileTransferCompleted,
this,
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
}
},
Qt::SingleShotConnection);
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
const auto event = evtIt->viewAs<RoomMessageEvent>();
if (event->has<EventContent::FileContent>()) {
const auto transferInfo = fileTransferInfo(eventId);
if (transferInfo.completed()) {
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
} else {
downloadFile(eventId,
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(
this,
&Room::fileTransferCompleted,
this,
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
}
},
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
}
}
}
}

View File

@@ -2,10 +2,12 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat

View File

@@ -2,10 +2,12 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat

View File

@@ -3,12 +3,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtLocation
import QtPositioning
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
/** Location marker for any of the shared location maps. */
MapQuickItem {
id: root

View File

@@ -134,8 +134,8 @@ Kirigami.ScrollablePage {
Keys.onReturnPressed: searchButton.clicked()
onTextChanged: {
searchTimer.restart();
if (root.model) {
root.model.searchText = text;
if (model) {
model.searchText = text;
}
}
}
@@ -147,8 +147,8 @@ Kirigami.ScrollablePage {
text: i18nc("@action:button", "Search")
onClicked: {
if (typeof root.model.search === 'function') {
root.model.search();
if (typeof model.search === 'function') {
model.search();
}
}
@@ -160,8 +160,8 @@ Kirigami.ScrollablePage {
id: searchTimer
interval: 500
running: true
onTriggered: if (typeof root.model.search === 'function') {
root.model.search();
onTriggered: if (typeof model.search === 'function') {
model.search();
}
}
}

View File

@@ -39,7 +39,7 @@ LoginStep {
Connections {
target: Registration
function onConnected(connection): void {
function onConnected(connection: NeoChatConnection): void {
root.processed("Loading");
}
}

View File

@@ -23,7 +23,7 @@ LoginStep {
Connections {
target: Controller
function onConnectionAdded(connection) {
function onConnectionAdded(connection: NeoChatConnection): void {
connection.syncDone.connect(() => root.closeDialog());
}
}

View File

@@ -14,7 +14,7 @@ LoginStep {
Connections {
target: LoginHelper
function onConnected() {
function onConnected(): void {
processed("Loading");
}
}

View File

@@ -18,10 +18,10 @@ LoginStep {
Connections {
target: LoginHelper
function onSsoUrlChanged() {
function onSsoUrlChanged(): void {
UrlHelper.openUrl(LoginHelper.ssoUrl);
}
function onConnected() {
function onConnected(): void {
processed("Loading");
}
}

View File

@@ -214,7 +214,7 @@ Kirigami.Page {
Connections {
target: Registration
function onNextStepChanged() {
function onNextStepChanged(): void {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
@@ -232,7 +232,7 @@ Kirigami.Page {
Connections {
target: LoginHelper
function onLoginErrorOccured(message) {
function onLoginErrorOccured(message: string): void {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;

View File

@@ -102,7 +102,7 @@ ColumnLayout {
Connections {
target: MediaManager
function onPlaybackStarted() {
function onPlaybackStarted(): void {
if (audio.playbackState === MediaPlayer.PlayingState) {
audio.pause();
}

View File

@@ -125,8 +125,13 @@ QQC2.Control {
ChatDocumentHandler {
id: documentHandler
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
textItem: textArea
document: textArea.textDocument
cursorPosition: textArea.cursorPosition
selectionStart: textArea.selectionStart
selectionEnd: textArea.selectionEnd
room: root.Message.room
mentionColor: Kirigami.Theme.linkColor
errorColor: Kirigami.Theme.negativeTextColor
}
TextMetrics {
@@ -161,45 +166,42 @@ QQC2.Control {
QQC2.ToolButton {
visible: !root.isBusy
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Attach an image or file")
icon.name: "mail-attachment"
onClicked: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
action: Kirigami.Action {
text: i18nc("@action:button", "Attach an image or file")
icon.name: "mail-attachment"
onTriggered: {
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
display: QQC2.AbstractButton.IconOnly
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send"
onClicked: _private.post()
action: Kirigami.Action {
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
icon.name: "document-send"
onTriggered: {
_private.post();
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: cancelButton
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onClicked: {
root.chatBarCache.clearRelations();
}
Kirigami.Action {
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel")
icon.name: "dialog-close"
onTriggered: {
root.chatBarCache.clearRelations();
}
shortcut: "Escape"
onTriggered: cancelButton.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
@@ -262,7 +264,7 @@ QQC2.Control {
documentHandler.document;
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
textArea.text = chatBarCache.relationMessage;
documentHandler.updateMentions(chatBarCache.editId);
documentHandler.updateMentions(textArea.textDocument, chatBarCache.editId);
textArea.forceActiveFocus();
textArea.cursorPosition = textArea.text.length;
}

View File

@@ -70,7 +70,7 @@ ColumnLayout {
}
Connections {
target: mapView.map
function onCopyrightLinkActivated() {
function onCopyrightLinkActivated(link: string): void {
Qt.openUrlExternally(link);
}
}

View File

@@ -68,7 +68,7 @@ ColumnLayout {
Connections {
target: mapView.map
function onCopyrightLinkActivated(link: string) {
function onCopyrightLinkActivated(link: string): void {
Qt.openUrlExternally(link);
}
}

View File

@@ -167,7 +167,7 @@ Video {
Connections {
target: MediaManager
function onPlaybackStarted() {
function onPlaybackStarted(): void {
if (root.playbackState === MediaPlayer.PlayingState) {
root.pause();
}
@@ -370,17 +370,15 @@ Video {
id: maximizeButton
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Maximize")
icon.name: "view-fullscreen"
onClicked: {
root.Message.timeline.interactive = false;
root.pause();
RoomManager.maximizeMedia(root.eventId);
action: Kirigami.Action {
text: i18n("Maximize")
icon.name: "view-fullscreen"
onTriggered: {
root.Message.timeline.interactive = false;
root.pause();
RoomManager.maximizeMedia(root.eventId);
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
}
}
background: Kirigami.ShadowedRectangle {

View File

@@ -59,7 +59,7 @@ Kirigami.Page {
Connections {
target: mapView.map
function onCopyrightLinkActivated(link: string) {
function onCopyrightLinkActivated(link: string): void {
Qt.openUrlExternally(link);
}
}

Some files were not shown because too many files have changed in this diff Show More