Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolas Fella
945b27eca8 Add activation support to rooms runner
This is needed to raise the window when activating the runner action on Wayland
2023-09-23 19:39:03 +00:00
176 changed files with 102281 additions and 116544 deletions

View File

@@ -19,7 +19,6 @@
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [

View File

@@ -3,7 +3,7 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml

View File

@@ -13,7 +13,6 @@ Dependencies:
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
@@ -26,6 +25,7 @@ Dependencies:
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'

View File

@@ -41,7 +41,3 @@ License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: autotests/data/*
Copyright: none
License: CC0-1.0

View File

@@ -57,15 +57,15 @@ set_package_properties(Qt6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if(ANDROID)
@@ -76,17 +76,11 @@ if(ANDROID)
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
find_package(ICU 61.0 COMPONENTS uc)
set_package_properties(ICU PROPERTIES
TYPE REQUIRED
PURPOSE "Unicode library"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
@@ -101,8 +95,7 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server"
)
# The android part is just for CI. We do NOT support any builds without E2EE
if (NOT TARGET Olm::Olm AND NOT ANDROID)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
@@ -118,7 +111,7 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
ecm_find_qmlmodule(QtLocation)
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES

View File

@@ -4,26 +4,25 @@
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
import os
import time
import subprocess
import sys
import unittest
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class LoginTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
def setUpClass(self):
desired_caps = {}
desired_caps["app"] = "neochat --ignore-ssl-errors"
desired_caps["timeouts"] = {'implicit': 10000}
self.driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723',
desired_capabilities=desired_caps)
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
def setUp(self):
pass
@@ -46,6 +45,5 @@ class LoginTest(unittest.TestCase):
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Join some rooms to get started").click()
if __name__ == '__main__':
unittest.main()

View File

@@ -3,8 +3,6 @@
enable_testing()
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
@@ -34,9 +32,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)

View File

@@ -1,157 +0,0 @@
// 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 <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTest>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include "chatbarcache.h"
#include "neochatroom.h"
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class ChatBarCacheTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void edit();
void attachment();
};
void ChatBarCacheTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
QFile testMinSyncFile;
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
testMinSyncFile.open(QIODevice::ReadOnly);
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
room->update(std::move(roomData));
}
void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::noRoom()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
}
QTEST_MAIN(ChatBarCacheTest)
#include "chatbarcachetest.moc"

View File

@@ -1,381 +0,0 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "Thread root",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$threadroot:example.org",
"origin_server_ts": 1690821582879,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "Thread message 1",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadroot:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage1:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "Thread message 2",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadmessage1:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage2:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,87 +0,0 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -64,7 +64,6 @@ private Q_SLOTS:
void replyAuthor();
void replyBody();
void replyMediaInfo();
void thread();
void location();
void readMarkers();
};
@@ -74,11 +73,329 @@ void EventHandlerTest::initTestCase()
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
QFile testEventHandlerSyncFile;
testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json"));
testEventHandlerSyncFile.open(QIODevice::ReadOnly);
const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object());
const auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
@@ -369,29 +686,6 @@ void EventHandlerTest::replyMediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::thread()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), false);
QCOMPARE(eventHandler.threadRoot(), QString());
event = room->messageEvents().at(9).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org"));
event = room->messageEvents().at(10).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::location()
{
auto event = room->messageEvents().at(7).get();

View File

@@ -2,4 +2,4 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

View File

@@ -11,7 +11,6 @@
</provides>
<name>NeoChat</name>
<name xml:lang="ar">نيوتشات</name>
<name xml:lang="ast">NeoChat</name>
<name xml:lang="az">NeoChat</name>
<name xml:lang="ca">NeoChat</name>
<name xml:lang="ca-valencia">NeoChat</name>
@@ -54,7 +53,6 @@
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
@@ -99,7 +97,7 @@ to provide a convergent experience across multiple platforms.</p>
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="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 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="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="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>
@@ -284,7 +282,7 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot environment="windows">
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
@@ -311,7 +309,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot environment="windows">
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
@@ -343,7 +341,6 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>

View File

@@ -4,7 +4,6 @@
Version=1.5
Name=NeoChat
Name[ar]=نيوتشات
Name[ast]=NeoChat
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat

View File

@@ -1,8 +1 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M3 3H19V14H8.68787L4 18.1019V14H3V3ZM4 13H5V15.8981L8.31213 13H18V4H4V13Z" fill="currentColor"/>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M17 15.2929L14.8536 13.1465L14.1465 13.8536L18 17.7071V13.5H17V15.2929Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 6H15V7H5V6Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 8H13V9H5V8Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 10H11V11H5V10Z" fill="currentColor"/>
</svg>
<svg width="22" height="22" fill="none" version="1.1" id="svg13" xmlns="http://www.w3.org/2000/svg"><style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 928 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -40,12 +40,6 @@ add_library(neochat STATIC
models/userfiltermodel.h
models/publicroomlistmodel.cpp
models/publicroomlistmodel.h
models/spacechildrenmodel.cpp
models/spacechildrenmodel.h
models/spacechildsortfiltermodel.cpp
models/spacechildsortfiltermodel.h
models/spacetreeitem.cpp
models/spacetreeitem.h
models/userdirectorylistmodel.cpp
models/userdirectorylistmodel.h
models/pushrulemodel.cpp
@@ -133,12 +127,6 @@ add_library(neochat STATIC
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
roomlastmessageprovider.cpp
roomlastmessageprovider.h
chatbarcache.cpp
chatbarcache.h
colorschemer.cpp
colorschemer.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -156,7 +144,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
@@ -170,6 +157,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/NeochatMaximizeComponent.qml
qml/FancyEffectsContainer.qml
qml/TypingPane.qml
qml/ShimmerGradient.qml
qml/QuickSwitcher.qml
qml/HoverActions.qml
qml/ChatBox.qml
@@ -219,6 +207,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/CreateSpaceDialog.qml
qml/EmojiDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
@@ -282,10 +271,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomMedia.qml
qml/ChooseRoomDialog.qml
qml/ShareAction.qml
qml/SpaceHomePage.qml
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
@@ -327,15 +312,16 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID)
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF6::WindowSystem ICU::uc)
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
@@ -345,27 +331,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
target_link_libraries(neochat PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
Qt::Gui
Qt::Multimedia
Qt::Network
Qt::QuickControls2
KF6::I18n
KF6::Kirigami2
KF6::Notifications
KF6::ConfigCore
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::ColorScheme
KF6::ItemModels
QuotientQt6
cmark::cmark
QCoro::Core
)
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF6::I18n KF6::Kirigami2 KF6::Notifications KF6::ConfigCore KF6::ConfigGui KF6::CoreAddons KF6::SonnetCore KF6::ItemModels QuotientQt6 cmark::cmark QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -451,7 +417,6 @@ if(ANDROID)
"preferences-desktop-notification"
"computer-symbolic"
"gps"
"system-users-symbolic"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)

View File

@@ -38,33 +38,45 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
void ActionsHandler::handleNewMessage()
{
if (!chatBarCache) {
return;
}
checkEffects(chatBarCache->text());
if (!chatBarCache->attachmentPath().isEmpty()) {
QUrl url(chatBarCache->attachmentPath());
checkEffects(m_room->chatBoxText());
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath());
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({});
m_room->uploadFile(QUrl(path), m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : m_room->chatBoxText());
m_room->setChatBoxAttachmentPath({});
m_room->setChatBoxText({});
return;
}
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
}
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
@@ -82,7 +94,7 @@ QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *ment
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
@@ -122,7 +134,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
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);
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
@@ -149,7 +161,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
return;
}
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString());
}
void ActionsHandler::checkEffects(const QString &text)

View File

@@ -8,7 +8,6 @@
#include <Quotient/events/roommessageevent.h>
#include "chatbarcache.h"
#include "neochatroom.h"
class NeoChatRoom;
@@ -52,15 +51,21 @@ Q_SIGNALS:
void showEffect(const QString &effect);
public Q_SLOTS:
/**
* @brief Pre-process text and send message event.
* @brief Pre-process text and send message.
*/
void handleMessageEvent(ChatBarCache *chatBarCache);
void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
private:
NeoChatRoom *m_room = nullptr;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, QList<Mention> *mentions);
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
};

View File

@@ -1,175 +0,0 @@
// 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 "chatbarcache.h"
#include "eventhandler.h"
#include "neochatroom.h"
ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent)
{
}
QString ChatBarCache::text() const
{
return m_text;
}
void ChatBarCache::setText(const QString &text)
{
if (text == m_text) {
return;
}
m_text = text;
Q_EMIT textChanged();
}
bool ChatBarCache::isReplying() const
{
return m_relationType == Reply && !m_relationId.isEmpty();
}
QString ChatBarCache::replyId() const
{
if (m_relationType != Reply) {
return {};
}
return m_relationId;
}
void ChatBarCache::setReplyId(const QString &replyId)
{
if (m_relationType == Reply && m_relationId == replyId) {
return;
}
m_relationId = replyId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Reply;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
bool ChatBarCache::isEditing() const
{
return m_relationType == Edit && !m_relationId.isEmpty();
}
QString ChatBarCache::editId() const
{
if (m_relationType != Edit) {
return {};
}
return m_relationId;
}
void ChatBarCache::setEditId(const QString &editId)
{
if (m_relationType == Edit && m_relationId == editId) {
return;
}
m_relationId = editId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Edit;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
QVariantMap ChatBarCache::relationUser() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
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 (m_relationId.isEmpty()) {
return room->getUser(nullptr);
}
return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId()));
}
QString ChatBarCache::relationMessage() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
if (m_relationId.isEmpty()) {
return {};
}
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 {};
}
EventHandler eventhandler;
eventhandler.setRoom(room);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
eventhandler.setEvent(&**event);
return eventhandler.getPlainBody();
}
return {};
}
bool ChatBarCache::isThreaded() const
{
return !m_threadId.isEmpty();
}
QString ChatBarCache::threadId() const
{
return m_threadId;
}
void ChatBarCache::setThreadId(const QString &threadId)
{
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
}
QString ChatBarCache::attachmentPath() const
{
return m_attachmentPath;
}
void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
{
if (attachmentPath == m_attachmentPath) {
return;
}
m_attachmentPath = attachmentPath;
m_relationType = None;
m_relationId = QString();
Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;
}
QString ChatBarCache::savedText() const
{
return m_savedText;
}
void ChatBarCache::setSavedText(const QString &savedText)
{
m_savedText = savedText;
}

View File

@@ -1,201 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QTextCursor>
/**
* @brief Defines a user mention in the current chat or edit text.
*/
struct Mention {
QTextCursor cursor; /**< Contains the mention's text and position in the text. */
QString text; /**< The inserted text of the mention. */
int start = 0; /**< Start position of the mention. */
int position = 0; /**< End position of the mention. */
QString id; /**< The id the mention (used to create link when sending the message). */
};
/**
* @class ChatBarCache
*
* A class to cache data from a chat bar.
*
* A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
*
* This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
* have a separate cache for each chat bar.
*
* @note The NeoChatRoom which this component is created in is expected to be set
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, MessageEditComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief The text in the chat bar.
*
* Due to problems with QTextDocument, unlike the other properties here,
* text is *not* used to store the text when switching rooms.
*/
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
/**
* @brief Whether the chat bar is currently replying to a message.
*/
Q_PROPERTY(bool isReplying READ isReplying NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being replied to, if any.
*
* Will return empty if the RelationType is currently set to None or Edit.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString replyId READ replyId WRITE setReplyId NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is currently editing a message.
*/
Q_PROPERTY(bool isEditing READ isEditing NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being edited, if any.
*
* Will return empty if the RelationType is currently set to None or Reply.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an reply or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged)
/**
* @brief Get the user for the message being replied to.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room.
*
* Returns an empty user if not replying to a message.
*
* The user QVariantMap has the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa getUser, Quotient::User
*/
Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
/**
* @brief The content of the related message.
*
* Will be QString() if no related message.
*/
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is replying in a thread.
*/
Q_PROPERTY(bool isThreaded READ isThreaded NOTIFY threadIdChanged)
/**
* @brief The Matrix message ID of thread root event, if any.
*/
Q_PROPERTY(QString threadId READ threadId WRITE setThreadId NOTIFY threadIdChanged)
/**
* @brief The local path for a file to send, if any.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or reply.
*/
Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged)
public:
/**
* @brief Describes the type of relation which relationId can refer to.
*
* A chat bar can only be relating to a single message at a time making these
* exclusive.
*/
enum RelationType {
Reply, /**< The current relation is a message being replied to. */
Edit, /**< The current relation is a message being edited. */
None, /**< There is currently no relation event */
};
Q_ENUM(RelationType)
explicit ChatBarCache(QObject *parent = nullptr);
QString text() const;
void setText(const QString &text);
bool isReplying() const;
QString replyId() const;
void setReplyId(const QString &replyId);
bool isEditing() const;
QString editId() const;
void setEditId(const QString &editId);
QVariantMap relationUser() const;
QString relationMessage() const;
bool isThreaded() const;
QString threadId() const;
void setThreadId(const QString &threadId);
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
QList<Mention> *mentions();
/**
* @brief Get the saved chat bar text.
*/
QString savedText() const;
/**
* @brief Save the chat bar text.
*/
void setSavedText(const QString &savedText);
Q_SIGNALS:
void textChanged();
void relationIdChanged();
void threadIdChanged();
void attachmentPathChanged();
private:
QString m_text = QString();
QString m_relationId = QString();
RelationType m_relationType = RelationType::None;
QString m_threadId = QString();
QString m_attachmentPath = QString();
QList<Mention> m_mentions;
QString m_savedText;
};

View File

@@ -57,12 +57,11 @@ public:
setFormat(error.first, error.second.size(), errorFormat);
}
}
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
auto room = handler->room();
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
if (!room) {
return;
}
auto mentions = handler->chatBarCache()->mentions();
auto mentions = room->mentions();
mentions->erase(std::remove_if(mentions->begin(),
mentions->end(),
[this](auto &mention) {
@@ -104,10 +103,15 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr;
if (previousRoom) {
disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
}
previousRoom = m_room;
connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
@@ -211,20 +215,6 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
ChatBarCache *ChatDocumentHandler::chatBarCache() const
{
return m_chatBarCache;
}
void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
{
if (m_chatBarCache == chatBarCache) {
return;
}
m_chatBarCache = chatBarCache;
Q_EMIT chatBarCacheChanged();
}
void ChatDocumentHandler::complete(int index)
{
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
@@ -313,7 +303,11 @@ QString ChatDocumentHandler::getText() const
if (!m_room) {
return QString();
}
return m_chatBarCache->text();
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
}
}
void ChatDocumentHandler::pushMention(const Mention mention) const
@@ -321,7 +315,11 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
if (!m_room) {
return;
}
m_chatBarCache->mentions()->push_back(mention);
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
m_room->mentions()->push_back(mention);
}
}
QColor ChatDocumentHandler::mentionColor() const

View File

@@ -8,7 +8,6 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "chatbarcache.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
@@ -103,11 +102,6 @@ class ChatDocumentHandler : public QObject
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The cache for the chat bar the text document is being handled for.
*/
Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged)
/**
* @brief The color to highlight user mentions.
*/
@@ -139,9 +133,6 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] ChatBarCache *chatBarCache() const;
void setChatBarCache(ChatBarCache *chatBarCache);
Q_INVOKABLE void complete(int index);
void updateCompletions();
@@ -158,7 +149,6 @@ Q_SIGNALS:
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void chatBarCacheChanged();
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
@@ -173,7 +163,6 @@ private:
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;
QPointer<ChatBarCache> m_chatBarCache;
bool completionVisible = false;
QColor m_mentionColor;

View File

@@ -93,9 +93,8 @@ public:
Q_ENUM(PasswordStatus)
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
static Controller *create(QQmlEngine *, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}

View File

@@ -901,28 +901,6 @@ QVariantMap EventHandler::getReplyMediaInfo() const
return getMediaInfoForEvent(replyPtr);
}
bool EventHandler::isThreaded() const
{
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot() const
{
// Get the thread root ID from m.relates_to if it exists.
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return getId();
}
return {};
}
float EventHandler::getLatitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
@@ -1008,5 +986,3 @@ QString EventHandler::getReadMarkersString() const
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

View File

@@ -326,20 +326,6 @@ public:
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
bool isThreaded() const;
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
QString threadRoot() const;
/**
* @brief Return the latitude for the event.
*

View File

@@ -3,7 +3,7 @@
#pragma once
#include <QList>
#include <QVector>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/stateevent.h>
@@ -60,7 +60,7 @@ public:
*
* @sa ImagePackImage
*/
QList<ImagePackEventContent::ImagePackImage> images;
QVector<ImagePackEventContent::ImagePackImage> images;
explicit ImagePackEventContent(const QJsonObject &o);

View File

@@ -12,8 +12,7 @@ class LocationHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
QML_UNCREATABLE("")
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);

View File

@@ -117,7 +117,7 @@ public:
file.write(buf.constData(), buf.size());
file.flush();
if (oldHandler && (!context.category || (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled))) {
if (oldHandler && (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled)) {
oldHandler(type, context, message);
}
}
@@ -197,6 +197,7 @@ void messageHandler(QtMsgType type, const QMessageLogContext &context, const QSt
break;
case QtFatalMsg:
sInstance()->log(QtInfoMsg, context, message);
abort();
}
}
@@ -211,7 +212,7 @@ void filter(QLoggingCategory *category)
void initLogging()
{
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtInfoMsg).isEnabled(QtDebugMsg);
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtDebugMsg).isEnabled(QtDebugMsg);
oldCategoryFilter = QLoggingCategory::installFilter(filter);
oldHandler = qInstallMessageHandler(messageHandler);
sInstance->setOrigHandler(oldHandler);

View File

@@ -34,22 +34,69 @@
#include "neochat-version.h"
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include <Quotient/room.h>
#include <Quotient/user.h>
#include <Quotient/util.h>
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "colorschemer.h"
#include "chatdocumenthandler.h"
#include "controller.h"
#include "delegatesizehelper.h"
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "mediasizehelper.h"
#include "models/accountemoticonmodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/imagepacksmodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/reactionmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/statefiltermodel.h"
#include "models/stickermodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "pollhandler.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "windowcontroller.h"
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
#include "models/completionmodel.h"
#include "models/statemodel.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -122,7 +169,7 @@ int main(int argc, char *argv[])
font.setHintingPreference(QFont::PreferNoHinting);
app.setFont(font);
#endif
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
KLocalizedString::setApplicationDomain("neochat");
QGuiApplication::setOrganizationName("KDE"_ls);
@@ -154,7 +201,9 @@ int main(int argc, char *argv[])
initLogging();
#if Quotient_VERSION_MINOR == 8
Connection::setEncryptionDefault(true);
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
@@ -163,16 +212,18 @@ int main(int argc, char *argv[])
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
#endif
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
#endif
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
// qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
QQmlApplicationEngine engine;

View File

@@ -7,10 +7,10 @@
#include <QAbstractListModel>
#include <QCoroTask>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
#include <Quotient/connection.h>

View File

@@ -3,7 +3,6 @@
#include "actionsmodel.h"
#include "chatbarcache.h"
#include "controller.h"
#include "neochatroom.h"
#include "roommanager.h"
@@ -20,7 +19,7 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls
"#00d4ff"_ls, "#00aaff"_ls, "#007fff"_ls, "#0055ff"_ls, "#002bff"_ls, "#0000ff"_ls, "#2a00ff"_ls, "#5500ff"_ls, "#7f00ff"_ls,
"#aa00ff"_ls, "#d400ff"_ls, "#ff00ff"_ls, "#ff00d4"_ls, "#ff00aa"_ls, "#ff0080"_ls, "#ff0055"_ls, "#ff002b"_ls, "#ff0000"_ls};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
@@ -46,7 +45,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -55,10 +54,10 @@ auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *)
return QString();
};
QList<ActionsModel::Action> actions{
QVector<ActionsModel::Action> actions{
Action{
QStringLiteral("shrug"),
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
[](const QString &message, NeoChatRoom *) {
return QStringLiteral("¯\\\\_(ツ)_/¯ %1").arg(message);
},
true,
@@ -68,7 +67,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("lenny"),
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
[](const QString &message, NeoChatRoom *) {
return QStringLiteral("( ͡° ͜ʖ ͡°) %1").arg(message);
},
true,
@@ -78,7 +77,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("tableflip"),
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
[](const QString &message, NeoChatRoom *) {
return QStringLiteral("(╯°□°)╯︵ ┻━┻ %1").arg(message);
},
true,
@@ -88,7 +87,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unflip"),
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
[](const QString &message, NeoChatRoom *) {
return QStringLiteral("┬──┬ ( ゜-゜ノ) %1").arg(message);
},
true,
@@ -98,7 +97,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbow"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
[](const QString &text, NeoChatRoom *room) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -107,8 +106,8 @@ QList<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Text,
chatBarCache->replyId(),
chatBarCache->editId());
room->chatBoxReplyId(),
room->chatBoxEditId());
return QString();
},
false,
@@ -118,7 +117,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbowme"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
[](const QString &text, NeoChatRoom *room) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -127,8 +126,8 @@ QList<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Emote,
chatBarCache->replyId(),
chatBarCache->editId());
room->chatBoxReplyId(),
room->chatBoxEditId());
return QString();
},
false,
@@ -138,7 +137,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("plain"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {});
return QString();
},
@@ -149,13 +148,13 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
[](const QString &text, NeoChatRoom *room) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
chatBarCache->replyId(),
chatBarCache->editId());
room->chatBoxReplyId(),
room->chatBoxEditId());
return QString();
},
false,
@@ -165,7 +164,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("me"),
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
[](const QString &text, NeoChatRoom *) {
return text;
},
true,
@@ -175,7 +174,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("notice"),
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
[](const QString &text, NeoChatRoom *) {
return text;
},
true,
@@ -185,7 +184,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("invite"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -221,7 +220,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("join"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -245,7 +244,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("knock"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
auto parts = text.split(QLatin1String(" "));
QString roomName = parts[0];
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
@@ -277,7 +276,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -316,7 +315,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("nick"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -347,7 +346,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ignore"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -375,7 +374,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unignore"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -403,8 +402,9 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("react"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
if (chatBarCache->replyId().isEmpty()) {
[](const QString &text, NeoChatRoom *room) {
QString replyEventId = room->chatBoxReplyId();
if (replyEventId.isEmpty()) {
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
const auto &evt = **it;
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
@@ -413,7 +413,7 @@ QList<ActionsModel::Action> actions{
}
}
}
room->toggleReaction(chatBarCache->replyId(), text);
room->toggleReaction(replyEventId, text);
return QString();
},
false,
@@ -423,7 +423,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ban"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -462,7 +462,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unban"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -495,7 +495,7 @@ QList<ActionsModel::Action> actions{
},
Action{
QStringLiteral("kick"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
[](const QString &text, NeoChatRoom *room) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -574,7 +574,7 @@ QHash<int, QByteArray> ActionsModel::roleNames() const
};
}
QList<Action> &ActionsModel::allActions() const
QVector<Action> &ActionsModel::allActions() const
{
return actions;
}

View File

@@ -7,7 +7,6 @@
#include <QAbstractListModel>
#include <Quotient/events/roommessageevent.h>
class ChatBarCache;
class NeoChatRoom;
/**
@@ -29,7 +28,7 @@ public:
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *, ChatBarCache *)> handle;
std::function<QString(const QString &, NeoChatRoom *)> handle;
/**
* @brief Whether the action is a message type action.
*
@@ -88,7 +87,7 @@ public:
/**
* @brief Return a vector with all supported actions.
*/
QList<Action> &allActions() const;
QVector<Action> &allActions() const;
private:
ActionsModel() = default;

View File

@@ -51,9 +51,8 @@ public:
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
static CustomEmojiModel *create(QQmlEngine *, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}

View File

@@ -98,6 +98,6 @@ Q_SIGNALS:
private:
void fetchDevices();
QList<Quotient::Device> m_devices;
QVector<Quotient::Device> m_devices;
QPointer<Quotient::Connection> m_connection;
};

View File

@@ -14,8 +14,6 @@
EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent)
, m_config(KSharedConfig::openStateConfig())
, m_configGroup(KConfigGroup(m_config, QStringLiteral("Editor")))
{
if (_emojis.isEmpty()) {
#include "emojis.h"
@@ -63,9 +61,9 @@ QHash<int, QByteArray> EmojiModel::roleNames() const
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
}
QStringList EmojiModel::lastUsedEmojis() const
QVariantList EmojiModel::history() const
{
return m_configGroup.readEntry(QStringLiteral("lastUsedEmojis"), QStringList());
return m_settings.value(QStringLiteral("Editor/emojis"), QVariantList()).toList();
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
@@ -95,21 +93,19 @@ QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
void EmojiModel::emojiUsed(const QVariant &modelData)
{
auto list = lastUsedEmojis();
const auto emoji = modelData.value<Emoji>();
QVariantList list = history();
auto it = list.begin();
while (it != list.end()) {
if (*it == emoji.shortName) {
if ((*it).value<Emoji>().unicode == modelData.value<Emoji>().unicode) {
it = list.erase(it);
} else {
it++;
}
}
list.push_front(emoji.shortName);
m_configGroup.writeEntry(QStringLiteral("lastUsedEmojis"), list);
list.push_front(modelData);
m_settings.setValue(QStringLiteral("Editor/emojis"), list);
Q_EMIT historyChanged();
}
@@ -117,11 +113,11 @@ void EmojiModel::emojiUsed(const QVariant &modelData)
QVariantList EmojiModel::emojis(Category category) const
{
if (category == History) {
return emojiHistory();
return history();
}
if (category == HistoryNoCustom) {
QVariantList list;
for (const auto &e : emojiHistory()) {
for (const auto &e : history()) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
@@ -221,19 +217,4 @@ QVariantList EmojiModel::categoriesWithCustom() const
return cats;
}
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
for (const auto &historicEmoji : lastUsedEmojis()) {
for (const auto &emojiCategory : _emojis) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);
}
}
}
}
return list;
}
#include "moc_emojimodel.cpp"

View File

@@ -3,11 +3,10 @@
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
#include <QSettings>
struct Emoji {
Emoji(QString unicode, QString shortname, bool isCustom = false)
@@ -24,6 +23,21 @@ struct Emoji {
}
Emoji() = default;
friend QDataStream &operator<<(QDataStream &arch, const Emoji &object)
{
arch << object.unicode;
arch << object.shortName;
return arch;
}
friend QDataStream &operator>>(QDataStream &arch, Emoji &object)
{
arch >> object.unicode;
arch >> object.shortName;
object.isCustom = object.unicode.startsWith(QStringLiteral("image://"));
return arch;
}
QString unicode;
QString shortName;
QString description;
@@ -49,6 +63,11 @@ class EmojiModel : public QAbstractListModel
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of recently used emojis.
*/
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
/**
* @brief Return a list of emoji categories.
*
@@ -67,9 +86,8 @@ public:
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
static EmojiModel *create(QQmlEngine *, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
@@ -158,11 +176,7 @@ public:
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
/**
* @brief Return a list of the last used emoji shortnames
*/
QStringList lastUsedEmojis() const;
Q_INVOKABLE QVariantList history() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;
@@ -175,10 +189,7 @@ public Q_SLOTS:
private:
static QHash<Category, QVariantList> _emojis;
/// Returns QVariants containing the last used Emojis
QVariantList emojiHistory() const;
KSharedConfig::Ptr m_config;
KConfigGroup m_configGroup;
// TODO: Port away from QSettings
QSettings m_settings;
EmojiModel(QObject *parent = nullptr);
};

View File

@@ -151,7 +151,7 @@ void ImagePacksModel::setShowEmoticons(bool showEmoticons)
m_showEmoticons = showEmoticons;
Q_EMIT showEmoticonsChanged();
}
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
QVector<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
{
if (index < 0 || index >= m_events.size()) {
return {};

View File

@@ -5,9 +5,9 @@
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QList>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
class NeoChatRoom;
@@ -86,7 +86,7 @@ public:
/**
* @brief Return a vector of the images in the pack at the given index.
*/
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
[[nodiscard]] QVector<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
Q_SIGNALS:
void roomChanged();
@@ -96,7 +96,7 @@ Q_SIGNALS:
private:
QPointer<NeoChatRoom> m_room;
QList<Quotient::ImagePackEventContent> m_events;
QVector<Quotient::ImagePackEventContent> m_events;
bool m_showStickers = true;
bool m_showEmoticons = true;
void reloadImages();

View File

@@ -35,7 +35,7 @@ void KeywordNotificationRuleModel::updateNotificationRules(const QString &type)
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"].toObject());
const QList<Quotient::PushRule> contentRules = ruleData.content;
const QVector<Quotient::PushRule> contentRules = ruleData.content;
beginResetModel();
m_notificationRules.clear();
@@ -78,11 +78,11 @@ void KeywordNotificationRuleModel::addKeyword(const QString &keyword)
NotificationsManager::instance().initializeKeywordNotificationAction();
}
const QList<QVariant> actions = NotificationsManager::instance().getKeywordNotificationActions();
const QVector<QVariant> actions = NotificationsManager::instance().getKeywordNotificationActions();
auto job = Controller::instance()
.activeConnection()
->callApi<Quotient::SetPushRuleJob>("global", "content", keyword, actions, "", "", QList<Quotient::PushCondition>(), keyword);
->callApi<Quotient::SetPushRuleJob>("global", "content", keyword, actions, "", "", QVector<Quotient::PushCondition>(), keyword);
connect(job, &Quotient::BaseJob::success, this, [this, keyword]() {
beginInsertRows(QModelIndex(), m_notificationRules.count(), m_notificationRules.count());
m_notificationRules.append(keyword);

View File

@@ -47,8 +47,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ReplyDelegateTypeRole] = "replyDelegateType";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[IsThreadedRole] = "isThreaded";
roles[ThreadRootRole] = "threadRoot";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
@@ -57,6 +55,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
roles[AuthorIdRole] = "authorId";
roles[VerifiedRole] = "verified";
roles[AuthorDisplayNameRole] = "authorDisplayName";
roles[IsRedactedRole] = "isRedacted";
@@ -100,9 +99,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
createEventObjects(roomMessageEvent);
}
createEventObjects(&*event->viewAs<RoomMessageEvent>());
}
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
@@ -122,8 +119,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
for (auto &&event : events) {
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
if (message != nullptr) {
createEventObjects(message);
if (NeoChatConfig::self()->showFancyEffects()) {
QString planBody = message->plainBody();
// snowflake
@@ -159,9 +157,8 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
});
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
createEventObjects(roomMessageEvent);
}
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
@@ -230,9 +227,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) {
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
createEventObjects(event);
}
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
}
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
});
@@ -272,7 +267,7 @@ int MessageEventModel::timelineBaseIndex() const
return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0;
}
void MessageEventModel::refreshEventRoles(int row, const QList<int> &roles)
void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles)
{
const auto idx = index(row);
Q_EMIT dataChanged(idx, idx, roles);
@@ -316,7 +311,7 @@ void MessageEventModel::moveReadMarker(const QString &toEventId)
endMoveRows();
}
int MessageEventModel::refreshEventRoles(const QString &id, const QList<int> &roles)
int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &roles)
{
// On 64-bit platforms, difference_type for std containers is long long
// but Qt uses int throughout its interfaces; hence casting to int below.
@@ -592,14 +587,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getReplyMediaInfo();
}
if (role == IsThreadedRole) {
return eventHandler.isThreaded();
}
if (role == ThreadRootRole) {
return eventHandler.threadRoot();
}
if (role == ShowAuthorRole) {
for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r);
@@ -672,6 +659,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_reactionModels.contains(evt.id());
}
if (role == AuthorIdRole) {
return evt.senderId();
}
if (role == VerifiedRole) {
if (evt.originalEvent()) {
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
@@ -708,6 +699,10 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return;
}
auto eventId = event->id();
EventHandler eventHandler;

View File

@@ -63,9 +63,6 @@ public:
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
IsThreadedRole,
ThreadRootRole,
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -76,6 +73,8 @@ public:
ReactionRole, /**< List model for this event. */
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
AuthorIdRole, /**< Matrix ID of the message author. */
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
IsRedactedRole, /**< Whether an event has been deleted. */
@@ -140,8 +139,8 @@ private:
void fetchMore(const QModelIndex &parent) override;
void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QList<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
void refreshEventRoles(int row, const QVector<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
void createEventObjects(const Quotient::RoomMessageEvent *event);

View File

@@ -124,20 +124,6 @@ void PublicRoomListModel::setKeyword(const QString &value)
Q_EMIT hasMoreChanged();
}
bool PublicRoomListModel::showOnlySpaces() const
{
return m_showOnlySpaces;
}
void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
{
if (showOnlySpaces == m_showOnlySpaces) {
return;
}
m_showOnlySpaces = showOnlySpaces;
Q_EMIT showOnlySpacesChanged();
}
void PublicRoomListModel::next(int count)
{
if (count < 1) {
@@ -150,11 +136,7 @@ void PublicRoomListModel::next(int count)
return;
}
QStringList roomTypes;
if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space");
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes});
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, {}});
Q_EMIT loadingChanged();
connect(job, &BaseJob::finished, this, [this] {

View File

@@ -45,11 +45,6 @@ class PublicRoomListModel : public QAbstractListModel
*/
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether only space rooms should be shown.
*/
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/**
* @brief Whether the model has more items to load.
*/
@@ -108,9 +103,6 @@ public:
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool hasMore() const;
[[nodiscard]] bool loading() const;
@@ -126,13 +118,12 @@ private:
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
bool attempted = false;
bool m_loading = false;
QString nextBatch;
QList<Quotient::PublicRoomsChunk> rooms;
QVector<Quotient::PublicRoomsChunk> rooms;
Quotient::QueryPublicRoomsJob *job = nullptr;
@@ -140,7 +131,6 @@ Q_SIGNALS:
void connectionChanged();
void serverChanged();
void keywordChanged();
void showOnlySpacesChanged();
void hasMoreChanged();
void loadingChanged();
};

View File

@@ -106,7 +106,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
endResetModel();
}
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
void PushRuleModel::setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
{
for (const auto &rule : rules) {
QString roomId;
@@ -307,8 +307,8 @@ void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction:
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
{
PushNotificationKind::Kind kind = PushNotificationKind::Content;
const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QList<Quotient::PushCondition> pushConditions;
const QVector<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QVector<Quotient::PushCondition> pushConditions;
if (!roomId.isEmpty()) {
kind = PushNotificationKind::Override;
@@ -369,7 +369,7 @@ void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QStrin
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QList<QVariant> actions;
QVector<QVariant> actions;
if (ruleId == QStringLiteral(".m.rule.call")) {
actions = actionToVariant(action, QStringLiteral("ring"));
} else {
@@ -379,7 +379,7 @@ void PushRuleModel::setNotificationRuleActions(const QString &kind, const QStrin
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
}
PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVariant> &actions, bool enabled)
{
bool notify = false;
bool isNoisy = false;
@@ -422,16 +422,16 @@ PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVaria
}
}
QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
QVector<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) {
Q_ASSERT(false);
return QList<QVariant>();
return QVector<QVariant>();
}
QList<QVariant> actions;
QVector<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append(QStringLiteral("notify"));

View File

@@ -238,14 +238,14 @@ private:
PushNotificationAction::Action m_defaultKeywordAction;
QList<Rule> m_rules;
void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
void setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
int getRuleIndex(const QString &ruleId) const;
PushNotificationSection::Section getSection(Quotient::PushRule rule);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
PushNotificationAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
QList<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
PushNotificationAction::Action variantToAction(const QVector<QVariant> &actions, bool enabled);
QVector<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
};
Q_DECLARE_METATYPE(PushRuleModel *)

View File

@@ -4,12 +4,6 @@
#include "reactionmodel.h"
#include <QDebug>
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <KLocalizedString>
@@ -35,38 +29,11 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
const auto &reaction = m_reactions.at(index.row());
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
const auto reactionText = isEmoji(reaction.reaction)
? QStringLiteral("<span style=\"font-family: 'emoji';\">") + reaction.reaction + QStringLiteral("</span>")
: reaction.reaction;
if (role == TextContentRole) {
if (role == TextRole) {
if (reaction.authors.count() > 1) {
return QStringLiteral("%1 %2").arg(reactionText, QString::number(reaction.authors.count()));
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
} else {
return reactionText;
return reaction.reaction;
}
}
@@ -97,7 +64,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
"%2 reacted with %3",
reaction.authors.count(),
text,
reactionText);
reaction.reaction);
return text;
}
@@ -134,7 +101,7 @@ void ReactionModel::setReactions(QList<Reaction> reactions)
QHash<int, QByteArray> ReactionModel::roleNames() const
{
return {
{TextContentRole, "textContent"},
{TextRole, "text"},
{ReactionRole, "reaction"},
{ToolTipRole, "toolTip"},
{AuthorsRole, "authors"},

View File

@@ -34,7 +34,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
TextContentRole = Qt::DisplayRole, /**< The text to show in the reaction. */
TextRole = Qt::DisplayRole, /**< The text to show in the reaction. */
ReactionRole, /**< The reaction emoji. */
ToolTipRole, /**< The tool tip to show for the reaction. */
AuthorsRole, /**< The list of authors who sent the given reaction. */

View File

@@ -368,14 +368,11 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == IsChildSpaceRole) {
return SpaceHierarchyCache::instance().isChildSpace(room->id());
}
if (role == ReplacementIdRole) {
return room->successorId();
}
return QVariant();
}
void RoomListModel::refresh(NeoChatRoom *room, const QList<int> &roles)
void RoomListModel::refresh(NeoChatRoom *room, const QVector<int> &roles)
{
const auto it = std::find(m_rooms.begin(), m_rooms.end(), room);
if (it == m_rooms.end()) {

View File

@@ -79,7 +79,6 @@ public:
RoomIdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
};
Q_ENUM(EventRoles)
@@ -159,7 +158,7 @@ private Q_SLOTS:
void doAddRoom(Quotient::Room *room);
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
void deleteRoom(Quotient::Room *room);
void refresh(NeoChatRoom *room, const QList<int> &roles = {});
void refresh(NeoChatRoom *room, const QVector<int> &roles = {});
void refreshNotificationCount();
void refreshHighlightCount();

View File

@@ -11,13 +11,12 @@
#include <KConfig>
#include <KConfigGroup>
#include <KSharedConfig>
ServerListModel::ServerListModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
QString domain = Controller::instance().activeConnection()->domain();
@@ -92,8 +91,8 @@ int ServerListModel::rowCount(const QModelIndex &parent) const
void ServerListModel::checkServer(const QString &url)
{
const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
if (!serverGroup.hasKey(url)) {
if (Quotient::isJobPending(m_checkServerJob)) {
@@ -109,8 +108,8 @@ void ServerListModel::checkServer(const QString &url)
void ServerListModel::addServer(const QString &url)
{
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
if (!serverGroup.hasKey(url)) {
Server newServer = Server{
@@ -126,21 +125,17 @@ void ServerListModel::addServer(const QString &url)
}
serverGroup.writeEntry(url, url);
stateConfig->sync();
}
void ServerListModel::removeServerAtIndex(int row)
{
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
serverGroup.deleteEntry(data(index(row), UrlRole).toString());
beginRemoveRows(QModelIndex(), row, row);
m_servers.removeAt(row);
endRemoveRows();
stateConfig->sync();
}
QHash<int, QByteArray> ServerListModel::roleNames() const

View File

@@ -3,7 +3,6 @@
#include "sortfilterroomlistmodel.h"
#include "neochatconnection.h"
#include "roomlistmodel.h"
#include "spacehierarchycache.h"
@@ -15,10 +14,6 @@ SortFilterRoomListModel::SortFilterRoomListModel(QObject *parent)
connect(this, &SortFilterRoomListModel::filterTextChanged, this, [this]() {
invalidateFilter();
});
connect(this, &SortFilterRoomListModel::sourceModelChanged, this, [this]() {
connect(sourceModel(), &QAbstractListModel::rowsInserted, this, &SortFilterRoomListModel::invalidateRowsFilter);
connect(sourceModel(), &QAbstractListModel::rowsRemoved, this, &SortFilterRoomListModel::invalidateRowsFilter);
});
}
void SortFilterRoomListModel::setRoomSortOrder(SortFilterRoomListModel::RoomSortOrder sortOrder)
@@ -83,15 +78,9 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
{
Q_UNUSED(source_parent);
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())
->connection()
->room(sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::ReplacementIdRole).toString())) {
return false;
}
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != QStringLiteral("upgraded")
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) {

View File

@@ -12,7 +12,7 @@ SortFilterSpaceListModel::SortFilterSpaceListModel(QObject *parent)
sort(0);
invalidateFilter();
connect(this, &QAbstractProxyModel::sourceModelChanged, this, [this]() {
connect(sourceModel(), &QAbstractListModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, QList<int> roles) {
connect(sourceModel(), &QAbstractListModel::dataChanged, this, [this](const QModelIndex &, const QModelIndex &, QVector<int> roles) {
if (roles.contains(RoomListModel::IsChildSpaceRole)) {
invalidate();
}

View File

@@ -1,334 +0,0 @@
// 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 "spacechildrenmodel.h"
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/room.h>
#include "controller.h"
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent)
{
m_rootItem = new SpaceTreeItem();
}
SpaceChildrenModel::~SpaceChildrenModel()
{
delete m_rootItem;
}
NeoChatRoom *SpaceChildrenModel::space() const
{
return m_space;
}
void SpaceChildrenModel::setSpace(NeoChatRoom *space)
{
if (space == m_space) {
return;
}
// disconnect the new room signal from the old connection in case it is different.
if (m_space != nullptr) {
disconnect(m_space->connection(), &Quotient::Connection::loadedRoomState, this, nullptr);
}
m_space = space;
Q_EMIT spaceChanged();
for (auto job : m_currentJobs) {
if (job) {
job->abandon();
}
}
m_currentJobs.clear();
auto connection = m_space->connection();
connect(connection, &Quotient::Connection::loadedRoomState, this, [this](Quotient::Room *room) {
if (m_pendingChildren.contains(room->name())) {
m_pendingChildren.removeAll(room->name());
refreshModel();
}
});
connect(m_space, &Quotient::Room::changed, this, [this]() {
refreshModel();
});
refreshModel();
}
bool SpaceChildrenModel::loading() const
{
return m_loading;
}
void SpaceChildrenModel::refreshModel()
{
beginResetModel();
m_replacedRooms.clear();
delete m_rootItem;
m_loading = true;
Q_EMIT loadingChanged();
m_rootItem = new SpaceTreeItem(nullptr, m_space->id(), m_space->displayName(), m_space->canonicalAlias());
endResetModel();
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(m_space->id(), Quotient::none, Quotient::none, 1);
m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, job]() {
insertChildren(job->rooms());
});
}
void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent)
{
SpaceTreeItem *parentItem = getItem(parent);
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
children.erase(children.begin());
}
// If this is the first set of children added to the root item then we need to
// set it so that we are no longer loading.
if (rowCount(QModelIndex()) == 0 && !children.empty()) {
m_loading = false;
Q_EMIT loadingChanged();
}
beginInsertRows(parent, parentItem->childCount(), parentItem->childCount() + children.size() - 1);
for (unsigned long i = 0; i < children.size(); ++i) {
if (children[i].roomId == m_space->id() || children[i].roomId == parentItem->id()) {
continue;
} else {
int insertRow = parentItem->childCount();
if (const auto room = m_space->connection()->room(children[i].roomId)) {
const auto predecessorId = room->predecessorId();
if (!predecessorId.isEmpty()) {
m_replacedRooms += predecessorId;
}
const auto successorId = room->successorId();
if (!successorId.isEmpty()) {
m_replacedRooms += successorId;
}
}
parentItem->insertChild(insertRow,
new SpaceTreeItem(parentItem,
children[i].roomId,
children[i].name,
children[i].canonicalAlias,
children[i].topic,
children[i].numJoinedMembers,
children[i].avatarUrl,
children[i].guestCanJoin,
children[i].worldReadable,
children[i].roomType == QLatin1String("m.space")));
if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
insertChildren(job->rooms(), index(insertRow, 0, parent));
});
}
}
}
endInsertRows();
}
SpaceTreeItem *SpaceChildrenModel::getItem(const QModelIndex &index) const
{
if (index.isValid()) {
SpaceTreeItem *item = static_cast<SpaceTreeItem *>(index.internalPointer());
if (item) {
return item;
}
}
return m_rootItem;
}
QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
SpaceTreeItem *child = getItem(index);
if (role == DisplayNameRole) {
auto displayName = child->name();
if (!displayName.isEmpty()) {
return displayName;
}
displayName = child->canonicalAlias();
if (!displayName.isEmpty()) {
return displayName;
}
return child->id();
}
if (role == AvatarUrlRole) {
return child->avatarUrl();
}
if (role == TopicRole) {
return child->topic();
}
if (role == RoomIDRole) {
return child->id();
}
if (role == AliasRole) {
return child->canonicalAlias();
}
if (role == MemberCountRole) {
return child->memberCount();
}
if (role == AllowGuestsRole) {
return child->allowGuests();
}
if (role == WorldReadableRole) {
return child->worldReadable();
}
if (role == IsJoinedRole) {
return child->isJoined();
}
if (role == IsSpaceRole) {
return child->isSpace();
}
if (role == CanAddChildrenRole) {
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
return room->canSendState(QLatin1String("m.space.child"));
}
return false;
}
if (role == ParentDisplayNameRole) {
const auto parent = child->parentItem();
auto displayName = parent->name();
if (!displayName.isEmpty()) {
return displayName;
}
displayName = parent->canonicalAlias();
if (!displayName.isEmpty()) {
return displayName;
}
return parent->id();
}
if (role == CanSetParentRole) {
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
return room->canSendState(QLatin1String("m.space.parent"));
}
return false;
}
if (role == IsDeclaredParentRole) {
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
return room->currentState().contains(QLatin1String("m.space.parent"), child->parentItem()->id());
}
return false;
}
if (role == CanRemove) {
const auto parent = child->parentItem();
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(parent->id()))) {
return room->canSendState(QLatin1String("m.space.child"));
}
return false;
}
if (role == ParentRoomRole) {
if (const auto parentRoom = static_cast<NeoChatRoom *>(m_space->connection()->room(child->parentItem()->id()))) {
return QVariant::fromValue(parentRoom);
}
return QVariant::fromValue(nullptr);
}
return {};
}
QModelIndex SpaceChildrenModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
SpaceTreeItem *parentItem = getItem(parent);
if (!parentItem) {
return QModelIndex();
}
SpaceTreeItem *childItem = parentItem->child(row);
if (childItem) {
return createIndex(row, column, childItem);
}
return QModelIndex();
}
QModelIndex SpaceChildrenModel::parent(const QModelIndex &index) const
{
if (!index.isValid()) {
return QModelIndex();
}
SpaceTreeItem *childItem = static_cast<SpaceTreeItem *>(index.internalPointer());
SpaceTreeItem *parentItem = childItem->parentItem();
if (parentItem == m_rootItem) {
return QModelIndex();
}
return createIndex(parentItem->row(), 0, parentItem);
}
int SpaceChildrenModel::rowCount(const QModelIndex &parent) const
{
SpaceTreeItem *parentItem;
if (parent.column() > 0) {
return 0;
}
if (!parent.isValid()) {
parentItem = m_rootItem;
} else {
parentItem = static_cast<SpaceTreeItem *>(parent.internalPointer());
}
return parentItem->childCount();
}
int SpaceChildrenModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[DisplayNameRole] = "displayName";
roles[AvatarUrlRole] = "avatarUrl";
roles[TopicRole] = "topic";
roles[RoomIDRole] = "roomId";
roles[MemberCountRole] = "memberCount";
roles[AllowGuestsRole] = "allowGuests";
roles[WorldReadableRole] = "worldReadable";
roles[IsJoinedRole] = "isJoined";
roles[AliasRole] = "alias";
roles[IsSpaceRole] = "isSpace";
roles[CanAddChildrenRole] = "canAddChildren";
roles[ParentDisplayNameRole] = "parentDisplayName";
roles[CanSetParentRole] = "canSetParent";
roles[IsDeclaredParentRole] = "isDeclaredParent";
roles[CanRemove] = "canRemove";
roles[ParentRoomRole] = "parentRoom";
return roles;
}
bool SpaceChildrenModel::isRoomReplaced(const QString &roomId) const
{
return m_replacedRooms.contains(roomId);
}
void SpaceChildrenModel::addPendingChild(const QString &childName)
{
m_pendingChildren += childName;
}
#include "moc_spacechildrenmodel.cpp"

View File

@@ -1,144 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractItemModel>
#include <QQmlEngine>
#include <Quotient/csapi/space_hierarchy.h>
#include <qtmetamacros.h>
#include "neochatroom.h"
#include "spacetreeitem.h"
/**
* @class SpaceChildrenModel
*
* Create a model that contains a list of the child rooms for any given space id.
*/
class SpaceChildrenModel : public QAbstractItemModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current space that the hierarchy is being generated for.
*/
Q_PROPERTY(NeoChatRoom *space READ space WRITE setSpace NOTIFY spaceChanged)
/**
* @brief Whether the model is loading the initial set of children.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
enum Roles {
DisplayNameRole = Qt::DisplayRole,
AvatarUrlRole,
TopicRole,
RoomIDRole,
AliasRole,
MemberCountRole,
AllowGuestsRole,
WorldReadableRole,
IsJoinedRole,
IsSpaceRole,
CanAddChildrenRole,
ParentDisplayNameRole,
CanSetParentRole,
IsDeclaredParentRole,
CanRemove,
ParentRoomRole,
};
explicit SpaceChildrenModel(QObject *parent = nullptr);
~SpaceChildrenModel();
NeoChatRoom *space() const;
void setSpace(NeoChatRoom *space);
bool loading() const;
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
/**
* @brief Returns the index of the item in the model specified by the given row, column and parent index.
*
* @sa QAbstractItemModel::index
*/
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns the parent of the model item with the given index.
*
* If the item has no parent, an invalid QModelIndex is returned.
*
* @sa QAbstractItemModel::parent
*/
QModelIndex parent(const QModelIndex &index) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Number of columns in the model.
*
* @sa QAbstractItemModel::columnCount
*/
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
QHash<int, QByteArray> roleNames() const override;
/**
* @brief Whether the room has been replaced.
*
* @note This information is only available if the local user is either a member
* of the replaced room or is a member of the successor room as currently
* there is no other way to obtain the required information.
*/
bool isRoomReplaced(const QString &roomId) const;
/**
* @brief Add the name of new child room that is expected to be added soon.
*
* A pending child is one where Quotient::Connection::createRoom has been called
* but the room hasn't synced with the server yet. This list is used to check
* whether a new room loading should trigger a refresh of the model, as we only
* want to trigger a refresh if the loading room is part of this space.
*/
Q_INVOKABLE void addPendingChild(const QString &childName);
Q_SIGNALS:
void spaceChanged();
void loadingChanged();
private:
NeoChatRoom *m_space = nullptr;
SpaceTreeItem *m_rootItem;
bool m_loading = false;
QList<QPointer<Quotient::GetSpaceHierarchyJob>> m_currentJobs;
QList<QString> m_pendingChildren;
QList<QString> m_replacedRooms;
SpaceTreeItem *getItem(const QModelIndex &index) const;
void refreshModel();
void insertChildren(std::vector<Quotient::GetSpaceHierarchyJob::ChildRoomsChunk> children, const QModelIndex &parent = QModelIndex());
};

View File

@@ -1,46 +0,0 @@
// 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 "spacechildsortfiltermodel.h"
#include "spacechildrenmodel.h"
SpaceChildSortFilterModel::SpaceChildSortFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
setRecursiveFilteringEnabled(true);
sort(0);
}
void SpaceChildSortFilterModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
invalidateFilter();
}
QString SpaceChildSortFilterModel::filterText() const
{
return m_filterText;
}
bool SpaceChildSortFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (!source_left.data(SpaceChildrenModel::IsSpaceRole).toBool() && source_right.data(SpaceChildrenModel::IsSpaceRole).toBool()) {
return false;
}
return true;
}
bool SpaceChildSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (auto sourceModel = static_cast<SpaceChildrenModel *>(this->sourceModel())) {
bool isReplaced = sourceModel->isRoomReplaced(index.data(SpaceChildrenModel::RoomIDRole).toString());
bool acceptRoom = index.data(SpaceChildrenModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive);
return !isReplaced && acceptRoom;
}
return true;
}
#include "moc_spacechildsortfiltermodel.cpp"

View File

@@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class SpaceChildSortFilterModel
*
* This class creates a custom QSortFilterProxyModel for filtering and sorting spaces
* in a SpaceChildrenModel.
*
* @sa SpaceChildrenModel
*/
class SpaceChildSortFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The text to use to filter room names.
*/
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
public:
SpaceChildSortFilterModel(QObject *parent = nullptr);
void setFilterText(const QString &filterText);
[[nodiscard]] QString filterText() const;
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
*
* @sa QSortFilterProxyModel::lessThan
*/
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
/**
* @brief Custom filter function checking if an event type has been filtered out.
*
* The filter rejects a row if the room is known been replaced or if a search
* string is set it will only return rooms that match.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
Q_SIGNALS:
void filterTextChanged();
private:
QString m_filterText;
};

View File

@@ -1,140 +0,0 @@
// 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 "spacetreeitem.h"
#include "controller.h"
SpaceTreeItem::SpaceTreeItem(SpaceTreeItem *parent,
const QString &id,
const QString &name,
const QString &canonicalAlias,
const QString &topic,
int memberCount,
const QUrl &avatarUrl,
bool allowGuests,
bool worldReadable,
bool isSpace)
: m_parentItem(parent)
, m_id(id)
, m_name(name)
, m_canonicalAlias(canonicalAlias)
, m_topic(topic)
, m_memberCount(memberCount)
, m_avatarUrl(avatarUrl)
, m_allowGuests(allowGuests)
, m_worldReadable(worldReadable)
, m_isSpace(isSpace)
{
}
SpaceTreeItem::~SpaceTreeItem()
{
qDeleteAll(m_children);
}
SpaceTreeItem *SpaceTreeItem::child(int number)
{
if (number < 0 || number >= m_children.size()) {
return nullptr;
}
return m_children[number];
}
int SpaceTreeItem::childCount() const
{
return m_children.count();
}
bool SpaceTreeItem::insertChild(int row, SpaceTreeItem *newChild)
{
if (row < 0 || row > m_children.size()) {
return false;
}
m_children.insert(row, newChild);
return true;
}
bool SpaceTreeItem::removeChild(int row)
{
if (row < 0 || row >= m_children.size()) {
return false;
}
delete m_children.takeAt(row);
return true;
}
int SpaceTreeItem::row() const
{
if (m_parentItem) {
return m_parentItem->m_children.indexOf(const_cast<SpaceTreeItem *>(this));
}
return 0;
}
SpaceTreeItem *SpaceTreeItem::parentItem()
{
return m_parentItem;
}
QString SpaceTreeItem::id() const
{
return m_id;
}
QString SpaceTreeItem::name() const
{
return m_name;
}
QString SpaceTreeItem::canonicalAlias() const
{
return m_canonicalAlias;
}
QString SpaceTreeItem::topic() const
{
return m_topic;
}
int SpaceTreeItem::memberCount() const
{
return m_memberCount;
}
QUrl SpaceTreeItem::avatarUrl() const
{
if (m_avatarUrl.isEmpty() || m_avatarUrl.scheme() != QLatin1String("mxc")) {
return {};
}
auto connection = Controller::instance().activeConnection();
auto url = connection->makeMediaUrl(m_avatarUrl);
if (url.scheme() == QLatin1String("mxc")) {
return url;
}
return {};
}
bool SpaceTreeItem::allowGuests() const
{
return m_allowGuests;
}
bool SpaceTreeItem::worldReadable() const
{
return m_worldReadable;
}
bool SpaceTreeItem::isJoined() const
{
auto connection = Controller::instance().activeConnection();
if (!connection) {
return false;
}
return connection->room(id(), Quotient::JoinState::Join) != nullptr;
}
bool SpaceTreeItem::isSpace() const
{
return m_isSpace;
}

View File

@@ -1,136 +0,0 @@
// 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 <Quotient/csapi/space_hierarchy.h>
/**
* @class SpaceTreeItem
*
* This class defines an item in the space tree hierarchy model.
*
* @note This is separate from Quotient::Room and NeoChatRoom because we don't have
* full room information for any room/space the user hasn't joined and we
* don't want to create one for ever possible child in a space as that would
* be expensive.
*
* @sa Quotient::Room, NeoChatRoom
*/
class SpaceTreeItem
{
public:
explicit SpaceTreeItem(SpaceTreeItem *parent = nullptr,
const QString &id = {},
const QString &name = {},
const QString &canonicalAlias = {},
const QString &topic = {},
int memberCount = {},
const QUrl &avatarUrl = {},
bool allowGuests = {},
bool worldReadable = {},
bool isSpace = {});
~SpaceTreeItem();
/**
* @brief Return the child at the given row number.
*
* Nullptr is returned if there is no child at the given row number.
*/
SpaceTreeItem *child(int number);
/**
* @brief The number of children this item has.
*/
int childCount() const;
/**
* @brief Insert the given child at the given row number.
*/
bool insertChild(int row, SpaceTreeItem *newChild);
/**
* @brief Remove the child at the given row number.
*
* @return True if a child was removed, false if the given row isn't valid.
*/
bool removeChild(int row);
/**
* @brief Return this item's parent.
*/
SpaceTreeItem *parentItem();
/**
* @brief Return the row number for this child relative to the parent.
*
* @return The row value if the child has a parent, 0 otherwise.
*/
int row() const;
/**
* @brief The ID of the room.
*/
QString id() const;
/**
* @brief The name of the room, if any.
*/
QString name() const;
/**
* @brief The canonical alias of the room, if any.
*/
QString canonicalAlias() const;
/**
* @brief The topic of the room, if any.
*/
QString topic() const;
/**
* @brief The number of members joined to the room.
*/
int memberCount() const;
/**
* @brief The URL for the room's avatar, if one is set.
*
* @return A CS API QUrl.
*/
QUrl avatarUrl() const;
/**
* @brief Whether guest users may join the room and participate in it.
*
* If they can, they will be subject to ordinary power level rules like any other users.
*/
bool allowGuests() const;
/**
* @brief Whether the room may be viewed by guest users without joining.
*/
bool worldReadable() const;
/**
* @brief Whether the local user is a member of the rooom.
*/
bool isJoined() const;
/**
* @brief Whether the room is a space.
*/
bool isSpace() const;
private:
QList<SpaceTreeItem *> m_children;
SpaceTreeItem *m_parentItem;
QString m_id;
QString m_name;
QString m_canonicalAlias;
QString m_topic;
int m_memberCount;
QUrl m_avatarUrl;
bool m_allowGuests;
bool m_worldReadable;
bool m_isSpace;
};

View File

@@ -6,9 +6,9 @@
#include "events/imagepackevent.h"
#include "neochatroom.h"
#include <QAbstractListModel>
#include <QList>
#include <QObject>
#include <QQmlEngine>
#include <QVector>
class ImagePacksModel;
@@ -100,7 +100,7 @@ Q_SIGNALS:
private:
ImagePacksModel *m_model = nullptr;
int m_index = 0;
QList<Quotient::ImagePackEventContent::ImagePackImage> m_images;
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_images;
NeoChatRoom *m_room;
void reloadImages();
};

View File

@@ -103,7 +103,7 @@ private:
bool attempted = false;
QList<Quotient::SearchUserDirectoryJob::User> users;
QVector<Quotient::SearchUserDirectoryJob::User> users;
Quotient::SearchUserDirectoryJob *job = nullptr;
};

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