Compare commits

..

4 Commits

Author SHA1 Message Date
James Graham
45c46ddcbb Tobias' fix 2024-09-12 09:12:53 +01:00
James Graham
31d83ac0e3 Hack to see if kquickimageeditor is also the problem 2024-09-12 08:12:31 +01:00
James Graham
909eec30d2 Temp disable color scheme so CI builds 2024-09-11 08:51:59 +01:00
James Graham
dbed3e99c2 Create a mobile version of FileDelegateContextMenu with no purpose import 2024-09-11 08:33:49 +01:00
212 changed files with 32424 additions and 37108 deletions

View File

@@ -3,4 +3,5 @@
[BlueprintSettings] [BlueprintSettings]
kde/applications/neochat.packageAppx=True kde/applications/neochat.packageAppx=True
kde/frameworks/extra-cmake-modules.version=master
libs/qt.qtMajorVersion=6 libs/qt.qtMajorVersion=6

View File

@@ -110,7 +110,7 @@
{ {
"type": "git", "type": "git",
"url": "https://github.com/quotient-im/libQuotient.git", "url": "https://github.com/quotient-im/libQuotient.git",
"tag": "0.9.2", "branch": "0.8.x",
"disable-submodules": true "disable-submodules": true
} }
], ],

View File

@@ -10,7 +10,6 @@ include:
- /gitlab-templates/windows-qt6.yml - /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml # - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml - /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml - /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml - /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml - /gitlab-templates/craft-windows-x86-64-qt6.yml

View File

@@ -14,7 +14,6 @@ Dependencies:
'frameworks/kquickcharts': '@latest-kf6' 'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6' 'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6' 'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6' 'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6' 'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6' 'frameworks/prison': '@latest-kf6'

View File

@@ -8,13 +8,13 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "12") set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "3") set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.6") set(KF_MIN_VERSION "6.5")
set(QT_MIN_VERSION "6.5") set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -39,8 +39,6 @@ include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory) include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk) include(ECMAddAndroidApk)
include(ECMQmlModule) include(ECMQmlModule)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
if (NOT ANDROID) if (NOT ANDROID)
include(KDEClangFormat) include(KDEClangFormat)
endif() endif()
@@ -66,7 +64,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW) qt_policy(SET QTP0004 NEW)
endif () endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES set_package_properties(KF6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Basic application components" PURPOSE "Basic application components"
@@ -103,11 +101,11 @@ else()
) )
endif() endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU) if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED) find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif() endif()
find_package(QuotientQt6 0.9) find_package(QuotientQt6 0.8.2)
set_package_properties(QuotientQt6 PROPERTIES set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API" DESCRIPTION "Qt wrapper around Matrix API"
@@ -115,6 +113,11 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server" PURPOSE "Talk with matrix server"
) )
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
find_package(cmark) find_package(cmark)
set_package_properties(cmark PROPERTIES set_package_properties(cmark PROPERTIES
TYPE REQUIRED TYPE REQUIRED

View File

@@ -11,7 +11,7 @@ A Qt/QML based Matrix client.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a> <a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a> <a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://apps.kde.org/store_badges/snapstore/en.svg'/></a> <a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
## Introduction ## Introduction

View File

@@ -55,6 +55,5 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- for Android >= 33 -->
</manifest> </manifest>

View File

@@ -12,7 +12,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.6.0' classpath 'com.android.tools.build:gradle:7.4.1'
} }
} }

View File

@@ -5,26 +5,6 @@
"!room_id_1234:localhost:1234": { "!room_id_1234:localhost:1234": {
"state": { "state": {
"events": [ "events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{ {
"type": "m.room.member", "type": "m.room.member",
"state_key": "@user:localhost:1234", "state_key": "@user:localhost:1234",
@@ -46,26 +26,6 @@
}, },
"timeline": { "timeline": {
"events": [ "events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{ {
"type": "m.room.message", "type": "m.room.message",
"sender": "@user:localhost:1234", "sender": "@user:localhost:1234",

View File

@@ -11,11 +11,11 @@ ecm_add_test(
TEST_NAME neochatroomtest TEST_NAME neochatroomtest
) )
# ecm_add_test( ecm_add_test(
# texthandlertest.cpp texthandlertest.cpp
# LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
# TEST_NAME texthandlertest TEST_NAME texthandlertest
# ) )
ecm_add_test( ecm_add_test(
delegatesizehelpertest.cpp delegatesizehelpertest.cpp
@@ -53,6 +53,12 @@ ecm_add_test(
TEST_NAME messageeventmodeltest TEST_NAME messageeventmodeltest
) )
ecm_add_test(
actionshandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME actionshandlertest
)
ecm_add_test( ecm_add_test(
windowcontrollertest.cpp windowcontrollertest.cpp
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
@@ -76,9 +82,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
TEST_NAME linkpreviewertest TEST_NAME linkpreviewertest
) )
ecm_add_test(
messagecontentmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME messagecontentmodeltest
)

View File

@@ -0,0 +1,43 @@
// 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
#include <QTest>
#include "actionshandler.h"
#include "chatbarcache.h"
#include "testutils.h"
class ActionsHandlerTest : public QObject
{
Q_OBJECT
private:
Quotient::Connection *connection = Quotient::Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
ActionsHandler *actionsHandler = new ActionsHandler(this);
private Q_SLOTS:
void nullObject();
};
void ActionsHandlerTest::nullObject()
{
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(nullptr);
auto chatBarCache = new ChatBarCache(this);
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(chatBarCache);
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
actionsHandler->setRoom(room);
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(nullptr);
// The final one should throw no warning so we make sure.
QTest::failOnWarning("ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(chatBarCache);
}
QTEST_GUILESS_MAIN(ActionsHandlerTest)
#include "actionshandlertest.moc"

View File

@@ -51,7 +51,7 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString())); QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -65,7 +65,7 @@ void ChatBarCacheTest::noRoom()
// ChatBarCache has no parent. // ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember()); QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -81,7 +81,7 @@ void ChatBarCacheTest::badParent()
// ChatBarCache has no parent. // ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember()); QCOMPARE(chatBarCache->relationUser(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation."); QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -99,7 +99,7 @@ void ChatBarCacheTest::reply()
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org")); QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(QLatin1String("@example:example.org"))); QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -121,7 +121,7 @@ void ChatBarCacheTest::edit()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true); QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org")); QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationAuthor(), room->member(QLatin1String("@example:example.org"))); QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message")); QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
@@ -138,7 +138,7 @@ void ChatBarCacheTest::attachment()
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString())); QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString()); QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path")); QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
} }

View File

@@ -213,13 +213,11 @@ void EventHandlerTest::genericBody_data()
QTest::addColumn<int>("eventNum"); QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output"); QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message"); QTest::newRow("message") << 0 << QStringLiteral("sent a message");
QTest::newRow("member") << 1 QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
<< QStringLiteral( QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
"<a href=\"https://matrix.to/#/@example:example.org\">after</a> changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message");
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event"); QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
QTest::newRow("video") << 4 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message"); QTest::newRow("video") << 4 << QStringLiteral("sent a message");
} }
void EventHandlerTest::genericBody() void EventHandlerTest::genericBody()
@@ -227,16 +225,13 @@ void EventHandlerTest::genericBody()
QFETCH(int, eventNum); QFETCH(int, eventNum);
QFETCH(QString, output); QFETCH(QString, output);
QCOMPARE(EventHandler::genericBody(room, room->messageEvents().at(eventNum).get()), output); QCOMPARE(EventHandler::genericBody(room->messageEvents().at(eventNum).get()), output);
} }
void EventHandlerTest::nullGenericBody() void EventHandlerTest::nullGenericBody()
{ {
QTest::ignoreMessage(QtWarningMsg, "genericBody called with room set to nullptr.");
QCOMPARE(EventHandler::genericBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "genericBody called with event set to nullptr."); QTest::ignoreMessage(QtWarningMsg, "genericBody called with event set to nullptr.");
QCOMPARE(EventHandler::genericBody(room, nullptr), QString()); QCOMPARE(EventHandler::genericBody(nullptr), QString());
} }
void EventHandlerTest::markdownBody() void EventHandlerTest::markdownBody()

View File

@@ -1,61 +0,0 @@
// 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
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include "models/messagecontentmodel.h"
#include "testutils.h"
using namespace Quotient;
using namespace Qt::Literals::StringLiterals;
class MessageContentModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
private Q_SLOTS:
void initTestCase();
void missingEvent();
};
void MessageContentModelTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
}
void MessageContentModelTest::missingEvent()
{
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#firstRoom:kde.org"));
auto model1 = MessageContentModel(room, "$153456789:example.org"_L1);
QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), "Loading"_L1);
auto model2 = MessageContentModel(room, "$153456789:example.org"_L1, true);
QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), "Loading reply"_L1);
room->syncNewEvents(QLatin1String("test-min-sync.json"));
QCOMPARE(model1.rowCount(), 2);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Author);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::ComponentTypeRole), MessageComponentType::Text);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::DisplayRole), u"<b>This is an example<br>text message</b>"_s);
}
QTEST_MAIN(MessageContentModelTest)
#include "messagecontentmodeltest.moc"

View File

@@ -12,7 +12,9 @@
#include "enums/messagecomponenttype.h" #include "enums/messagecomponenttype.h"
#include "models/customemojimodel.h" #include "models/customemojimodel.h"
#include "models/messagecontentmodel.h"
#include "neochatconnection.h" #include "neochatconnection.h"
#include "utils.h"
#include "testutils.h" #include "testutils.h"
@@ -63,7 +65,6 @@ private Q_SLOTS:
void receiveRichEdited(); void receiveRichEdited();
void receiveLineSeparator(); void receiveLineSeparator();
void receiveRichCodeUrl(); void receiveRichCodeUrl();
void receiveRichColor();
void componentOutput_data(); void componentOutput_data();
void componentOutput(); void componentOutput();
@@ -85,11 +86,11 @@ void TextHandlerTest::initTestCase()
void TextHandlerTest::allowedAttributes() void TextHandlerTest::allowedAttributes()
{ {
const QString testInputString1 = QStringLiteral("<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"); const QString testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
const QString testOutputString1 = QStringLiteral("<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"); const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
// Handle urls where the href has either single (') or double (") quotes. // Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = QStringLiteral("<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"); const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
const QString testOutputString2 = QStringLiteral("<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"); const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString1); testTextHandler.setData(testInputString1);
@@ -117,7 +118,7 @@ void TextHandlerTest::stripDisallowedTags()
void TextHandlerTest::stripDisallowedAttributes() void TextHandlerTest::stripDisallowedAttributes()
{ {
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>"); const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
const QString testOutputString = QStringLiteral("Test"); const QString testOutputString = QStringLiteral("<p>Test</p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -144,8 +145,8 @@ void TextHandlerTest::emptyCodeTags()
void TextHandlerTest::sendSimpleStringCase() void TextHandlerTest::sendSimpleStringCase()
{ {
const QString testInputString = QStringLiteral("This data should just be left alone."); const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
const QString testOutputString = QStringLiteral("This data should just be left alone."); const QString testOutputString = QStringLiteral("<p>This data should just be put in a paragraph.</p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -158,8 +159,8 @@ void TextHandlerTest::sendSingleParaMarkup()
const QString testInputString = QStringLiteral( const QString testInputString = QStringLiteral(
"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`."); "Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`.");
const QString testOutputString = QStringLiteral( const QString testOutputString = QStringLiteral(
"Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img " "<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>."); "src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -185,7 +186,7 @@ void TextHandlerTest::sendMultipleSectionMarkup()
void TextHandlerTest::sendBadLinks() void TextHandlerTest::sendBadLinks()
{ {
const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)"); const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)");
const QString testOutputString = QStringLiteral("<a>link</a>, <img alt=\"image\">"); const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -222,8 +223,8 @@ void TextHandlerTest::sendCodeClass()
void TextHandlerTest::sendCustomEmoji() void TextHandlerTest::sendCustomEmoji()
{ {
const QString testInputString = QStringLiteral(":test:"); const QString testInputString = QStringLiteral(":test:");
const QString testOutputString = const QString testOutputString = QStringLiteral(
QStringLiteral("<img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" />"); "<p><img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" /></p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -236,7 +237,7 @@ void TextHandlerTest::sendCustomEmojiCode_data()
QTest::addColumn<QString>("testInputString"); QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString"); QTest::addColumn<QString>("testOutputString");
QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral("<code>:test:</code>"); QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral("<p><code>:test:</code></p>");
QTest::newRow("block") << QStringLiteral("```\n:test:\n```") << QStringLiteral("<pre><code>:test:\n</code></pre>"); QTest::newRow("block") << QStringLiteral("```\n:test:\n```") << QStringLiteral("<pre><code>:test:\n</code></pre>");
} }
@@ -374,7 +375,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill() void TextHandlerTest::receiveRichUserPill()
{ {
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"); const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
const QString testOutputString = QStringLiteral("<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>"); const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -385,7 +386,7 @@ void TextHandlerTest::receiveRichUserPill()
void TextHandlerTest::receiveRichStrikethrough() void TextHandlerTest::receiveRichStrikethrough()
{ {
const QString testInputString = QStringLiteral("<p><del>Test</del></p>"); const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
const QString testOutputString = QStringLiteral("<s>Test</s>"); const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -521,25 +522,6 @@ void TextHandlerTest::receiveRichCodeUrl()
QCOMPARE(testTextHandler.handleRecieveRichText(), input); QCOMPARE(testTextHandler.handleRecieveRichText(), input);
} }
void TextHandlerTest::receiveRichColor()
{
const QString testInputString = QStringLiteral(
"<span data-mx-color=\"#ff00be\">¯</span><span data-mx-color=\"#ff3b1d\">\\</span><span data-mx-color=\"#ffa600\">_</span><span "
"data-mx-color=\"#64d200\">(</span><span data-mx-color=\"#00e261\">ツ</span><span data-mx-color=\"#00e7ff\">)</span><span "
"data-mx-color=\"#00e1ff\">_</span><span data-mx-color=\"#00bdff\">/</span><span data-mx-color=\"#ff60ff\">¯</span>");
const QString testOutputString = QStringLiteral(
"<span style=\"color: #ff00be;\">¯</span><span style=\"color: #ff3b1d;\">\\</span><span style=\"color: #ffa600;\">_</span><span style=\"color: "
"#64d200;\">(</span><span style=\"color: #00e261;\">ツ</span><span style=\"color: #00e7ff;\">)</span><span style=\"color: #00e1ff;\">_</span><span "
"style=\"color: #00bdff;\">/</span><span style=\"color: #ff60ff;\">¯</span>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
qInfo() << testTextHandler.handleRecieveRichText();
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::componentOutput_data() void TextHandlerTest::componentOutput_data()
{ {
QTest::addColumn<QString>("testInputString"); QTest::addColumn<QString>("testInputString");
@@ -555,7 +537,7 @@ void TextHandlerTest::componentOutput_data()
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}}; QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>") QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, << QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Quote, QStringLiteral("blockquote"), {}}}; MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};
QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>") QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, << QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}}; MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};

View File

@@ -50,40 +50,41 @@
<name xml:lang="x-test">xxNeoChatxx</name> <name xml:lang="x-test">xxNeoChatxx</name>
<name xml:lang="zh-CN">NeoChat</name> <name xml:lang="zh-CN">NeoChat</name>
<name xml:lang="zh-TW">NeoChat</name> <name xml:lang="zh-TW">NeoChat</name>
<summary>Chat on Matrix</summary> <summary>Chat with your friends on matrix</summary>
<summary xml:lang="ar">دردش على ماتركس</summary> <summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
<summary xml:lang="ca">Xat a Matrix</summary> <summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xat a Matrix</summary> <summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="de">Über Matrix unterhalten</summary> <summary xml:lang="cs">Mluvte se svými přáteli na Matrixu</summary>
<summary xml:lang="en-GB">Chat on Matrix</summary> <summary xml:lang="en-GB">Chat with your friends on matrix</summary>
<summary xml:lang="eo">Babilo en Matrix</summary> <summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle en Matrix</summary> <summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketa Matrix-en</summary> <summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="fi">Keskustelu Matrixissä</summary> <summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter sur Matrix</summary> <summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charlar en Matrix</summary> <summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="he">התכתבות דרך Matrix</summary> <summary xml:lang="he">התכתבות עם החברים שלך ב־matrix</summary>
<summary xml:lang="hu">Csevegés Matrixon</summary> <summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Conversation en ditecto sur Matrix</summary> <summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Chat su Matrix</summary> <summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">საუბრეთ Matrix-ზე</summary> <summary xml:lang="ka">საუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="nl">Chat op Matrix</summary> <summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nn">Prat med via Matrix</summary> <summary xml:lang="lv">Tērzējiet ar saviem draugiem „Matrix“ tīklā</summary>
<summary xml:lang="pl">Rozmawiaj na Matriksie</summary> <summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="sl">Klepet na Matrixu</summary> <summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="sv">Chatta på Matrix</summary> <summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
<summary xml:lang="ta">மேட்ரிக்ஸுக்கான உரையாடல் செயலி</summary> <summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="tr">Matrix Üzerinde Sohbet</summary> <summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="uk">Спілкування у Matrix</summary> <summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="x-test">xxChat on Matrixxx</summary> <summary xml:lang="tr">Matrixte arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="zh-TW">在 Matrix 上聊天</summary> <summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
<summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary>
<description> <description>
<p>NeoChat 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.</p> <p>NeoChat 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.</p>
<p xml:lang="ar">نيوتشات هو تطبيق دردشة يتيح لك الاستفادة الكاملة من شبكة Matrix. فهو يوفر لك طريقة آمنة لإرسال الرسائل النصية ومقاطع الفيديو والملفات الصوتية إلى عائلتك وزملائك وأصدقائك.</p> <p xml:lang="ar">نيوتشات هو تطبيق دردشة يتيح لك الاستفادة الكاملة من شبكة Matrix. فهو يوفر لك طريقة آمنة لإرسال الرسائل النصية ومقاطع الفيديو والملفات الصوتية إلى عائلتك وزملائك وأصدقائك.</p>
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p> <p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p> <p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="de">NeoChat ist eine Anwendung für Unterhaltungen mit allen Vorteilen des Matrix-Netzwerkes. Sie bietet eine sichere Möglichkeit zum Versenden von Nachrichten, Videos und Audiodateien and die Familienmitglieder.</p>
<p xml:lang="el">Το NeoChat είναι μια εφαρμογή συνομιλίας που σας επιτρέπει να εκμεταλλευτείτε πλήρως το δίκτυο Matrix. Σας παρέχει έναν ασφαλή τρόπο να στέλνετε μηνύματα κειμένου, βίντεο και αρχεία ήχου στην οικογένεια, τους συναδέλφους και τους φίλους σας.</p>
<p xml:lang="en-GB">NeoChat 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.</p> <p xml:lang="en-GB">NeoChat 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.</p>
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p> <p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p> <p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
@@ -100,7 +101,6 @@
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p> <p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p> <p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p> <p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="ru">NeoChat — приложение для общения, предоставляющее все преимущества сети Matrix. С его помощью можно безопасно отправлять текстовые сообщения, видеозаписи и звуковые файлы родственникам, коллегам и друзьям.</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p> <p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
<p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p> <p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p>
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p> <p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
@@ -111,15 +111,13 @@
<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">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="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>
<p xml:lang="el">Το NeoChat στοχεύει να είναι μια πλήρως εξοπλισμένη εφαρμογή για τις προδιαγραφές Matrix. Ως εκ τούτου, υποστηρίζονται όλα τα στοιχεία της τρέχουσας σταθερής προδιαγραφής με τις αξιοσημείωτες εξαιρέσεις του VoIP, των νημάτων και ορισμένων πτυχών της κρυπτογράφησης στα άκρα. Υπάρχουν μερικές άλλες μικρότερες παραλείψεις που οφείλονται στο γεγονός ότι η προδιαγραφή Matrix εξελίσσεται συνεχώς, αλλά ο στόχος παραμένει να παρέχεται τελικά υποστήριξη για ολόκληρη την προδιαγραφή.</p>
<p xml:lang="en-GB">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="en-GB">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="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p> <p xml:lang="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p> <p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
<p xml:lang="eu">«NeoChat»ek «Matrix» zehaztapenaren ezaugarri guztiak eskaintzen dituen aplikazio bat izan nahi du. Beraz, egungo zehaztapen egonkorrean dagoen guztiaren euskarria du, VoIP, hariak eta muturren artean zifratzeko salbuespen nabarmenekin. Badira beste ez-betetze txikiago batzuk, «Matrix»en zehaztapena etengabe eboluzioan dagoelako, baina azken helburua zehaztapen osoaren euskarria ematea izaten jarraitzen du.</p> <p xml:lang="eu">«NeoChat»ek «Matrix» zehaztapenaren ezaugarri guztiak eskaintzen dituen aplikazio bat izan nahi du. Beraz, egungo zehaztapen egonkorrean dagoen guztiaren euskarria du, VoIP, hariak eta muturren artean zifratzeko salbuespen nabarmenekin. Badira beste ez-betetze txikiago batzuk, «Matrix»en zehaztapena etengabe eboluzioan dagoelako, baina azken helburua zehaztapen osoaren euskarria ematea izaten jarraitzen du.</p>
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p> <p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p> <p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é fornecer compatibilidade coa especificación completa.</p> <p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="he">NeoChat מתיימר להיות יישום עתיר יכולות לפי מפרט Matrix. כיוון שזה ייעודו, כל מה שבמפרט היציב עם חריגות משמעותיות כגון VoIP, שרשורים ועוד מגוון היבטים של הצפנה מקצה לקצה נתמכים גם הם. יש מספר השמטות קטן עקב העובדה שהמפרט של Matrix ממשיך להתפתח אך המטרה היא להמשיך לספק תמיכה בסופו של דבר לכל המפרט.</p> <p xml:lang="he">NeoChat מתיימר להיות יישום עתיר יכולות לפי מפרט Matrix. כיוון שזה ייעודו, כל מה שבמפרט היציב עם חריגות משמעותיות כגון VoIP, שרשורים ועוד מגוון היבטים של הצפנה מקצה לקצה נתמכים גם הם. יש מספר השמטות קטן עקב העובדה שהמפרט של Matrix ממשיך להתפתח אך המטרה היא להמשיך לספק תמיכה בסופו של דבר לכל המפרט.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p> <p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenmente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p> <p xml:lang="ia">NeoChat aspira a esser un application plenmente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
@@ -142,8 +140,6 @@
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p> <p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p> <p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p> <p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="de">Durch die Weiterentwicklung der Matrix-Spezifikation unterstützt auch NeoChat einige als noch instabil gekennzeichnete Funktionen. Derzeit sind das:</p>
<p xml:lang="el">Λόγω της φύσης της ανάπτυξης των προδιαγραφών Matrix, το NeoChat υποστηρίζει επίσης πολλά ασταθή χαρακτηριστικά. Επί του παρόντος, αυτά είναι:</p>
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p> <p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
<p xml:lang="eo">Pro la naturo de la Matrix-specifevoluo NeoChat ankaŭ subtenas multajn malstabilajn funkciojn. Nuntempe ĉi tiuj estas:</p> <p xml:lang="eo">Pro la naturo de la Matrix-specifevoluo NeoChat ankaŭ subtenas multajn malstabilajn funkciojn. Nuntempe ĉi tiuj estas:</p>
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p> <p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
@@ -175,7 +171,6 @@
<li xml:lang="ar">التصويت - MSC3381</li> <li xml:lang="ar">التصويت - MSC3381</li>
<li xml:lang="ca">Enquestes - MSC3381</li> <li xml:lang="ca">Enquestes - MSC3381</li>
<li xml:lang="ca-valencia">Enquestes - MSC3381</li> <li xml:lang="ca-valencia">Enquestes - MSC3381</li>
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li> <li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="eo">Enketoj - MSC3381</li> <li xml:lang="eo">Enketoj - MSC3381</li>
<li xml:lang="es">Encuestas - MSC3381</li> <li xml:lang="es">Encuestas - MSC3381</li>
@@ -194,7 +189,6 @@
<li xml:lang="nn">Avstemmingar  MSC3381</li> <li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li> <li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li> <li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="ru">Голосования — MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li> <li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li> <li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li> <li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
@@ -206,7 +200,6 @@
<li xml:lang="ar">حزم الملصقات - MSC2545</li> <li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li> <li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li> <li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
<li xml:lang="en-GB">Sticker Packs - MSC2545</li> <li xml:lang="en-GB">Sticker Packs - MSC2545</li>
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li> <li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li> <li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
@@ -237,7 +230,6 @@
<li xml:lang="ar">موقع الأحداث - MSC3488</li> <li xml:lang="ar">موقع الأحداث - MSC3488</li>
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li> <li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li> <li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="el">Τοποθεσία γεγονότα - MSC3488</li>
<li xml:lang="en-GB">Location Events - MSC3488</li> <li xml:lang="en-GB">Location Events - MSC3488</li>
<li xml:lang="eo">Lokaj Eventoj - MSC3488</li> <li xml:lang="eo">Lokaj Eventoj - MSC3488</li>
<li xml:lang="es">Eventos de ubicación - MSC3488</li> <li xml:lang="es">Eventos de ubicación - MSC3488</li>
@@ -302,8 +294,6 @@
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption> <caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption> <caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption> <caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="de">Hauptansicht mit Raumliste, Unterhaltung und Raum-Informationen</caption>
<caption xml:lang="el">Κύρια προβολή με λίστα δωματίων, συνομιλία και πληροφορίες δωματίων</caption>
<caption xml:lang="en-GB">Main view with room list, chat, and room information</caption> <caption xml:lang="en-GB">Main view with room list, chat, and room information</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption> <caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption> <caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
@@ -337,8 +327,6 @@
<caption xml:lang="ar">اكتشف مجتمعات جديدة مع فضاءات ماتركس</caption> <caption xml:lang="ar">اكتشف مجتمعات جديدة مع فضاءات ماتركس</caption>
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption> <caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption> <caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="de">Neue Gemeinschaften mit den Umgebungen von Matrix erkunden</caption>
<caption xml:lang="el">Ανακαλύψτε νέες κοινότητες με το Matrix Spaces</caption>
<caption xml:lang="en-GB">Discover new communities with Matrix Spaces</caption> <caption xml:lang="en-GB">Discover new communities with Matrix Spaces</caption>
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption> <caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption> <caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
@@ -355,10 +343,8 @@
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption> <caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption> <caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption> <caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="ru">Поиск новых сообществ с помощью Matrix Spaces</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption> <caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
<caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption> <caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption>
<caption xml:lang="ta">மேட்ரிக்ஸு இடங்களின் மூலம் புதிய சமூகங்களைக் கண்டுபிடிக்கலாம்</caption>
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption> <caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption> <caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption> <caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
@@ -377,8 +363,6 @@
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption> <caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption> <caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption> <caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="de">Hauptansicht mit Raumliste, Unterhaltung und Raum-Informationen</caption>
<caption xml:lang="el">Κύρια προβολή με λίστα δωματίων, συνομιλία και πληροφορίες δωματίων</caption>
<caption xml:lang="en-GB">Main view with room list, chat, and room information</caption> <caption xml:lang="en-GB">Main view with room list, chat, and room information</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption> <caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption> <caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
@@ -413,8 +397,6 @@
<caption xml:lang="ca">Pantalla d'inici de sessió</caption> <caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption> <caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="cs">Přihlašovací obrazovka</caption> <caption xml:lang="cs">Přihlašovací obrazovka</caption>
<caption xml:lang="de">Anmeldebildschirm</caption>
<caption xml:lang="el">Οθόνη εισόδου</caption>
<caption xml:lang="en-GB">Login screen</caption> <caption xml:lang="en-GB">Login screen</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption> <caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption> <caption xml:lang="es">Pantalla de inicio de sesión</caption>
@@ -447,12 +429,6 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="24.12.3" date="2025-03-06"/>
<release version="24.12.2" date="2025-02-06"/>
<release version="24.12.1" date="2025-01-09"/>
<release version="24.12.0" date="2024-12-12"/>
<release version="24.08.3" date="2024-11-07"/>
<release version="24.08.2" date="2024-10-10"/>
<release version="24.08.1" date="2024-09-12"/> <release version="24.08.1" date="2024-09-12"/>
<release version="24.08.0" date="2024-08-22"/> <release version="24.08.0" date="2024-08-22"/>
<release version="24.05.2" date="2024-07-04"/> <release version="24.05.2" date="2024-07-04"/>

View File

@@ -87,33 +87,47 @@ GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端 GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端 GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix Comment=Client for the Matrix protocol
Comment[ar]=دردش على ماتركس Comment[ar]=عميل لميفاق ماتركس
Comment[ca]=Xat a Matrix Comment[az]=Matrix protokolu üçün müştəri
Comment[ca@valencia]=Xat a Matrix Comment[ca]=Client per al protocol Matrix
Comment[de]=Über Matrix unterhalten Comment[ca@valencia]=Client per al protocol Matrix
Comment[en_GB]=Chat on Matrix Comment[de]=Programm für das Matrix-Protokoll
Comment[eo]=Babilo en Matrix Comment[el]=Πελάτης για το πρωτόκολλο Matrix
Comment[es]=Chat en Matrix Comment[en_GB]=Client for the Matrix protocol
Comment[eu]=Berriketa Matrix-en Comment[eo]=Kliento por la Matrix-protokolo
Comment[fi]=Keskustele Matrixissä Comment[es]=Cliente para el protocolo Matrix
Comment[fr]=Clavarder sur Matrix Comment[eu]=Matrix protokolorako bezeroa
Comment[gl]=Charle en Matrix Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[he]=התכתבות דרך Matrix Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Csevegés Matrixon Comment[gl]=Cliente para o protocolo Matrix.
Comment[ia]=Conversation en ditecto sur Matrix Comment[he]=לקוח לפרוטוקול Matrix
Comment[it]= su Matrix Comment[hu]=Kliens a Matrix protokollhoz
Comment[ka]=საუბარი Matrix-ზე Comment[ia]=Cliente per le protocollo de Matrix
Comment[nl]=Chat op Matrix Comment[id]=Klien untuk protokol Matrix
Comment[pl]=Rozmawiaj na Matriksie Comment[ie]=Un cliente del protocol Matrix
Comment[pt_BR]=Bate papo na Matrix Comment[it]=Client per il protocollo Matrix
Comment[sl]=Klepet na Matrixu Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[sv]=Chatta på Matrix Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும் Comment[lt]=Matrix protokolo kliento programa
Comment[tr]=Matrix üzerinde sohbet edin Comment[lv]=Klients „Matrix“ protokolam
Comment[uk]=Спілкування у Matrix Comment[nl]=Client voor het Matrix-protocol
Comment[zh_CN]=在 Matrix 上聊天 Comment[nn]=Klient for Matrix-protokollen
Comment[zh_TW]=在 Matrix 上聊天 Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[ru]=Клиент для протокола Matrix
Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix
Comment[ta]=Matrix நெறிமுறைக்கான வாங்கி
Comment[tr]=Matrix protokolü için istemci
Comment[uk]=Клієнт протоколу Matrix
Comment[x-test]=xxClient for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的客户端
Comment[zh_TW]=Matrix 通訊協定的用戶端
MimeType=x-scheme-handler/matrix; MimeType=x-scheme-handler/matrix;
Exec=neochat %u Exec=neochat %u
Terminal=false Terminal=false

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

@@ -1,122 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Slovenian "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Uporabniški priročnik za NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>Stran z navodili za NeoChat.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01.11.2022</date>
<releaseinfo
>22,09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Odjemalec za interakcijo s protokolom za matrično sporočanje</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Opis</title>
<para
><command
>neochat</command
> je aplikacija za klepet za matrični protokol, ki deluje na namizju in mobilni napravi. </para>
</refsect1>
<refsect1 id="options"
><title
>Možnosti</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>Uri matrike za uporabnika ali sobo. npr. matrix:u/user:example.org in matrix:r/root:example.org. Tako bo NeoChat poskušal odpreti dano sobo ali pogovor. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Poročanje o napakah</title>
<para
>Napake in zahteve po funkcijah lahko prijavite na <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi? product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Poglej tudi</title>
<simplelist>
<member
>Seznam pogostih vprašanj o Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Avtorske pravice</title>
<para
>Avtorske pravice &copy; 2020-2022 Tobias Fella </para>
<para
>Avtorske pravice &copy; 2020-2022 Carl Schwan </para>
<para
>Licenca: GNU General Public različica 3 ali novejša &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0 .html</ulink
>&gt;</para>
</refsect1>
</refentry>

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

@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

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

@@ -1,183 +0,0 @@
# SPDX-FileCopyrightText: 2024 Scarlett Moore <sgmoore@kde.org>
#
# SPDX-License-Identifier: CC0-1.0
---
name: neochat
base: core24
adopt-info: neochat
grade: stable
confinement: strict
apps:
neochat:
extensions:
- kde-neon-6
command: usr/bin/neochat
common-id: org.kde.neochat
desktop: usr/share/applications/org.kde.neochat.desktop
plugs:
- home
- removable-media
- audio-playback
- unity7
- network
- network-bind
- network-manager-observe
- password-manager-service
- accounts-service
compression: lzo
package-repositories:
- type: apt
ppa: ubuntu-toolchain-r/test
slots:
session-dbus-interface:
interface: dbus
name: org.kde.neochat
bus: session
parts:
olm:
source: https://gitlab.matrix.org/matrix-org/olm.git
source-depth: 1
source-tag: '3.2.12'
plugin: cmake
cmake-parameters:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/usr
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libsecret:
source: https://gitlab.gnome.org/GNOME/libsecret.git
source-tag: '0.21.4'
source-depth: 1
plugin: meson
meson-parameters:
- --prefix=/usr
- -Doptimization=3
- -Ddebug=true
- -Dmanpage=false
- -Dvapi=false
- -Dintrospection=false
- -Dcrypto=disabled
- -Dgtk_doc=false
build-packages:
- meson
- libglib2.0-dev
- libgcrypt20-dev
prime:
- -usr/include
- -usr/lib/*/pkgconfig
qtkeychain:
after: [libsecret]
source: https://github.com/frankosterfeld/qtkeychain.git
source-tag: 0.14.3
source-depth: 1
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PKG_CONFIG_PATH: $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TRANSLATIONS=NO
- -DBUILD_WITH_QT6=ON
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libquotient:
after:
- olm
- qtkeychain
source: https://github.com/quotient-im/libQuotient.git
source-tag: 0.9.1
source-depth: 1
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
build-snaps:
- cmake
build-packages:
- libssl-dev
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
- -DQuotient_ENABLE_E2EE=ON
- -DBUILD_WITH_QT6=ON
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
kquickimageeditor:
source: https://invent.kde.org/libraries/kquickimageeditor.git
source-tag: 'v0.3.0'
source-depth: 1
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_WITH_QT6=ON
- -DBUILD_TESTING=OFF
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
neochat:
after:
- qtkeychain
- libquotient
- kquickimageeditor
parse-info:
- usr/share/metainfo/org.kde.neochat.appdata.xml
source: .
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PYTHONPATH: ${CRAFT_STAGE}/lib/python3.12/site-packages:${CRAFT_STAGE}/usr/lib/python3/dist-packages
- LD_LIBRARY_PATH: "/snap/mesa-2404/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/kde-qt6-core24-sdk/current/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$LD_LIBRARY_PATH"
build-packages:
- cmark
- libcmark-dev
- libsqlite3-dev
- libvulkan-dev
- libxkbcommon-dev
- libicu-dev
- libpulse0
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
prime:
- -usr/share/man
deps:
after: [neochat]
plugin: nil
stage-packages:
- libcmark0.30.2
prime:
- usr/lib/*/libcmark.so*
gpu-2404:
after: [neochat]
source: https://github.com/canonical/gpu-snap.git
plugin: dump
override-prime: |
craftctl default
${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
prime:
- bin/gpu-2404-wrapper

View File

@@ -10,6 +10,8 @@ endif()
add_library(neochat STATIC add_library(neochat STATIC
controller.cpp controller.cpp
controller.h controller.h
actionshandler.cpp
actionshandler.h
models/emojimodel.cpp models/emojimodel.cpp
models/emojimodel.h models/emojimodel.h
emojitones.cpp emojitones.cpp
@@ -192,20 +194,12 @@ add_library(neochat STATIC
neochatroommember.h neochatroommember.h
models/threadmodel.cpp models/threadmodel.cpp
models/threadmodel.h models/threadmodel.h
enums/messagetype.h
messagecomponent.h
) )
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE QT_QML_SINGLETON_TYPE TRUE
) )
if(ANDROID OR WIN32)
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
QT_QML_SOURCE_TYPENAME ShareAction
)
endif()
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
QML_FILES QML_FILES
@@ -244,8 +238,11 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EmojiSas.qml qml/EmojiSas.qml
qml/ConfirmDeactivateAccountDialog.qml qml/ConfirmDeactivateAccountDialog.qml
qml/VerificationCanceled.qml qml/VerificationCanceled.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml qml/FileDelegateContextMenu.qml
qml/FileDelegateContextMenuMobile.qml
qml/MessageSourceSheet.qml qml/MessageSourceSheet.qml
qml/ConfirmEncryptionDialog.qml qml/ConfirmEncryptionDialog.qml
qml/RoomSearchPage.qml qml/RoomSearchPage.qml
@@ -311,15 +308,15 @@ add_subdirectory(login)
add_subdirectory(chatbar) add_subdirectory(chatbar)
if(NOT ANDROID AND NOT WIN32) if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
qml/ShareAction.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
)
else() else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
QT_RESOURCE_ALIAS qml/ShareAction.qml
)
qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml) qt_target_qml_sources(neochat QML_FILES qml/ShareActionStub.qml)
endif() endif()
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h) configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
if(WIN32) if(WIN32)
@@ -389,7 +386,7 @@ if(NOT ANDROID)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU) target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif() endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU) if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER) target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11=1) target_compile_definitions(neochat PUBLIC -DHAVE_X11=1)
target_sources(neochat PRIVATE runner.cpp) target_sources(neochat PRIVATE runner.cpp)
@@ -418,7 +415,6 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui KF6::ConfigGui
KF6::CoreAddons KF6::CoreAddons
KF6::SonnetCore KF6::SonnetCore
KF6::IconThemes
KF6::ColorScheme KF6::ColorScheme
KF6::ItemModels KF6::ItemModels
QuotientQt6 QuotientQt6
@@ -446,11 +442,8 @@ if(ANDROID)
target_sources(neochat-app PRIVATE notifyrc.qrc) target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL) target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS kirigami_package_breeze_icons(ICONS
"arrow-down-symbolic" "arrow-down"
"arrow-up-symbolic" "arrow-up"
"arrow-up-double-symbolic"
"arrow-left-symbolic"
"arrow-right-symbolic"
"checkmark" "checkmark"
"help-about" "help-about"
"im-user" "im-user"
@@ -459,7 +452,6 @@ if(ANDROID)
"mail-attachment" "mail-attachment"
"dialog-cancel" "dialog-cancel"
"preferences-desktop-emoticons" "preferences-desktop-emoticons"
"preferences-security"
"document-open" "document-open"
"document-save" "document-save"
"document-send" "document-send"
@@ -492,7 +484,6 @@ if(ANDROID)
"network-connect" "network-connect"
"list-remove-user" "list-remove-user"
"org.kde.neochat" "org.kde.neochat"
"org.kde.neochat.tray"
"preferences-system-users" "preferences-system-users"
"preferences-desktop-theme-global" "preferences-desktop-theme-global"
"notifications" "notifications"
@@ -530,13 +521,11 @@ if(ANDROID)
"object-rotate-left" "object-rotate-left"
"object-rotate-right" "object-rotate-right"
"add-subtitle" "add-subtitle"
"security-high"
"security-low" "security-low"
"security-low-symbolic" "security-low-symbolic"
"kde" "kde"
"list-remove-symbolic" "list-remove-symbolic"
"edit-delete" "edit-delete"
"user-home-symbolic"
) )
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android) ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else() else()

170
src/actionshandler.cpp Normal file
View File

@@ -0,0 +1,170 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "actionshandler.h"
#include <Quotient/csapi/joining.h>
#include <Quotient/events/roommemberevent.h>
#include <cmark.h>
#include <KLocalizedString>
#include <QStringBuilder>
#include "models/actionsmodel.h"
#include "neochatconfig.h"
#include "texthandler.h"
using namespace Quotient;
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
{
}
NeoChatRoom *ActionsHandler::room() const
{
return m_room;
}
void ActionsHandler::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
{
if (!m_room || !chatBarCache) {
qWarning() << "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.";
return;
}
checkEffects(chatBarCache->text());
if (!chatBarCache->attachmentPath().isEmpty()) {
QUrl url(chatBarCache->attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
m_room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text());
chatBarCache->setAttachmentPath({});
chatBarCache->setText({});
return;
}
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(chatBarCache->text(), handledText, chatBarCache);
}
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
{
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
for (const auto &mention : *mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
handledText = handledText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
mentions->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
{
Q_ASSERT(m_room);
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
auto match = sed.match(text);
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
const QString flags = match.captured(3);
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == m_room->localMember().id() && event->hasTextContent()) {
QString originalString;
if (event->content()) {
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
} else {
originalString = event->plainBody();
}
if (flags == "/g"_ls) {
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
} else {
m_room->postHtmlMessage(handledText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
event->id());
}
return;
}
}
}
}
}
auto messageType = RoomMessageEvent::MsgType::Text;
if (handledText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (handledText.indexOf(action.prefix) == 1
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
if (action.messageAction) {
break;
} else {
return;
}
}
}
}
TextHandler textHandler;
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
if (handledText.count("<p>"_ls) == 1 && handledText.count("</p>"_ls) == 1) {
handledText.remove("<p>"_ls);
handledText.remove("</p>"_ls);
}
if (handledText.length() == 0) {
return;
}
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
void ActionsHandler::checkEffects(const QString &text)
{
std::optional<QString> effect = std::nullopt;
if (text.contains(QStringLiteral("\u2744"))) {
effect = QLatin1String("snowflake");
} else if (text.contains(QStringLiteral("\u1F386"))) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u2F387"))) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u1F389"))) {
effect = QLatin1String("confetti");
} else if (text.contains(QStringLiteral("\u1F38A"))) {
effect = QLatin1String("confetti");
}
if (effect.has_value()) {
Q_EMIT showEffect(*effect);
}
}
#include "moc_actionshandler.cpp"

66
src/actionshandler.h Normal file
View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/roommessageevent.h>
#include "chatbarcache.h"
#include "neochatroom.h"
class NeoChatRoom;
/**
* @class ActionsHandler
*
* This class handles chat messages ready for posting to a room.
*
* Everything that needs to be done to prepare the message for posting in a room
* including:
* - File handling
* - User mentions
* - Quick edits
* - Chat actions
* - Custom emojis
*
* @note A chat action is a message starting with /, resulting in something other
* than a normal message being sent (e.g. /me, /join).
*
* @sa ActionsModel, NeoChatRoom
*/
class ActionsHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room that messages will be sent to.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
explicit ActionsHandler(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
void roomChanged();
void showEffect(const QString &effect);
public Q_SLOTS:
/**
* @brief Pre-process text and send message event.
*/
void handleMessageEvent(ChatBarCache *chatBarCache);
private:
QPointer<NeoChatRoom> m_room;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, QList<Mention> *mentions);
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
};

View File

@@ -11,6 +11,7 @@ ecm_add_qml_module(chatbar GENERATE_PLUGIN_SOURCE
CompletionMenu.qml CompletionMenu.qml
EmojiDelegate.qml EmojiDelegate.qml
EmojiGrid.qml EmojiGrid.qml
ReplyPane.qml
PieProgressBar.qml PieProgressBar.qml
EmojiPicker.qml EmojiPicker.qml
EmojiDialog.qml EmojiDialog.qml

View File

@@ -53,6 +53,14 @@ QQC2.Control {
} }
} }
/**
* @brief The ActionsHandler object to use.
*
* This is expected to have the correct room set otherwise messages will be sent
* to the wrong room.
*/
required property ActionsHandler actionsHandler
/** /**
* @brief The list of actions in the ChatBar. * @brief The list of actions in the ChatBar.
* *
@@ -167,7 +175,6 @@ QQC2.Control {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing Layout.margins: Kirigami.Units.largeSpacing
Layout.preferredHeight: active ? item.implicitHeight : 0
active: visible active: visible
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0 visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
@@ -176,14 +183,13 @@ QQC2.Control {
RowLayout { RowLayout {
QQC2.ScrollView { QQC2.ScrollView {
id: chatBarScrollView id: chatBarScrollView
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8 Layout.maximumHeight: Kirigami.Units.gridUnit * 8
Layout.minimumHeight: Kirigami.Units.gridUnit * 3
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased // HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
@@ -252,22 +258,20 @@ QQC2.Control {
} }
} }
Keys.onEnterPressed: event => { Keys.onEnterPressed: event => {
const controlIsPressed = event.modifiers & Qt.ControlModifier;
if (completionMenu.visible) { if (completionMenu.visible) {
completionMenu.complete(); completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) { } else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n"); textField.insert(cursorPosition, "\n");
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) { } else {
_private.postMessage(); _private.postMessage();
} }
} }
Keys.onReturnPressed: event => { Keys.onReturnPressed: event => {
const controlIsPressed = event.modifiers & Qt.ControlModifier;
if (completionMenu.visible) { if (completionMenu.visible) {
completionMenu.complete(); completionMenu.complete();
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile || NeoChatConfig.sendMessageWith === 1 && !controlIsPressed || NeoChatConfig.sendMessageWith === 0 && controlIsPressed) { } else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n"); textField.insert(cursorPosition, "\n");
} else if (NeoChatConfig.sendMessageWith === 0 && !controlIsPressed || NeoChatConfig.sendMessageWith === 1 && controlIsPressed) { } else {
_private.postMessage(); _private.postMessage();
} }
} }
@@ -321,11 +325,12 @@ QQC2.Control {
id: actionsRow id: actionsRow
spacing: 0 spacing: 0
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4 Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
Repeater { Repeater {
model: root.actions model: root.actions
delegate: QQC2.ToolButton { delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source) icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger() onClicked: modelData.trigger()
@@ -342,6 +347,7 @@ QQC2.Control {
} }
} }
} }
DelegateSizeHelper { DelegateSizeHelper {
id: chatBarSizeHelper id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46 startBreakpoint: Kirigami.Units.gridUnit * 46
@@ -355,32 +361,15 @@ QQC2.Control {
Component { Component {
id: replyPane id: replyPane
Item { ReplyPane {
implicitWidth: replyComponent.implicitWidth userName: _private.chatBarCache.relationUser.displayName
implicitHeight: replyComponent.implicitHeight userColor: _private.chatBarCache.relationUser.color
ReplyComponent { userAvatar: _private.chatBarCache.relationUser.avatarUrl
id: replyComponent text: _private.chatBarCache.relationMessage
replyEventId: _private.chatBarCache.replyId
replyAuthor: _private.chatBarCache.relationAuthor
replyContentModel: _private.chatBarCache.relationEventContentModel
maxContentWidth: paneLoader.item.width
}
QQC2.Button {
id: cancelButton
anchors.top: parent.top onCancel: {
anchors.right: parent.right _private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
_private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
} }
} }
} }
@@ -402,10 +391,11 @@ QQC2.Control {
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
function postMessage() { function postMessage() {
_private.chatBarCache.postMessage(); root.actionsHandler.handleMessageEvent(_private.chatBarCache);
repeatTimer.stop(); repeatTimer.stop();
root.currentRoom.markAllMessagesAsRead(); root.currentRoom.markAllMessagesAsRead();
textField.clear(); textField.clear();
_private.chatBarCache.clearRelations();
messageSent(); messageSent();
} }

View File

@@ -33,7 +33,7 @@ QQC2.ItemDelegate {
Kirigami.Icon { Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5 width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5 height: Kirigami.Units.gridUnit * 0.5
source: "arrow-down-symbolic" source: "arrow-down"
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
visible: root.showTones visible: root.showTones
@@ -43,9 +43,6 @@ QQC2.ItemDelegate {
anchors.fill: parent anchors.fill: parent
visible: root.emoji.startsWith("mxc") || root.isImage visible: root.emoji.startsWith("mxc") || root.isImage
source: visible ? root.emoji : "" source: visible ? root.emoji : ""
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
} }
} }

View File

@@ -84,7 +84,6 @@ QQC2.ScrollView {
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
anchors.centerIn: parent anchors.centerIn: parent
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
text: root.stickers ? i18n("No stickers") : i18n("No emojis") text: root.stickers ? i18n("No stickers") : i18n("No emojis")
visible: emojis.count === 0 visible: emojis.count === 0
} }

View File

@@ -66,7 +66,6 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0 QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
visible: categories.count !== 0
ListView { ListView {
id: categories id: categories
@@ -202,13 +201,8 @@ ColumnLayout {
width: root.categoryIconSize width: root.categoryIconSize
height: width height: width
checked: stickerModel.packIndex === model.index checked: stickerModel.packIndex === model.index
padding: Kirigami.Units.largeSpacing
contentItem: Image { contentItem: Image {
source: model.avatarUrl source: model.avatarUrl
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
} }
QQC2.ToolTip.text: model.name QQC2.ToolTip.text: model.name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay

98
src/chatbar/ReplyPane.qml Normal file
View File

@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
RowLayout {
id: root
property string userName
property color userColor
property url userAvatar: ""
property var text
signal cancel
Rectangle {
id: verticalBorder
Layout.fillHeight: true
implicitWidth: Kirigami.Units.smallSpacing
color: userColor
}
ColumnLayout {
RowLayout {
KirigamiComponents.Avatar {
id: replyAvatar
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
source: userAvatar
name: userName
color: userColor
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
color: userColor
text: userName
elide: Text.ElideRight
}
}
QQC2.TextArea {
id: textArea
Layout.fillWidth: true
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
selectByMouse: true
selectByKeyboard: true
readOnly: true
wrapMode: TextEdit.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TextMetrics {
id: replyTextMetrics
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
}
}
}
QQC2.ToolButton {
id: cancelButton
Layout.alignment: Qt.AlignVCenter
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
root.cancel();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}

View File

@@ -7,9 +7,7 @@
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "models/actionsmodel.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "texthandler.h"
ChatBarCache::ChatBarCache(QObject *parent) ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -30,37 +28,6 @@ void ChatBarCache::setText(const QString &text)
Q_EMIT textChanged(); Q_EMIT textChanged();
} }
QString ChatBarCache::sendText() const
{
if (!attachmentPath().isEmpty()) {
QUrl url(attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
return text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : text();
}
return formatMentions();
}
QString ChatBarCache::formatMentions() const
{
auto mentions = m_mentions;
std::sort(mentions.begin(), mentions.end(), [](const auto &a, const auto &b) {
return a.cursor.anchor() > b.cursor.anchor();
});
auto formattedText = text();
for (const auto &mention : mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
formattedText = formattedText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
return formattedText;
}
bool ChatBarCache::isReplying() const bool ChatBarCache::isReplying() const
{ {
return m_relationType == Reply && !m_relationId.isEmpty(); return m_relationType == Reply && !m_relationId.isEmpty();
@@ -86,7 +53,6 @@ void ChatBarCache::setReplyId(const QString &replyId)
m_relationType = Reply; m_relationType = Reply;
} }
m_attachmentPath = QString(); m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId); Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
} }
@@ -116,12 +82,11 @@ void ChatBarCache::setEditId(const QString &editId)
m_relationType = Edit; m_relationType = Edit;
} }
m_attachmentPath = QString(); m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId); Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
} }
Quotient::RoomMember ChatBarCache::relationAuthor() const Quotient::RoomMember ChatBarCache::relationUser() const
{ {
if (parent() == nullptr) { if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation."; qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
@@ -159,28 +124,6 @@ QString ChatBarCache::relationMessage() const
return {}; return {};
} }
MessageContentModel *ChatBarCache::relationEventContentModel()
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return nullptr;
}
if (m_relationId.isEmpty()) {
return nullptr;
}
if (m_relationContentModel != nullptr) {
return m_relationContentModel;
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return nullptr;
}
m_relationContentModel = new MessageContentModel(room, m_relationId, true);
return m_relationContentModel;
}
bool ChatBarCache::isThreaded() const bool ChatBarCache::isThreaded() const
{ {
return !m_threadId.isEmpty(); return !m_threadId.isEmpty();
@@ -213,7 +156,6 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
m_attachmentPath = attachmentPath; m_attachmentPath = attachmentPath;
m_relationType = None; m_relationType = None;
const auto oldEventId = std::exchange(m_relationId, QString()); const auto oldEventId = std::exchange(m_relationId, QString());
delete m_relationContentModel;
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged(oldEventId, m_relationId); Q_EMIT relationIdChanged(oldEventId, m_relationId);
} }
@@ -223,7 +165,6 @@ void ChatBarCache::clearRelations()
const auto oldEventId = std::exchange(m_relationId, QString()); const auto oldEventId = std::exchange(m_relationId, QString());
const auto oldThreadId = std::exchange(m_threadId, QString()); const auto oldThreadId = std::exchange(m_threadId, QString());
m_attachmentPath = QString(); m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId); Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId); Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT attachmentPathChanged(); Q_EMIT attachmentPathChanged();
@@ -292,44 +233,4 @@ void ChatBarCache::setSavedText(const QString &savedText)
m_savedText = savedText; m_savedText = savedText;
} }
void ChatBarCache::postMessage()
{
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return;
}
if (!attachmentPath().isEmpty()) {
room->uploadFile(QUrl(attachmentPath()), sendText());
clearCache();
return;
}
const auto result = ActionsModel::handleAction(room, this);
if (!result.first.has_value()) {
return;
}
TextHandler textHandler;
textHandler.setData(*std::get<std::optional<QString>>(result));
const auto sendText = textHandler.handleSendText();
if (sendText.length() == 0) {
return;
}
auto type = std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result);
room->postMessage(text(), sendText, type ? *type : Quotient::RoomMessageEvent::MsgType::Text, replyId(), editId(), threadId());
clearCache();
}
void ChatBarCache::clearCache()
{
setText({});
m_mentions.clear();
m_savedText = QString();
clearRelations();
}
#include "moc_chatbarcache.cpp" #include "moc_chatbarcache.cpp"

View File

@@ -8,8 +8,6 @@
#include <QQuickTextDocument> #include <QQuickTextDocument>
#include <QTextCursor> #include <QTextCursor>
#include "models/messagecontentmodel.h"
class ChatDocumentHandler; class ChatDocumentHandler;
namespace Quotient namespace Quotient
@@ -102,7 +100,7 @@ class ChatBarCache : public QObject
* *
* @sa Quotient::RoomMember * @sa Quotient::RoomMember
*/ */
Q_PROPERTY(Quotient::RoomMember relationAuthor READ relationAuthor NOTIFY relationIdChanged) Q_PROPERTY(Quotient::RoomMember relationUser READ relationUser NOTIFY relationIdChanged)
/** /**
* @brief The content of the related message. * @brief The content of the related message.
@@ -111,13 +109,6 @@ class ChatBarCache : public QObject
*/ */
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged) Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief The MessageContentModel for the related message.
*
* Will be nullptr if no related message.
*/
Q_PROPERTY(MessageContentModel *relationEventContentModel READ relationEventContentModel NOTIFY relationIdChanged)
/** /**
* @brief Whether the chat bar is replying in a thread. * @brief Whether the chat bar is replying in a thread.
*/ */
@@ -153,7 +144,6 @@ public:
explicit ChatBarCache(QObject *parent = nullptr); explicit ChatBarCache(QObject *parent = nullptr);
QString text() const; QString text() const;
QString sendText() const;
void setText(const QString &text); void setText(const QString &text);
bool isReplying() const; bool isReplying() const;
@@ -164,10 +154,9 @@ public:
QString editId() const; QString editId() const;
void setEditId(const QString &editId); void setEditId(const QString &editId);
Quotient::RoomMember relationAuthor() const; Quotient::RoomMember relationUser() const;
QString relationMessage() const; QString relationMessage() const;
MessageContentModel *relationEventContentModel();
bool isThreaded() const; bool isThreaded() const;
QString threadId() const; QString threadId() const;
@@ -203,11 +192,6 @@ public:
*/ */
void setSavedText(const QString &savedText); void setSavedText(const QString &savedText);
/**
* @brief Post the contents of the cache as a message in the room.
*/
Q_INVOKABLE void postMessage();
Q_SIGNALS: Q_SIGNALS:
void textChanged(); void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId); void relationIdChanged(const QString &oldEventId, const QString &newEventId);
@@ -216,16 +200,10 @@ Q_SIGNALS:
private: private:
QString m_text = QString(); QString m_text = QString();
QString formatMentions() const;
QString m_relationId = QString(); QString m_relationId = QString();
RelationType m_relationType = RelationType::None; RelationType m_relationType = RelationType::None;
QString m_threadId = QString(); QString m_threadId = QString();
QString m_attachmentPath = QString(); QString m_attachmentPath = QString();
QList<Mention> m_mentions; QList<Mention> m_mentions;
QString m_savedText; QString m_savedText;
QPointer<MessageContentModel> m_relationContentModel;
void clearCache();
}; };

View File

@@ -28,7 +28,8 @@ void ColorSchemer::apply(int idx)
int ColorSchemer::indexForCurrentScheme() int ColorSchemer::indexForCurrentScheme()
{ {
return c->indexForSchemeId(c->activeSchemeId()).row(); return -1;
// return c->indexForSchemeId(c->activeSchemeId()).row();
} }
#include "moc_colorschemer.cpp" #include "moc_colorschemer.cpp"

View File

@@ -18,7 +18,6 @@
#include <Quotient/settings.h> #include <Quotient/settings.h>
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "proxycontroller.h" #include "proxycontroller.h"
@@ -63,7 +62,11 @@ Controller::Controller(QObject *parent)
}); });
} else { } else {
auto c = new NeoChatConnection(this); auto c = new NeoChatConnection(this);
#if Quotient_VERSION_MINOR > 8
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234")); c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234"));
#else
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
#endif
connect(c, &Connection::connected, this, [c, this]() { connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c); m_accountRegistry.add(c);
c->syncLoop(); c->syncLoop();
@@ -165,9 +168,6 @@ void Controller::addConnection(NeoChatConnection *c)
dropConnection(c); dropConnection(c);
}); });
connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount); connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
connect(c, &NeoChatConnection::syncDone, this, [this, c]() {
m_notificationsManager.handleNotifications(c);
});
c->sync(); c->sync();
@@ -178,8 +178,6 @@ void Controller::dropConnection(NeoChatConnection *c)
{ {
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection"); Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
c->disconnect(this);
c->disconnect(&m_notificationsManager);
m_accountRegistry.drop(c); m_accountRegistry.drop(c);
Q_EMIT connectionDropped(c); Q_EMIT connectionDropped(c);
} }
@@ -225,7 +223,14 @@ void Controller::invokeLogin()
Qt::SingleShotConnection); Qt::SingleShotConnection);
} }
}); });
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
});
#if Quotient_VERSION_MINOR > 8
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken); connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
#else
connection->assumeIdentity(account.userId(), accessToken);
#endif
}); });
} }
} }
@@ -245,17 +250,17 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QStrin
switch (job->error()) { switch (job->error()) {
case QKeychain::EntryNotFound: case QKeychain::EntryNotFound:
Q_EMIT errorOccured(i18n("Access token wasn't found: Maybe it was deleted?")); Q_EMIT errorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
break; break;
case QKeychain::AccessDeniedByUser: case QKeychain::AccessDeniedByUser:
case QKeychain::AccessDenied: case QKeychain::AccessDenied:
Q_EMIT errorOccured(i18n("Access to keychain was denied: Please allow NeoChat to read the access token")); Q_EMIT errorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
break; break;
case QKeychain::NoBackendAvailable: case QKeychain::NoBackendAvailable:
Q_EMIT errorOccured(i18n("No keychain available: Please install a keychain, e.g. KWallet or GNOME keyring on Linux")); Q_EMIT errorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
break; break;
case QKeychain::OtherError: case QKeychain::OtherError:
Q_EMIT errorOccured(i18n("Unable to read access token: %1", job->errorString())); Q_EMIT errorOccured(i18n("Unable to read access token"), job->errorString());
break; break;
default: default:
break; break;
@@ -294,7 +299,7 @@ bool Controller::supportSystemTray() const
void Controller::setQuitOnLastWindowClosed() void Controller::setQuitOnLastWindowClosed()
{ {
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
if (supportSystemTray() && NeoChatConfig::self()->systemTray()) { if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this); m_trayIcon = new TrayIcon(this);
m_trayIcon->show(); m_trayIcon->show();
} else { } else {
@@ -320,18 +325,11 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
return; return;
} }
if (m_connection != nullptr) {
m_connection->disconnect(this);
m_connection->disconnect(&m_notificationsManager);
}
m_connection = connection; m_connection = connection;
if (m_connection != nullptr) { if (m_connection != nullptr) {
m_connection->refreshBadgeNotificationCount(); m_connection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(m_connection, m_connection->badgeNotificationCount()); updateBadgeNotificationCount(m_connection, m_connection->badgeNotificationCount());
connect(m_connection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
} }
Q_EMIT activeConnectionChanged(m_connection); Q_EMIT activeConnectionChanged(m_connection);
@@ -346,7 +344,7 @@ void Controller::listenForNotifications()
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit); connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) { connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
instance().m_notificationsManager.postPushNotification(data); NotificationsManager::instance().postPushNotification(data);
timer->stop(); timer->stop();
}); });
@@ -358,11 +356,6 @@ void Controller::listenForNotifications()
#endif #endif
} }
void Controller::clearInvitationNotification(const QString &roomId)
{
m_notificationsManager.clearInvitationNotification(roomId);
}
void Controller::updateBadgeNotificationCount(NeoChatConnection *connection, int count) void Controller::updateBadgeNotificationCount(NeoChatConnection *connection, int count)
{ {
if (connection == m_connection) { if (connection == m_connection) {
@@ -422,21 +415,21 @@ void Controller::setTestMode(bool test)
void Controller::removeConnection(const QString &userId) void Controller::removeConnection(const QString &userId)
{ {
// When loadAccessTokenFromKeyChain() fails m_connectionsLoading won't have an
// entry for it so we need to check both separately.
if (m_accountsLoading.contains(userId)) {
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
}
if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) { if (m_connectionsLoading.contains(userId) && m_connectionsLoading[userId]) {
auto connection = m_connectionsLoading[userId]; auto connection = m_connectionsLoading[userId];
m_accountsLoading.removeAll(userId);
Q_EMIT accountsLoadingChanged();
SettingsGroup("Accounts"_ls).remove(userId); SettingsGroup("Accounts"_ls).remove(userId);
} }
} }
bool Controller::csSupported() const bool Controller::csSupported() const
{ {
#if Quotient_VERSION_MINOR > 8
return true; return true;
#else
return false;
#endif
} }
void Controller::revertToDefaultConfig() void Controller::revertToDefaultConfig()

View File

@@ -7,7 +7,6 @@
#include <QQmlEngine> #include <QQmlEngine>
#include "neochatconnection.h" #include "neochatconnection.h"
#include "notificationsmanager.h"
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
class TrayIcon; class TrayIcon;
@@ -54,6 +53,16 @@ class Controller : public QObject
Q_PROPERTY(bool csSupported READ csSupported CONSTANT) Q_PROPERTY(bool csSupported READ csSupported CONSTANT)
public: public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
};
Q_ENUM(MessageType)
static Controller &instance(); static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *) static Controller *create(QQmlEngine *engine, QJSEngine *)
{ {
@@ -89,13 +98,6 @@ public:
*/ */
static void listenForNotifications(); static void listenForNotifications();
/**
* @brief Clear an existing invite notification for the given room.
*
* Nothing happens if the given room doesn't have an invite notification.
*/
Q_INVOKABLE void clearInvitationNotification(const QString &roomId);
Q_INVOKABLE QString loadFileContent(const QString &path) const; Q_INVOKABLE QString loadFileContent(const QString &path) const;
Quotient::AccountRegistry &accounts(); Quotient::AccountRegistry &accounts();
@@ -131,20 +133,16 @@ private:
QString m_endpoint; QString m_endpoint;
QStringList m_shownImages; QStringList m_shownImages;
NotificationsManager m_notificationsManager;
private Q_SLOTS: private Q_SLOTS:
void invokeLogin(); void invokeLogin();
void setQuitOnLastWindowClosed(); void setQuitOnLastWindowClosed();
void updateBadgeNotificationCount(NeoChatConnection *connection, int count); void updateBadgeNotificationCount(NeoChatConnection *connection, int count);
Q_SIGNALS: Q_SIGNALS:
/** void errorOccured(const QString &error, const QString &detail);
* @brief Request a error message be shown to the user.
*/
void errorOccured(const QString &error);
void connectionAdded(NeoChatConnection *connection); void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection); void connectionDropped(NeoChatConnection *connection);
void activeConnectionChanged(NeoChatConnection *connection); void activeConnectionChanged(NeoChatConnection *connection);
void accountsLoadingChanged(); void accountsLoadingChanged();
void showMessage(MessageType messageType, const QString &message);
}; };

View File

@@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
@@ -24,7 +23,7 @@ ColumnLayout {
model: root.connection.accountDataEventTypes model: root.connection.accountDataEventTypes
delegate: FormCard.FormButtonDelegate { delegate: FormCard.FormButtonDelegate {
text: modelData text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.connection.accountDataJsonString(modelData) sourceText: root.connection.accountDataJsonString(modelData)
}, { }, {
title: i18nc("@title:window", "Event Source"), title: i18nc("@title:window", "Event Source"),

View File

@@ -3,7 +3,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
@@ -48,7 +47,7 @@ ColumnLayout {
model: root.room.accountDataEventTypes model: root.room.accountDataEventTypes
delegate: FormCard.FormButtonDelegate { delegate: FormCard.FormButtonDelegate {
text: modelData text: modelData
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
sourceText: root.room.roomAcountDataJson(text) sourceText: root.room.roomAcountDataJson(text)
}, { }, {
title: i18n("Event Source"), title: i18n("Event Source"),
@@ -78,7 +77,7 @@ ColumnLayout {
if (model.eventCount === 1) { if (model.eventCount === 1) {
openEventSource(model.type, model.stateKey); openEventSource(model.type, model.stateKey);
} else { } else {
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), { pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
room: root.room, room: root.room,
eventType: model.type eventType: model.type
}, { }, {
@@ -90,7 +89,7 @@ ColumnLayout {
} }
} }
function openEventSource(type: string, stateKey: string): void { function openEventSource(type: string, stateKey: string): void {
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateModel, model: stateModel,
allowEdit: true, allowEdit: true,
room: root.room, room: root.room,

View File

@@ -3,7 +3,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
@@ -38,7 +37,7 @@ FormCard.FormCardPage {
} }
function openEventSource(stateKey: string): void { function openEventSource(stateKey: string): void {
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), { applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
model: stateKeysModel, model: stateKeysModel,
allowEdit: true, allowEdit: true,
room: root.room, room: root.room,

View File

@@ -1,31 +0,0 @@
// 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 once
#include <QObject>
#include <QQmlEngine>
/**
* @class MessageType
*
* This class is designed to define the MessageType enumeration.
*/
class MessageType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief The types of messages that can be shown.
*/
enum Type {
Information = 0, /**< Info message, typically highlight color. */
Positive, /**< Positive message, typically green. */
Warning, /**< Warning message, typically amber. */
Error, /**< Error message, typically red. */
};
Q_ENUM(Type);
};

View File

@@ -10,7 +10,6 @@
#include <Quotient/events/encryptionevent.h> #include <Quotient/events/encryptionevent.h>
#include <Quotient/events/event.h> #include <Quotient/events/event.h>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/reactionevent.h> #include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h> #include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h> #include <Quotient/events/roomavatarevent.h>
@@ -34,21 +33,6 @@
using namespace Quotient; using namespace Quotient;
namespace
{
enum MemberChange {
None = 0,
AddName = 1,
Rename = 2,
RemoveName = 4,
AddAvatar = 8,
UpdateAvatar = 16,
RemoveAvatar = 32,
};
Q_DECLARE_FLAGS(MemberChanges, MemberChange)
Q_DECLARE_OPERATORS_FOR_FLAGS(MemberChanges)
};
QString EventHandler::id(const Quotient::RoomEvent *event) QString EventHandler::id(const Quotient::RoomEvent *event)
{ {
if (event == nullptr) { if (event == nullptr) {
@@ -225,10 +209,10 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{ {
QString body; QString body;
if (event.has<EventContent::FileContent>()) { if (event.hasFileContent()) {
// if filename is given or body is equal to filename, // if filename is given or body is equal to filename,
// then body is a caption // then body is a caption
QString filename = event.get<EventContent::FileContent>()->originalName; QString filename = event.content()->fileInfo()->originalName;
QString body = event.plainBody(); QString body = event.plainBody();
if (filename.isEmpty() || filename == body) { if (filename.isEmpty() || filename == body) {
return QString(); return QString();
@@ -236,8 +220,8 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
return body; return body;
} }
if (event.has<EventContent::TextContent>() && event.content()) { if (event.hasTextContent() && event.content()) {
body = event.get<EventContent::TextContent>()->body; body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -293,7 +277,7 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
{ {
if (event->isRedacted()) { if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason(); auto reason = event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason.toHtmlEscaped()); return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
} }
const bool prettyPrint = (format == Qt::RichText); const bool prettyPrint = (format == Qt::RichText);
@@ -308,7 +292,7 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
}, },
[room, prettyPrint](const RoomMemberEvent &e) { [room, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event // FIXME: Rewind to the name that was at the time of this event
auto subjectName = prettyPrint ? room->member(e.userId()).htmlSafeDisplayName() : room->member(e.userId()).displayName(); auto subjectName = room->member(e.userId()).htmlSafeDisplayName();
if (e.membership() == Membership::Leave) { if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) { if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName); subjectName = sanitized(*e.prevContent()->displayName);
@@ -464,11 +448,11 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
{ {
TextHandler textHandler; TextHandler textHandler;
if (event.has<EventContent::FileContent>()) { if (event.hasFileContent()) {
QString fileCaption = event.get<EventContent::FileContent>()->originalName; auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) { if (fileCaption.isEmpty()) {
fileCaption = event.plainBody(); fileCaption = event.plainBody();
} else if (fileCaption != event.plainBody()) { } else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption; fileCaption = event.plainBody() + " | "_ls + fileCaption;
} }
textHandler.setData(fileCaption); textHandler.setData(fileCaption);
@@ -476,8 +460,8 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
} }
QString body; QString body;
if (event.has<EventContent::TextContent>() && event.content()) { if (event.hasTextContent() && event.content()) {
body = event.get<EventContent::TextContent>()->body; body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else { } else {
body = event.plainBody(); body = event.plainBody();
} }
@@ -498,12 +482,8 @@ QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageE
} }
} }
QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomEvent *event) QString EventHandler::genericBody(const Quotient::RoomEvent *event)
{ {
if (room == nullptr) {
qCWarning(EventHandling) << "genericBody called with room set to nullptr.";
return {};
}
if (event == nullptr) { if (event == nullptr) {
qCWarning(EventHandling) << "genericBody called with event set to nullptr."; qCWarning(EventHandling) << "genericBody called with event set to nullptr.";
return {}; return {};
@@ -512,149 +492,123 @@ QString EventHandler::genericBody(const NeoChatRoom *room, const Quotient::RoomE
return i18n("<i>[This message was deleted]</i>"); return i18n("<i>[This message was deleted]</i>");
} }
const auto sender = room->member(event->senderId());
const auto senderString = QStringLiteral("<a href=\"https://matrix.to/#/%1\">%2</a>").arg(sender.id(), sender.htmlSafeDisplayName());
return switchOnType( return switchOnType(
*event, *event,
[senderString](const RoomMessageEvent &) { [](const RoomMessageEvent &e) {
return i18n("%1 sent a message", senderString); Q_UNUSED(e)
return i18n("sent a message");
}, },
[senderString](const StickerEvent &) { [](const StickerEvent &e) {
return i18n("%1 sent a sticker", senderString); Q_UNUSED(e)
return i18n("sent a sticker");
}, },
[senderString](const RoomMemberEvent &e) { [](const RoomMemberEvent &e) {
switch (e.membership()) { switch (e.membership()) {
case Membership::Invite: case Membership::Invite:
if (e.repeatsState()) { if (e.repeatsState()) {
return i18n("%1 reinvited someone to the room", senderString); return i18n("reinvited someone to the room");
} }
Q_FALLTHROUGH(); Q_FALLTHROUGH();
case Membership::Join: { case Membership::Join: {
QString text{};
// Part 1: invites and joins // Part 1: invites and joins
if (e.repeatsState()) { if (e.repeatsState()) {
return i18n("%1 joined the room (repeated)", senderString); text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) { } else if (e.changesMembership()) {
return e.membership() == Membership::Invite ? i18n("%1 invited someone to the room", senderString) text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
: i18n("%1 joined the room", senderString); }
if (!text.isEmpty()) {
return text;
} }
// Part 2: profile changes of joined members // Part 2: profile changes of joined members
MemberChanges changes = None;
if (e.isRename()) { if (e.isRename()) {
if (!e.newDisplayName()) { if (!e.newDisplayName()) {
changes |= RemoveName; text = i18nc("their refers to a singular user", "cleared their display name");
} else if (!e.prevContent()->displayName) {
changes |= AddName;
} else { } else {
changes |= Rename; text = i18nc("their refers to a singular user", "changed their display name");
} }
} }
if (e.isAvatarUpdate()) { if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) { if (!e.newAvatarUrl()) {
changes |= RemoveAvatar; text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) { } else if (!e.prevContent()->avatarUrl) {
changes |= AddAvatar; text += i18n("set an avatar");
} else { } else {
changes |= UpdateAvatar; text += i18nc("their refers to a singular user", "updated their avatar");
} }
} }
if (text.isEmpty()) {
if (changes.testFlag(AddName)) { text = i18nc("<user> changed nothing", "changed nothing");
if (changes.testFlag(AddAvatar)) {
return i18n("%1 set a display name and set an avatar", senderString);
} else if (changes.testFlag(UpdateAvatar)) {
return i18n("%1 set a display name and updated their avatar", senderString);
} else if (changes.testFlag(RemoveAvatar)) {
return i18n("%1 set a display name and cleared their avatar", senderString);
}
return i18n("%1 set a display name for this room", senderString);
} else if (changes.testFlag(Rename)) {
if (changes.testFlag(AddAvatar)) {
return i18n("%1 changed their display name and set an avatar", senderString);
} else if (changes.testFlag(UpdateAvatar)) {
return i18n("%1 changed their display name and updated their avatar", senderString);
} else if (changes.testFlag(RemoveAvatar)) {
return i18n("%1 changed their display name and cleared their avatar", senderString);
}
return i18n("%1 changed their display name", senderString);
} else if (changes.testFlag(RemoveName)) {
if (changes.testFlag(AddAvatar)) {
return i18n("%1 cleared their display name and set an avatar", senderString);
} else if (changes.testFlag(UpdateAvatar)) {
return i18n("%1 cleared their display name and updated their avatar", senderString);
} else if (changes.testFlag(RemoveAvatar)) {
return i18n("%1 cleared their display name and cleared their avatar", senderString);
}
return i18n("%1 cleared their display name", senderString);
} }
return text;
return i18nc("<user> changed nothing", "%1 changed nothing", senderString);
} }
case Membership::Leave: case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) { if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("%1 withdrew a user's invitation", senderString) return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
: i18n("%1 rejected the invitation", senderString);
} }
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) { if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("%1 unbanned a user", senderString) : i18n("%1 self-unbanned", senderString); return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
} }
return (e.senderId() != e.userId()) ? i18n("%1 put a user out of the room", senderString) : i18n("%1 left the room", senderString); return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case Membership::Ban: case Membership::Ban:
if (e.senderId() != e.userId()) { if (e.senderId() != e.userId()) {
return i18n("%1 banned a user from the room", senderString); return i18n("banned a user from the room");
} else { } else {
return i18n("%1 self-banned from the room", senderString); return i18n("self-banned from the room");
} }
case Membership::Knock: { case Membership::Knock: {
return i18n("%1 requested an invite", senderString); return i18n("requested an invite");
} }
default:; default:;
} }
return i18n("%1 made something unknown", senderString); return i18n("made something unknown");
}, },
[senderString](const RoomCanonicalAliasEvent &e) { [](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("%1 cleared the room main alias", senderString) : i18n("%1 set the room main alias", senderString); return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
}, },
[senderString](const RoomNameEvent &e) { [](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("%1 cleared the room name", senderString) : i18n("%1 set the room name", senderString); return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
}, },
[senderString](const RoomTopicEvent &e) { [](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("%1 cleared the topic", senderString) : i18n("%1 set the topic", senderString); return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
}, },
[senderString](const RoomAvatarEvent &) { [](const RoomAvatarEvent &) {
return i18n("%1 changed the room avatar", senderString); return i18n("changed the room avatar");
}, },
[senderString](const EncryptionEvent &) { [](const EncryptionEvent &) {
return i18n("%1 activated End-to-End Encryption", senderString); return i18n("activated End-to-End Encryption");
}, },
[senderString](const RoomCreateEvent &e) { [](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("%1 upgraded the room version", senderString) : i18n("%1 created the room", senderString); return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
}, },
[senderString](const RoomPowerLevelsEvent &) { [](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "%1 changed the power levels for this room", senderString); return i18nc("'power level' means permission level", "changed the power levels for this room");
}, },
[senderString](const LocationBeaconEvent &) { [](const LocationBeaconEvent &) {
return i18n("%1 sent a live location beacon", senderString); return i18n("sent a live location beacon");
}, },
[senderString](const RoomServerAclEvent &) { [](const RoomServerAclEvent &) {
return i18n("%1 changed the server access control lists for this room", senderString); return i18n("changed the server access control lists for this room");
}, },
[senderString](const WidgetEvent &e) { [](const WidgetEvent &e) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) { if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("%1 added a widget", senderString); return i18n("added a widget");
} }
if (e.contentJson().isEmpty()) { if (e.contentJson().isEmpty()) {
return i18n("%1 removed a widget", senderString); return i18n("removed a widget");
} }
return i18n("%1 configured a widget", senderString); return i18n("configured a widget");
}, },
[senderString](const StateEvent &) { [](const StateEvent &) {
return i18n("%1 updated the state", senderString); return i18n("updated the state");
}, },
[senderString](const PollStartEvent &) { [](const PollStartEvent &e) {
return i18n("%1 started a poll", senderString); Q_UNUSED(e);
return i18n("started a poll");
}, },
i18n("Unknown event")); i18n("Unknown event"));
} }
@@ -692,29 +646,32 @@ QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Qu
// Get the file info for the event. // Get the file info for the event.
if (event->is<RoomMessageEvent>()) { if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event); auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->has<EventContent::FileContentBase>()) { if (!roomMessageEvent->hasFileContent()) {
return {}; return {};
} }
const auto content = roomMessageEvent->get<EventContent::FileContentBase>(); const EventContent::FileInfo *fileInfo;
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, content.get(), eventId, false, false); fileInfo = roomMessageEvent->content()->fileInfo();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, fileInfo, eventId, false, false);
// if filename isn't specifically given, it is in body // if filename isn't specifically given, it is in body
// https://spec.matrix.org/latest/client-server-api/#mfile // https://spec.matrix.org/latest/client-server-api/#mfile
mediaInfo["filename"_ls] = content->commonInfo().originalName.isEmpty() ? roomMessageEvent->plainBody() : content->commonInfo().originalName; mediaInfo["filename"_ls] = (fileInfo->originalName.isEmpty()) ? roomMessageEvent->plainBody() : fileInfo->originalName;
return mediaInfo; return mediaInfo;
} else if (event->is<StickerEvent>()) { } else if (event->is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(event); const EventContent::FileInfo *fileInfo;
auto content = &stickerEvent->image();
return getMediaInfoFromFileInfo(room, content, eventId, false, true); auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
return getMediaInfoFromFileInfo(room, fileInfo, eventId, false, true);
} else { } else {
return {}; return {};
} }
} }
QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room, QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
const Quotient::EventContent::FileContentBase *fileContent, const EventContent::FileInfo *fileInfo,
const QString &eventId, const QString &eventId,
bool isThumbnail, bool isThumbnail,
bool isSticker) bool isSticker)
@@ -722,10 +679,10 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
QVariantMap mediaInfo; QVariantMap mediaInfo;
// Get the mxc URL for the media. // Get the mxc URL for the media.
if (!fileContent->url().isValid() || fileContent->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) { if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl(); mediaInfo["source"_ls] = QUrl();
} else { } else {
QUrl source = room->makeMediaUrl(eventId, fileContent->url()); QUrl source = room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid()) { if (source.isValid()) {
mediaInfo["source"_ls] = source; mediaInfo["source"_ls] = source;
@@ -734,7 +691,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
} }
} }
auto mimeType = fileContent->type(); auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available. // Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name(); mediaInfo["mimeType"_ls] = mimeType.name();
@@ -742,43 +699,45 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
mediaInfo["mimeIcon"_ls] = mimeType.iconName(); mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available. // Add media size if available.
mediaInfo["size"_ls] = fileContent->commonInfo().payloadSize; mediaInfo["size"_ls] = fileInfo->payloadSize;
mediaInfo["isSticker"_ls] = isSticker; mediaInfo["isSticker"_ls] = isSticker;
// Add parameter depending on media type. // Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) { if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileContent)) { if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width(); mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height(); mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static. // TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8()); mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
QVariantMap tempInfo; if (!isThumbnail) {
auto thumbnailInfo = getMediaInfoFromTumbnail(room, castInfo->thumbnail, eventId); QVariantMap tempInfo;
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) { auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
tempInfo = thumbnailInfo; if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
} else { tempInfo = thumbnailInfo;
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else { } else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash); QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
} }
mediaInfo["tempInfo"_ls] = tempInfo;
} }
mediaInfo["tempInfo"_ls] = tempInfo;
} }
} }
if (mimeType.name().contains(QStringLiteral("video"))) { if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileContent)) { if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width(); mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height(); mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration; mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) { if (!isThumbnail) {
QVariantMap tempInfo; QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromTumbnail(room, castInfo->thumbnail, eventId); auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) { if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo; tempInfo = thumbnailInfo;
} else { } else {
@@ -794,7 +753,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
} }
} }
if (mimeType.name().contains(QStringLiteral("audio"))) { if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileContent)) { if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration; mediaInfo["duration"_ls] = castInfo->duration;
} }
} }
@@ -802,38 +761,6 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
return mediaInfo; return mediaInfo;
} }
QVariantMap EventHandler::getMediaInfoFromTumbnail(const NeoChatRoom *room, const Quotient::EventContent::Thumbnail &thumbnail, const QString &eventId)
{
QVariantMap thumbnailInfo;
if (!thumbnail.url().isValid() || thumbnail.url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
thumbnailInfo["source"_ls] = QUrl();
} else {
QUrl source = room->makeMediaUrl(eventId, thumbnail.url());
if (source.isValid()) {
thumbnailInfo["source"_ls] = source;
} else {
thumbnailInfo["source"_ls] = QUrl();
}
}
auto mimeType = thumbnail.mimeType;
// Add the MIME type for the media if available.
thumbnailInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
thumbnailInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
thumbnailInfo["size"_ls] = thumbnail.payloadSize;
thumbnailInfo["width"_ls] = thumbnail.imageSize.width();
thumbnailInfo["height"_ls] = thumbnail.imageSize.height();
return thumbnailInfo;
}
bool EventHandler::hasReply(const Quotient::RoomEvent *event, bool showFallbacks) bool EventHandler::hasReply(const Quotient::RoomEvent *event, bool showFallbacks)
{ {
if (event == nullptr) { if (event == nullptr) {

View File

@@ -192,7 +192,7 @@ public:
* *
* @sa richBody(), plainBody() * @sa richBody(), plainBody()
*/ */
static QString genericBody(const NeoChatRoom *room, const Quotient::RoomEvent *event); static QString genericBody(const Quotient::RoomEvent *event);
/** /**
* @brief Output a string for the event to be used as a RoomList subtitle. * @brief Output a string for the event to be used as a RoomList subtitle.
@@ -290,9 +290,8 @@ private:
static QVariantMap getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event); static QVariantMap getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QVariantMap static getMediaInfoFromFileInfo(const NeoChatRoom *room, QVariantMap static getMediaInfoFromFileInfo(const NeoChatRoom *room,
const Quotient::EventContent::FileContentBase *fileContent, const Quotient::EventContent::FileInfo *fileInfo,
const QString &eventId, const QString &eventId,
bool isThumbnail = false, bool isThumbnail = false,
bool isSticker = false); bool isSticker = false);
static QVariantMap getMediaInfoFromTumbnail(const NeoChatRoom *room, const Quotient::EventContent::Thumbnail &thumbnail, const QString &eventId);
}; };

View File

@@ -3,6 +3,7 @@
#include "imagepackevent.h" #include "imagepackevent.h"
#include <QJsonObject> #include <QJsonObject>
#include <Quotient/omittable.h>
using namespace Quotient; using namespace Quotient;
@@ -10,10 +11,10 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
{ {
if (json.contains(QStringLiteral("pack"))) { if (json.contains(QStringLiteral("pack"))) {
pack = ImagePackEventContent::Pack{ pack = ImagePackEventContent::Pack{
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]), fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]), fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]), fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]), fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
}; };
} else { } else {
pack = std::nullopt; pack = std::nullopt;
@@ -30,9 +31,9 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
images += ImagePackImage{ images += ImagePackImage{
k, k,
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()), fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
fromJson<std::optional<QString>>(json["images"_ls][k]["body"_ls]), fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
info, info,
fromJson<std::optional<QStringList>>(json["images"_ls][k]["usage"_ls]), fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
}; };
} }
} }

View File

@@ -7,10 +7,13 @@
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
#include <Quotient/e2ee/sssshandler.h> #include <Quotient/e2ee/sssshandler.h>
#include <Quotient/keyimport.h>
#include <Quotient/keyverificationsession.h> #include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h> #include <Quotient/roommember.h>
#if Quotient_VERSION_MINOR > 8
#include <Quotient/keyimport.h>
#endif
#include "controller.h" #include "controller.h"
#include "neochatconfig.h" #include "neochatconfig.h"
@@ -40,9 +43,11 @@ struct ForeignSSSSHandler {
QML_NAMED_ELEMENT(SSSSHandler) QML_NAMED_ELEMENT(SSSSHandler)
}; };
#if Quotient_VERSION_MINOR > 8
struct ForeignKeyImport { struct ForeignKeyImport {
Q_GADGET Q_GADGET
QML_SINGLETON QML_SINGLETON
QML_FOREIGN(Quotient::KeyImport) QML_FOREIGN(Quotient::KeyImport)
QML_NAMED_ELEMENT(KeyImport) QML_NAMED_ELEMENT(KeyImport)
}; };
#endif

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeochatAdd3PIdJob::NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const std::optional<QJsonObject> &auth) NeochatAdd3PIdJob::NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), makePath("/_matrix/client/v3", "/account/3pid/add")) : BaseJob(HttpVerb::Post, QStringLiteral("Add3PIDJob"), makePath("/_matrix/client/v3", "/account/3pid/add"))
{ {
QJsonObject _dataJson; QJsonObject _dataJson;

View File

@@ -4,9 +4,10 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatAdd3PIdJob : public Quotient::BaseJob class NeochatAdd3PIdJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const std::optional<QJsonObject> &auth = {}); explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = {});
}; };

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth) NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password") : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{ {
QJsonObject _data; QJsonObject _data;

View File

@@ -5,8 +5,10 @@
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob class NeochatChangePasswordJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth = {}); explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = {});
}; };

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth) NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate") : BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{ {
QJsonObject data; QJsonObject data;

View File

@@ -4,9 +4,10 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{ {
public: public:
explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = {}); explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = {});
}; };

View File

@@ -5,7 +5,7 @@
using namespace Quotient; using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth) NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1()) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{ {
QJsonObject _data; QJsonObject _data;

View File

@@ -4,9 +4,10 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob class NeochatDeleteDeviceJob : public Quotient::BaseJob
{ {
public: public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth = {}); explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = {});
}; };

View File

@@ -4,6 +4,7 @@
#pragma once #pragma once
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
// TODO: Upstream to libQuotient // TODO: Upstream to libQuotient
class NeochatGetCommonRoomsJob : public Quotient::BaseJob class NeochatGetCommonRoomsJob : public Quotient::BaseJob

View File

@@ -25,7 +25,8 @@ void LoginHelper::init()
m_connection = new NeoChatConnection(); m_connection = new NeoChatConnection();
m_matrixId = QString(); m_matrixId = QString();
m_password = QString(); m_password = QString();
m_deviceName = QStringLiteral("NeoChat"); m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
.arg(QSysInfo::machineHostName(), QSysInfo::productType(), QSysInfo::productVersion(), QSysInfo::currentCpuArchitecture());
m_supportsSso = false; m_supportsSso = false;
m_supportsPassword = false; m_supportsPassword = false;
m_ssoUrl = QUrl(); m_ssoUrl = QUrl();
@@ -84,7 +85,7 @@ void LoginHelper::init()
m_connection = nullptr; m_connection = nullptr;
}); });
connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) { connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error))); Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
m_isLoggingIn = false; m_isLoggingIn = false;
Q_EMIT isLoggingInChanged(); Q_EMIT isLoggingInChanged();
}); });
@@ -92,14 +93,14 @@ void LoginHelper::init()
if (error == QStringLiteral("Invalid username or password")) { if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true); setInvalidPassword(true);
} else { } else {
Q_EMIT loginErrorOccured(i18n("Login Failed: %1", error)); Q_EMIT errorOccured(i18n("Login Failed: %1", error));
} }
m_isLoggingIn = false; m_isLoggingIn = false;
Q_EMIT isLoggingInChanged(); Q_EMIT isLoggingInChanged();
}); });
connect(m_connection, &Connection::resolveError, this, [this](QString error) { connect(m_connection, &Connection::resolveError, this, [](QString error) {
Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error))); Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
}); });
connect( connect(

View File

@@ -130,7 +130,7 @@ Q_SIGNALS:
void loginFlowsChanged(); void loginFlowsChanged();
void ssoUrlChanged(); void ssoUrlChanged();
void connected(); void connected();
void loginErrorOccured(const QString &message); void errorOccured(const QString &message);
void testingChanged(); void testingChanged();
void isLoggingInChanged(); void isLoggingInChanged();
void isLoggedInChanged(); void isLoggedInChanged();

View File

@@ -13,7 +13,6 @@ LoginStep {
id: root id: root
FormCard.FormTextDelegate { FormCard.FormTextDelegate {
textItem.wrapMode: Text.Wrap
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.") text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
} }
FormCard.AbstractFormDelegate { FormCard.AbstractFormDelegate {

View File

@@ -7,7 +7,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat import org.kde.neochat
import org.kde.neochat.settings import org.kde.neochat.settings
@@ -17,7 +16,6 @@ Kirigami.Page {
property bool showExisting: false property bool showExisting: false
property bool _showExisting: showExisting && root.currentStepString === root.initialStep property bool _showExisting: showExisting && root.currentStepString === root.initialStep
property bool showSettings: true
property alias currentStep: module.item property alias currentStep: module.item
property string currentStepString: initialStep property string currentStepString: initialStep
property string initialStep: "LoginRegister" property string initialStep: "LoginRegister"
@@ -92,27 +90,11 @@ Kirigami.Page {
id: loadedAccounts id: loadedAccounts
model: AccountRegistry model: AccountRegistry
delegate: FormCard.FormButtonDelegate { delegate: FormCard.FormButtonDelegate {
id: delegate text: model.userId
required property string userId
required property NeoChatConnection connection
text: QmlUtils.escapeString(connection.localUser.displayName)
description: connection.localUser.id
leadingPadding: Kirigami.Units.largeSpacing
onClicked: { onClicked: {
Controller.activeConnection = delegate.connection; Controller.activeConnection = model.connection;
root.connectionChosen(); root.connectionChosen();
} }
leading: KirigamiComponents.Avatar {
id: avatar
name: delegate.text
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
source: delegate.connection.localUser.avatarUrl.toString().length > 0 ? delegate.connection.makeMediaUrl(delegate.connection.localUser.avatarUrl) : ""
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
}
} }
} }
Repeater { Repeater {
@@ -232,7 +214,7 @@ Kirigami.Page {
Connections { Connections {
target: LoginHelper target: LoginHelper
function onLoginErrorOccured(message) { function onErrorOccured(message) {
headerMessage.text = message; headerMessage.text = message;
headerMessage.visible = message.length > 0; headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error; headerMessage.type = Kirigami.MessageType.Error;
@@ -250,7 +232,7 @@ Kirigami.Page {
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue") text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger() onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right-symbolic" icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
} }
@@ -258,7 +240,7 @@ Kirigami.Page {
text: i18nc("@action:button", "Go back") text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger() onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left-symbolic" icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
} }
} }
@@ -266,7 +248,6 @@ Kirigami.Page {
FormCard.FormCard { FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 2 Layout.topMargin: Kirigami.Units.largeSpacing * 2
maximumWidth: Kirigami.Units.gridUnit * 20 maximumWidth: Kirigami.Units.gridUnit * 20
visible: root.showSettings
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Settings") text: i18nc("@action:button", "Settings")
icon.name: "settings-configure" icon.name: "settings-configure"

View File

@@ -37,7 +37,6 @@
#include <KCrash> #include <KCrash>
#endif #endif
#include <KIconTheme>
#include <KLocalizedContext> #include <KLocalizedContext>
#include <KLocalizedString> #include <KLocalizedString>
@@ -49,6 +48,7 @@
#include "colorschemer.h" #include "colorschemer.h"
#include "controller.h" #include "controller.h"
#include "logger.h" #include "logger.h"
#include "neochatconfig.h"
#include "roommanager.h" #include "roommanager.h"
#include "sharehandler.h" #include "sharehandler.h"
#include "windowcontroller.h" #include "windowcontroller.h"
@@ -102,7 +102,6 @@ Q_DECL_EXPORT
#endif #endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true); QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW #ifdef HAVE_WEBVIEW
@@ -142,7 +141,7 @@ int main(int argc, char *argv[])
KAboutData about(QStringLiteral("neochat"), KAboutData about(QStringLiteral("neochat"),
i18n("NeoChat"), i18n("NeoChat"),
QStringLiteral(NEOCHAT_VERSION_STRING), QStringLiteral(NEOCHAT_VERSION_STRING),
i18n("Chat on Matrix"), i18n("Matrix client"),
KAboutLicense::GPL_V3, KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community")); i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
about.addAuthor(i18n("Carl Schwan"), about.addAuthor(i18n("Carl Schwan"),

View File

@@ -1,22 +0,0 @@
// 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 once
#include "enums/messagecomponenttype.h"
struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other;
QString content;
QVariantMap attributes;
int operator==(const MessageComponent &right) const
{
return type == right.type && content == right.content && attributes == right.attributes;
}
bool isEmpty() const
{
return type == MessageComponentType::Other;
}
};

View File

@@ -163,7 +163,11 @@ void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source) QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
{ {
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString()); auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
#if Quotient_VERSION_MINOR > 8
co_await qCoro(job.get(), &BaseJob::finished); co_await qCoro(job.get(), &BaseJob::finished);
#else
co_await qCoro(job, &BaseJob::finished);
#endif
if (job->error() != BaseJob::NoError) { if (job->error() != BaseJob::NoError) {
co_return; co_return;
} }
@@ -185,7 +189,11 @@ QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl sourc
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type) QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
{ {
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString()); auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
#if Quotient_VERSION_MINOR > 8
co_await qCoro(job.get(), &BaseJob::finished); co_await qCoro(job.get(), &BaseJob::finished);
#else
co_await qCoro(job, &BaseJob::finished);
#endif
if (job->error() != BaseJob::NoError) { if (job->error() != BaseJob::NoError) {
co_return; co_return;
} }

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