Compare commits
100 Commits
v24.01.75
...
work/tobia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3d3abc53 | ||
|
|
46fd288a95 | ||
|
|
3a1cac6c99 | ||
|
|
225092a193 | ||
|
|
2fcb17621c | ||
|
|
69e3ee862f | ||
|
|
47c12cb582 | ||
|
|
94cf75af68 | ||
|
|
259b9884c7 | ||
|
|
2980dc49e4 | ||
|
|
5e12f50899 | ||
|
|
79940f707f | ||
|
|
e7024a270c | ||
|
|
1a561f6649 | ||
|
|
2cbb6ed65c | ||
|
|
ab4639926a | ||
|
|
89b6c54f25 | ||
|
|
27c9c62564 | ||
|
|
bb8ffb02d1 | ||
|
|
201aa82c04 | ||
|
|
704430db7a | ||
|
|
dcbbbd9296 | ||
|
|
410258c478 | ||
|
|
6baf2e4888 | ||
|
|
dd6eaac556 | ||
|
|
840903128c | ||
|
|
68f0ca96da | ||
|
|
faf7af06fe | ||
|
|
4ef67c3e0d | ||
|
|
5efd17d370 | ||
|
|
0dbef58ff2 | ||
|
|
6a3df8baf4 | ||
|
|
2fc973f218 | ||
|
|
7d16999c44 | ||
|
|
f9ba31f2dc | ||
|
|
d4ad773ff1 | ||
|
|
822a4dc500 | ||
|
|
bfd1d431c1 | ||
|
|
94bf2481f0 | ||
|
|
5c32520c35 | ||
|
|
de47597f6e | ||
|
|
7780a72888 | ||
|
|
1f8c07fedf | ||
|
|
877575b4d3 | ||
|
|
1181df4db2 | ||
|
|
5be2113b32 | ||
|
|
7ad7af56e8 | ||
|
|
d3148f8c8b | ||
|
|
100f595026 | ||
|
|
15ba6d58e2 | ||
|
|
61ad892732 | ||
|
|
2a3e1dfcd7 | ||
|
|
d1dc6fc4ed | ||
|
|
6dc30a9ca7 | ||
|
|
ae0c5ffaef | ||
|
|
fc546d4a43 | ||
|
|
96e62e3ebe | ||
|
|
d979cd2fbc | ||
|
|
22298181cb | ||
|
|
1312fde470 | ||
|
|
7fe2feb1e4 | ||
|
|
67fb5d0824 | ||
|
|
359114bd3d | ||
|
|
3db9f1198b | ||
|
|
2435a6b953 | ||
|
|
e6c8b3fa4b | ||
|
|
2d55dca508 | ||
|
|
bbb0bc3092 | ||
|
|
4065aa6a2e | ||
|
|
5942eac5ed | ||
|
|
85c2b7dada | ||
|
|
86ef921cdb | ||
|
|
aab69c5bae | ||
|
|
624578ec77 | ||
|
|
197ff984fd | ||
|
|
a26337d5f4 | ||
|
|
31b4eefadd | ||
|
|
b8e592f8ba | ||
|
|
555d23863e | ||
|
|
ffa2d5dc0e | ||
|
|
9987edbaf2 | ||
|
|
d90298392d | ||
|
|
600dbd0603 | ||
|
|
2179e2cc35 | ||
|
|
8119ea3ccb | ||
|
|
3fc125a798 | ||
|
|
99f6778df4 | ||
|
|
f7bd24db34 | ||
|
|
41c2f9c4d5 | ||
|
|
56d5f3036b | ||
|
|
21bb7dce21 | ||
|
|
11081719a7 | ||
|
|
980de7d85d | ||
|
|
7735313b0c | ||
|
|
1a3055df86 | ||
|
|
84cad630cd | ||
|
|
0beb5df08d | ||
|
|
4ef44b8e93 | ||
|
|
59153be006 | ||
|
|
50be96f762 |
10
.craft.ini
Normal file
10
.craft.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
; SPDX-FileCopyrightText: None
|
||||
; SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/unreleased/kirigami-addons.version=master
|
||||
kde/frameworks.version=master
|
||||
kde/libs.version=master
|
||||
kde/plasma.version=master
|
||||
kde/unreleased.version=master
|
||||
libs/qt.qtMajorVersion=6
|
||||
@@ -2,9 +2,12 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
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/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
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
||||
- project: sysadmin/ci-utilities
|
||||
file:
|
||||
- /gitlab-templates/reuse-lint.yml
|
||||
- /gitlab-templates/android-qt6.yml
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
- /gitlab-templates/freebsd-qt6.yml
|
||||
# - /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
|
||||
@@ -27,10 +27,10 @@ Dependencies:
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
|
||||
@@ -45,3 +45,7 @@ License: BSD-2-Clause
|
||||
Files: autotests/data/*
|
||||
Copyright: none
|
||||
License: CC0-1.0
|
||||
|
||||
Files: appiumtests/data/*
|
||||
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
License: CC0-1.0
|
||||
|
||||
@@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
# KDE Applications version, managed by release script.
|
||||
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "01")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "75")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "80")
|
||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
@@ -58,12 +58,12 @@ 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 Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
|
||||
set_package_properties(KF6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
set_package_properties(KF6Kirigami2 PROPERTIES
|
||||
set_package_properties(KF6Kirigami PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
@@ -102,8 +102,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()
|
||||
|
||||
@@ -130,7 +129,7 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
PURPOSE "Add image editing capability to image attachments"
|
||||
)
|
||||
|
||||
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
|
||||
find_package(QCoro6 0.4 COMPONENTS Core Network REQUIRED)
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
@@ -140,6 +139,13 @@ set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
|
||||
find_package(KUnifiedPush QUIET)
|
||||
set_package_properties(KUnifiedPush PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Push notification support"
|
||||
URL "https://invent.kde.org/libraries/kunifiedpush"
|
||||
)
|
||||
|
||||
if(ANDROID)
|
||||
find_package(Sqlite3)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
|
||||
@@ -11,6 +11,7 @@ A Qt/QML based Matrix client.
|
||||
|
||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
|
||||
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -95,7 +96,7 @@ As is the case throughout the KDE ecosystem contributions are welcome from all.
|
||||
|
||||
## Contact
|
||||
|
||||
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org).
|
||||
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://go.kde.org/matrix/#/#neochat:kde.org). See [Matrix](https://community.kde.org/Matrix) for more details.
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ android {
|
||||
exclude 'lib/*/*_imageformats_qtga_*'
|
||||
exclude 'lib/*/*_imageformats_qtiff_*'
|
||||
exclude 'lib/*/*_qmltooling_*'
|
||||
exclude 'lib/*/*_multimedia_ffmpeg*' // temporary qt6 android fix
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
|
||||
@@ -21,3 +21,8 @@ add_test(
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME openuserdetailstest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/openuserdetailstest.py
|
||||
)
|
||||
|
||||
3
appiumtests/data/sync_response_no_rooms.json
Normal file
3
appiumtests/data/sync_response_no_rooms.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"next_batch": "batch1234"
|
||||
}
|
||||
50
appiumtests/data/sync_response_rooms.json
Normal file
50
appiumtests/data/sync_response_rooms.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"next_batch": "batch1234",
|
||||
"rooms": {
|
||||
"join": {
|
||||
"!room_id_1234:localhost:1234": {
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.member",
|
||||
"state_key": "@user:localhost:1234",
|
||||
"sender": "@user:localhost:1234",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"event_id": "$event_id_1234_0:localhost:1234",
|
||||
"room_id": "!room_id_1234:localhost:1234",
|
||||
"content": {
|
||||
"avatar_url": "",
|
||||
"displayname": "A Display Name",
|
||||
"membership": "join",
|
||||
"reason": "Nothing"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"sender": "@user:localhost:1234",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"event_id": "$event_id_1234_1:localhost:1234",
|
||||
"room_id": "!room_id_1234:localhost:1234",
|
||||
"content": {
|
||||
"body": "This is a message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
|
||||
import json
|
||||
from flask import Flask, request, abort
|
||||
import os
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/_matrix/client/v3/login", methods=["GET"])
|
||||
def login_get():
|
||||
result = dict()
|
||||
@@ -12,6 +14,13 @@ def login_get():
|
||||
result["flows"][0]["type"] = "m.login.password"
|
||||
return result
|
||||
|
||||
@app.route("/_matrix/client/v3/account/whoami", methods=["GET"])
|
||||
def whoami():
|
||||
result = dict()
|
||||
result["device_id"] = "device_id_1234"
|
||||
result["user_id"] = "@user:localhost:1234"
|
||||
return result
|
||||
|
||||
@app.route("/_matrix/client/v3/login", methods=["POST"])
|
||||
def login_post():
|
||||
data = request.get_json()
|
||||
@@ -19,15 +28,22 @@ def login_post():
|
||||
abort(403)
|
||||
print(data)
|
||||
result = dict()
|
||||
result["access_token"] = "token_1234"
|
||||
result["access_token"] = "token_login"
|
||||
result["device_id"] = "device_1234"
|
||||
result["user_id"] = "@user:localhost:1234"
|
||||
return result
|
||||
|
||||
def load_json(name):
|
||||
parts = __file__.split("/")
|
||||
parts.pop()
|
||||
datadir = "/".join(parts)
|
||||
return json.loads(open(f"{datadir}/data/{name}.json").read())
|
||||
|
||||
|
||||
@app.route("/_matrix/client/r0/sync")
|
||||
def sync():
|
||||
result = dict()
|
||||
result["next_batch"] = "batch1234"
|
||||
|
||||
result = load_json("sync_response_no_rooms") if ("login" in request.headers.get("Authorization")) else load_json("sync_response_rooms")
|
||||
return result
|
||||
|
||||
@app.route("/.well-known/matrix/client")
|
||||
@@ -37,6 +53,18 @@ def well_known():
|
||||
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
|
||||
return reply
|
||||
|
||||
@app.route("/_matrix/client/v3/profile/<id>")
|
||||
def profile(id):
|
||||
reply = dict()
|
||||
reply["avatar_url"] = "mxc://localhost:1234/asdf1234"
|
||||
reply["displayname"] = "User123"
|
||||
return reply
|
||||
|
||||
@app.route("/_matrix/client/v3/keys/upload", methods=["POST"])
|
||||
def upload_keys():
|
||||
reply = dict()
|
||||
return reply
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(ssl_context='adhoc', port=1234)
|
||||
|
||||
48
appiumtests/openuserdetailstest.py
Executable file
48
appiumtests/openuserdetailstest.py
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
import os
|
||||
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 OpenUserDetailsTest(unittest.TestCase):
|
||||
|
||||
mockServerProcess: subprocess.Popen
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
options = AppiumOptions()
|
||||
options.set_capability("app", "neochat --ignore-ssl-errors --test")
|
||||
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
if not self._outcome.result.wasSuccessful():
|
||||
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
self.mockServerProcess.terminate()
|
||||
self.driver.quit()
|
||||
|
||||
def test_open_sheet(self):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Empty room (!room_id_1234:localhost:1234)").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="A Display Name").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Account Details")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -46,3 +46,9 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME chatdocumenthandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
messageeventmodeltest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME messageeventmodeltest
|
||||
)
|
||||
|
||||
@@ -135,6 +135,21 @@
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSh:example.org",
|
||||
"origin_server_ts": 1432735824659,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@newline:example.org",
|
||||
"state_key": "@newline:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 12345
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -373,6 +388,20 @@
|
||||
"unsigned": {
|
||||
"age": 1238
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "A message from someone who thought it was a good idea to put newlines in their display name.",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456889:example.org",
|
||||
"origin_server_ts": 14327358246589,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@newline:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1230
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
|
||||
@@ -55,6 +55,8 @@ private Q_SLOTS:
|
||||
void nullAuthor();
|
||||
void authorDisplayName();
|
||||
void nullAuthorDisplayName();
|
||||
void singleLineSidplayName();
|
||||
void nullSingleLineDisplayName();
|
||||
void time();
|
||||
void nullTime();
|
||||
void timeString();
|
||||
@@ -203,6 +205,23 @@ void EventHandlerTest::nullAuthorDisplayName()
|
||||
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::singleLineSidplayName()
|
||||
{
|
||||
auto event = room->messageEvents().at(11).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullSingleLineDisplayName()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::time()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
|
||||
130
autotests/messageeventmodeltest.cpp
Normal file
130
autotests/messageeventmodeltest.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "models/messageeventmodel.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 MessageEventModelTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
MessageEventModel *model = nullptr;
|
||||
|
||||
TestRoom *setupTestRoom(const QString &syncFileName);
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void init();
|
||||
|
||||
void switchRoom();
|
||||
void simpleTimeline();
|
||||
void idToRow();
|
||||
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
void MessageEventModelTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
}
|
||||
|
||||
void MessageEventModelTest::init()
|
||||
{
|
||||
QCOMPARE(model, nullptr);
|
||||
model = new MessageEventModel;
|
||||
}
|
||||
|
||||
// Make sure that basic rooms can be switched without crashing.
|
||||
void MessageEventModelTest::switchRoom()
|
||||
{
|
||||
TestRoom *firstRoom = new TestRoom(connection, QStringLiteral("#firstRoom:kde.org"), JoinState::Join);
|
||||
TestRoom *secondRoom = new TestRoom(connection, QStringLiteral("#secondRoom:kde.org"), JoinState::Join);
|
||||
|
||||
QSignalSpy spy(model, SIGNAL(roomChanged()));
|
||||
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
model->setRoom(firstRoom);
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#firstRoom:kde.org"));
|
||||
model->setRoom(secondRoom);
|
||||
QCOMPARE(spy.count(), 2);
|
||||
QCOMPARE(model->room()->id(), QStringLiteral("#secondRoom:kde.org"));
|
||||
model->setRoom(nullptr);
|
||||
QCOMPARE(spy.count(), 3);
|
||||
QCOMPARE(model->room(), nullptr);
|
||||
}
|
||||
|
||||
void MessageEventModelTest::simpleTimeline()
|
||||
{
|
||||
auto room = setupTestRoom(QLatin1String("test-min-sync.json"));
|
||||
|
||||
model->setRoom(room);
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(model->data(model->index(0)), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(model->data(model->index(0), MessageEventModel::DelegateTypeRole), DelegateType::Message);
|
||||
QCOMPARE(model->data(model->index(0), MessageEventModel::PlainText), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(model->data(model->index(0), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
||||
QCOMPARE(model->data(model->index(-1)), QVariant());
|
||||
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
|
||||
QCOMPARE(model->data(model->index(1)), QVariant());
|
||||
}
|
||||
|
||||
void MessageEventModelTest::idToRow()
|
||||
{
|
||||
auto room = setupTestRoom(QLatin1String("test-min-sync.json"));
|
||||
model->setRoom(room);
|
||||
|
||||
QCOMPARE(model->eventIdToRow(QStringLiteral("$153456789:example.org")), 0);
|
||||
}
|
||||
|
||||
void MessageEventModelTest::cleanup()
|
||||
{
|
||||
delete model;
|
||||
model = nullptr;
|
||||
QCOMPARE(model, nullptr);
|
||||
}
|
||||
|
||||
TestRoom *MessageEventModelTest::setupTestRoom(const QString &syncFileName)
|
||||
{
|
||||
auto room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
if (!syncFileName.isEmpty()) {
|
||||
QFile testSyncFile;
|
||||
testSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + syncFileName);
|
||||
testSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(MessageEventModelTest)
|
||||
#include "messageeventmodeltest.moc"
|
||||
@@ -343,6 +343,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.08.4" date="2023-12-07"/>
|
||||
<release version="23.08.3" date="2023-11-09"/>
|
||||
<release version="23.08.2" date="2023-10-12"/>
|
||||
<release version="23.08.0" date="2023-08-24">
|
||||
|
||||
687
po/ar/neochat.po
687
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
678
po/az/neochat.po
678
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
670
po/ca/neochat.po
670
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
711
po/cs/neochat.po
711
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
669
po/da/neochat.po
669
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
668
po/de/neochat.po
668
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
682
po/el/neochat.po
682
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
686
po/eo/neochat.po
686
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
689
po/es/neochat.po
689
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
674
po/eu/neochat.po
674
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
675
po/fi/neochat.po
675
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
701
po/fr/neochat.po
701
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
675
po/hu/neochat.po
675
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
680
po/ia/neochat.po
680
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
675
po/id/neochat.po
675
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
677
po/ie/neochat.po
677
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
712
po/it/neochat.po
712
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
649
po/ja/neochat.po
649
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
673
po/ka/neochat.po
673
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
670
po/ko/neochat.po
670
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
650
po/lt/neochat.po
650
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
677
po/nl/neochat.po
677
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
657
po/nn/neochat.po
657
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
667
po/pa/neochat.po
667
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
696
po/pl/neochat.po
696
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
672
po/pt/neochat.po
672
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
681
po/ru/neochat.po
681
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
678
po/sk/neochat.po
678
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
681
po/sl/neochat.po
681
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
675
po/sv/neochat.po
675
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
673
po/ta/neochat.po
673
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
715
po/tr/neochat.po
715
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
678
po/uk/neochat.po
678
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,6 @@ add_library(neochat STATIC
|
||||
models/customemojimodel.h
|
||||
clipboard.cpp
|
||||
clipboard.h
|
||||
matriximageprovider.cpp
|
||||
matriximageprovider.h
|
||||
models/messageeventmodel.cpp
|
||||
models/messageeventmodel.h
|
||||
models/messagefiltermodel.cpp
|
||||
@@ -139,6 +137,10 @@ add_library(neochat STATIC
|
||||
chatbarcache.h
|
||||
colorschemer.cpp
|
||||
colorschemer.h
|
||||
models/notificationsmodel.cpp
|
||||
models/notificationsmodel.h
|
||||
models/timelinemodel.cpp
|
||||
models/timelinemodel.h
|
||||
)
|
||||
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
@@ -173,7 +175,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/TypingPane.qml
|
||||
qml/QuickSwitcher.qml
|
||||
qml/HoverActions.qml
|
||||
qml/ChatBox.qml
|
||||
qml/ChatBar.qml
|
||||
qml/AttachmentPane.qml
|
||||
qml/ReplyPane.qml
|
||||
@@ -290,11 +291,19 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
qml/Security.qml
|
||||
qml/QrCodeMaximizeComponent.qml
|
||||
qml/SelectSpacesDialog.qml
|
||||
qml/AttachDialog.qml
|
||||
qml/NotificationsView.qml
|
||||
qml/LoadingDelegate.qml
|
||||
qml/TimelineEndDelegate.qml
|
||||
RESOURCES
|
||||
qml/confetti.png
|
||||
qml/glowdot.png
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
|
||||
endif()
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "messageeventmodel_logging.h"
|
||||
IDENTIFIER "MessageEvent"
|
||||
@@ -365,7 +374,7 @@ target_link_libraries(neochat PUBLIC
|
||||
Qt::Network
|
||||
Qt::QuickControls2
|
||||
KF6::I18n
|
||||
KF6::Kirigami2
|
||||
KF6::Kirigami
|
||||
KF6::Notifications
|
||||
KF6::ConfigCore
|
||||
KF6::ConfigGui
|
||||
@@ -376,6 +385,7 @@ target_link_libraries(neochat PUBLIC
|
||||
QuotientQt6
|
||||
cmark::cmark
|
||||
QCoro::Core
|
||||
QCoro::Network
|
||||
)
|
||||
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
@@ -464,7 +474,7 @@ if(ANDROID)
|
||||
"gps"
|
||||
"system-users-symbolic"
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR/android})
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||
@@ -483,9 +493,18 @@ if (TARGET KF6::KIOWidgets)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
if (TARGET KUnifiedPush)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
|
||||
target_link_libraries(neochat PUBLIC KUnifiedPush)
|
||||
|
||||
if (NOT ANDROID)
|
||||
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ uint8_t *decode(const char *blurhash, int width, int height, int punch, int nCha
|
||||
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
|
||||
|
||||
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
return pixelArray;
|
||||
}
|
||||
|
||||
@@ -132,11 +132,7 @@ int ChatDocumentHandler::completionStartIndex() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID)
|
||||
const long long cursor = cursorPosition();
|
||||
#else
|
||||
const auto cursor = cursorPosition();
|
||||
#endif
|
||||
const qsizetype cursor = cursorPosition();
|
||||
const auto &text = getText();
|
||||
|
||||
auto start = std::min(cursor, text.size()) - 1;
|
||||
|
||||
@@ -158,7 +158,6 @@ private:
|
||||
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QPointer<ChatBarCache> m_chatBarCache;
|
||||
bool completionVisible = false;
|
||||
|
||||
QColor m_mentionColor;
|
||||
QColor m_errorColor;
|
||||
@@ -172,7 +171,5 @@ private:
|
||||
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
|
||||
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
|
||||
|
||||
CompletionModel *m_completionModel = nullptr;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
#include "trayicon_sni.h"
|
||||
#endif
|
||||
|
||||
bool testMode = false;
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
Controller::Controller(QObject *parent)
|
||||
@@ -56,9 +58,19 @@ Controller::Controller(QObject *parent)
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
|
||||
#endif
|
||||
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
invokeLogin();
|
||||
});
|
||||
if (!testMode) {
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
invokeLogin();
|
||||
});
|
||||
} else {
|
||||
auto c = new NeoChatConnection(this);
|
||||
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
|
||||
connect(c, &Connection::connected, this, [c, this]() {
|
||||
m_accountRegistry.add(c);
|
||||
c->syncLoop();
|
||||
Q_EMIT initiated();
|
||||
});
|
||||
}
|
||||
|
||||
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
|
||||
delete m_trayIcon;
|
||||
@@ -93,12 +105,30 @@ Controller::Controller(QObject *parent)
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (m_accountRegistry.size() > oldAccountCount) {
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() {
|
||||
NotificationsManager::instance().handleNotifications(connection);
|
||||
});
|
||||
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
|
||||
connection->setupPushNotifications(m_endpoint);
|
||||
});
|
||||
}
|
||||
oldAccountCount = m_accountRegistry.size();
|
||||
});
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
|
||||
connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) {
|
||||
m_endpoint = endpoint;
|
||||
for (auto "ientConnection : m_accountRegistry) {
|
||||
auto connection = dynamic_cast<NeoChatConnection *>(quotientConnection);
|
||||
connection->setupPushNotifications(endpoint);
|
||||
}
|
||||
});
|
||||
|
||||
connector->registerClient(i18n("Receiving push notifications"));
|
||||
|
||||
m_endpoint = connector->endpoint();
|
||||
#endif
|
||||
}
|
||||
|
||||
Controller &Controller::instance()
|
||||
@@ -300,9 +330,6 @@ void Controller::setQuitOnLastWindowClosed()
|
||||
m_trayIcon = nullptr;
|
||||
}
|
||||
}
|
||||
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -326,20 +353,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
|
||||
m_connection = connection;
|
||||
if (connection != nullptr) {
|
||||
NeoChatConfig::self()->setActiveConnection(connection->userId());
|
||||
connect(connection, &NeoChatConnection::networkError, this, [this]() {
|
||||
if (!m_isOnline) {
|
||||
return;
|
||||
}
|
||||
m_isOnline = false;
|
||||
Q_EMIT isOnlineChanged(false);
|
||||
});
|
||||
connect(connection, &NeoChatConnection::syncDone, this, [this] {
|
||||
if (m_isOnline) {
|
||||
return;
|
||||
}
|
||||
m_isOnline = true;
|
||||
Q_EMIT isOnlineChanged(true);
|
||||
});
|
||||
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
|
||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
|
||||
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
|
||||
@@ -357,29 +370,25 @@ void Controller::saveWindowGeometry()
|
||||
WindowController::instance().saveGeometry();
|
||||
}
|
||||
|
||||
bool Controller::isOnline() const
|
||||
{
|
||||
return m_isOnline;
|
||||
}
|
||||
|
||||
// TODO: Remove in favor of RoomManager::joinRoom
|
||||
void Controller::joinRoom(const QString &alias)
|
||||
{
|
||||
if (!alias.contains(":"_ls)) {
|
||||
Q_EMIT errorOccured(i18n("The room id you are trying to join is not valid"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto knownServer = alias.mid(alias.indexOf(":"_ls) + 1);
|
||||
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
|
||||
}
|
||||
|
||||
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
|
||||
{
|
||||
// HACK: Workaround bug QTBUG 93281
|
||||
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
|
||||
}
|
||||
|
||||
void Controller::listenForNotifications()
|
||||
{
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
|
||||
|
||||
connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) {
|
||||
NotificationsManager::instance().postPushNotification(data);
|
||||
});
|
||||
|
||||
connector->registerClient(i18n("Receiving push notifications"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::setApplicationProxy()
|
||||
{
|
||||
NeoChatConfig *cfg = NeoChatConfig::self();
|
||||
@@ -425,3 +434,8 @@ AccountRegistry &Controller::accounts()
|
||||
}
|
||||
|
||||
#include "moc_controller.cpp"
|
||||
|
||||
void Controller::setTestMode(bool test)
|
||||
{
|
||||
testMode = test;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/settings.h>
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
#include <kunifiedpush/connector.h>
|
||||
#endif
|
||||
|
||||
class NeoChatRoom;
|
||||
class TrayIcon;
|
||||
class QQuickTextDocument;
|
||||
@@ -51,11 +55,6 @@ class Controller : public QObject
|
||||
*/
|
||||
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Whether NeoChat is currently able to connect to the server.
|
||||
*/
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether NeoChat is running as a flatpak.
|
||||
*
|
||||
@@ -101,15 +100,8 @@ public:
|
||||
*/
|
||||
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
/**
|
||||
* @brief Join a room.
|
||||
*/
|
||||
Q_INVOKABLE void joinRoom(const QString &alias);
|
||||
|
||||
[[nodiscard]] bool supportSystemTray() const;
|
||||
|
||||
bool isOnline() const;
|
||||
|
||||
/**
|
||||
* @brief Sets the QNetworkProxy for the application.
|
||||
*
|
||||
@@ -126,8 +118,16 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
|
||||
|
||||
/**
|
||||
* @brief Start listening for notifications in dbus-activated mode.
|
||||
* These notifications will quit the application when closed.
|
||||
*/
|
||||
static void listenForNotifications();
|
||||
|
||||
Quotient::AccountRegistry &accounts();
|
||||
|
||||
static void setTestMode(bool testMode);
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -138,11 +138,11 @@ private:
|
||||
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
bool m_isOnline = true;
|
||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||
|
||||
Quotient::AccountRegistry m_accountRegistry;
|
||||
QStringList m_accountsLoading;
|
||||
QString m_endpoint;
|
||||
|
||||
private Q_SLOTS:
|
||||
void invokeLogin();
|
||||
@@ -164,7 +164,6 @@ Q_SIGNALS:
|
||||
void activeConnectionChanged();
|
||||
void passwordStatus(Controller::PasswordStatus status);
|
||||
void userConsentRequired(QUrl url);
|
||||
void isOnlineChanged(bool isOnline);
|
||||
void accountsLoadingChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
|
||||
@@ -40,6 +40,8 @@ public:
|
||||
Poll, /**< The initial event for a poll. */
|
||||
Location, /**< A location event. */
|
||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
||||
Loading, /**< A delegate to tell the user more messages are being loaded. */
|
||||
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(Type);
|
||||
|
||||
@@ -169,6 +169,28 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
auto displayName = m_room->htmlSafeMemberName(author->id());
|
||||
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
|
||||
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
|
||||
displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
|
||||
displayName.replace(QStringLiteral("<br />"), QStringLiteral(" "));
|
||||
displayName.replace(u'\n', QStringLiteral(" "));
|
||||
displayName.replace(u'\u2028', QStringLiteral(" "));
|
||||
return displayName;
|
||||
}
|
||||
|
||||
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
@@ -663,12 +685,12 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
// Get the mxc URL for the media.
|
||||
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
||||
if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
||||
|
||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
||||
if (source.isValid()) {
|
||||
mediaInfo["source"_ls] = source;
|
||||
} else {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
|
||||
@@ -111,6 +111,17 @@ public:
|
||||
*/
|
||||
QString getAuthorDisplayName(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Get the display name of the event author but with any newlines removed.
|
||||
*
|
||||
* Turns out you can put newlines in your display name so we need to handle that
|
||||
* primarily for the room list subtitle.
|
||||
*
|
||||
* @param isPending whether the event is pending as this cannot be derived from
|
||||
* just the event object.
|
||||
*/
|
||||
QString singleLineAuthorDisplayname(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Return a QDateTime object for the event timestamp.
|
||||
*/
|
||||
|
||||
49
src/main.cpp
49
src/main.cpp
@@ -5,6 +5,7 @@
|
||||
#include <QIcon>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
@@ -40,7 +41,6 @@
|
||||
#include "colorschemer.h"
|
||||
#include "controller.h"
|
||||
#include "logger.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "roommanager.h"
|
||||
#include "windowcontroller.h"
|
||||
@@ -171,6 +171,40 @@ int main(int argc, char *argv[])
|
||||
colorScheme.apply(NeoChatConfig::self()->colorScheme());
|
||||
}
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
||||
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
|
||||
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
||||
|
||||
QCommandLineOption testOption("test"_ls, i18n("Only used for autotests"));
|
||||
testOption.setFlags(QCommandLineOption::HiddenFromHelp);
|
||||
parser.addOption(testOption);
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
QCommandLineOption dbusActivatedOption(QStringLiteral("dbus-activated"), i18n("Internal usage only."));
|
||||
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
|
||||
parser.addOption(dbusActivatedOption);
|
||||
#endif
|
||||
|
||||
about.setupCommandLine(&parser);
|
||||
parser.process(app);
|
||||
about.processCommandLine(&parser);
|
||||
Controller::setTestMode(parser.isSet("test"_ls));
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
if (parser.isSet(dbusActivatedOption)) {
|
||||
// We want to be replaceable by the main client
|
||||
KDBusService service(KDBusService::Replace);
|
||||
|
||||
Controller::listenForNotifications();
|
||||
return QCoreApplication::exec();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
KDBusService service(KDBusService::Unique);
|
||||
#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());
|
||||
@@ -180,7 +214,6 @@ int main(int argc, char *argv[])
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
#ifdef HAVE_KDBUSADDONS
|
||||
KDBusService service(KDBusService::Unique);
|
||||
service.connect(&service,
|
||||
&KDBusService::activateRequested,
|
||||
&RoomManager::instance(),
|
||||
@@ -199,7 +232,7 @@ int main(int argc, char *argv[])
|
||||
auto args = arguments;
|
||||
args.removeFirst();
|
||||
for (const auto &arg : args) {
|
||||
RoomManager::instance().openResource(arg);
|
||||
RoomManager::instance().resolveResource(arg);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
@@ -208,22 +241,12 @@ int main(int argc, char *argv[])
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
|
||||
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
|
||||
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
|
||||
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
|
||||
|
||||
about.setupCommandLine(&parser);
|
||||
parser.process(app);
|
||||
about.processCommandLine(&parser);
|
||||
|
||||
if (parser.isSet("ignore-ssl-errors"_ls)) {
|
||||
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
}
|
||||
|
||||
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
|
||||
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
|
||||
|
||||
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "matriximageprovider.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconnection.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
|
||||
: mediaId(std::move(id))
|
||||
, requestedSize(size)
|
||||
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
|
||||
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
|
||||
mediaId,
|
||||
QString::number(requestedSize.width()),
|
||||
QString::number(requestedSize.height())))
|
||||
, m_connection(connection)
|
||||
, errorStr("Image request hasn't started"_ls)
|
||||
{
|
||||
if (requestedSize.isEmpty()) {
|
||||
requestedSize.setHeight(100);
|
||||
requestedSize.setWidth(100);
|
||||
}
|
||||
if (mediaId.count(QLatin1Char('/')) != 1) {
|
||||
if (mediaId.startsWith(QLatin1Char('/'))) {
|
||||
mediaId = mediaId.mid(1);
|
||||
} else {
|
||||
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
|
||||
Q_EMIT finished();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mediaId = mediaId.split(QLatin1Char('?'))[0];
|
||||
|
||||
QImage cachedImage;
|
||||
if (cachedImage.load(localFile)) {
|
||||
image = cachedImage;
|
||||
errorStr.clear();
|
||||
Q_EMIT finished();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_connection) {
|
||||
qWarning() << "Current connection is null";
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute a request on the main thread asynchronously
|
||||
moveToThread(m_connection->thread());
|
||||
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void ThumbnailResponse::startRequest()
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
// Runs in the main thread, not QML thread
|
||||
Q_ASSERT(QThread::currentThread() == m_connection->thread());
|
||||
job = m_connection->getThumbnail(mediaId, requestedSize);
|
||||
// Connect to any possible outcome including abandonment
|
||||
// to make sure the QML thread is not left stuck forever.
|
||||
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
|
||||
}
|
||||
|
||||
void ThumbnailResponse::prepareResult()
|
||||
{
|
||||
Q_ASSERT(QThread::currentThread() == job->thread());
|
||||
Q_ASSERT(job->error() != BaseJob::Pending);
|
||||
{
|
||||
QWriteLocker _(&lock);
|
||||
if (job->error() == BaseJob::Success) {
|
||||
image = job->thumbnail();
|
||||
|
||||
QString localPath = QFileInfo(localFile).absolutePath();
|
||||
QDir dir;
|
||||
if (!dir.exists(localPath)) {
|
||||
dir.mkpath(localPath);
|
||||
}
|
||||
|
||||
image.save(localFile);
|
||||
|
||||
errorStr.clear();
|
||||
} else if (job->error() == BaseJob::Abandoned) {
|
||||
errorStr = i18n("Image request has been cancelled");
|
||||
// qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
|
||||
} else {
|
||||
errorStr = job->errorString();
|
||||
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr;
|
||||
}
|
||||
job = nullptr;
|
||||
}
|
||||
Q_EMIT finished();
|
||||
}
|
||||
|
||||
QQuickTextureFactory *ThumbnailResponse::textureFactory() const
|
||||
{
|
||||
QReadLocker _(&lock);
|
||||
return QQuickTextureFactory::textureFactoryForImage(image);
|
||||
}
|
||||
|
||||
QString ThumbnailResponse::errorString() const
|
||||
{
|
||||
QReadLocker _(&lock);
|
||||
return errorStr;
|
||||
}
|
||||
|
||||
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
return new ThumbnailResponse(id, requestedSize, m_connection);
|
||||
}
|
||||
|
||||
#include "moc_matriximageprovider.cpp"
|
||||
@@ -1,80 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickAsyncImageProvider>
|
||||
|
||||
#include <Quotient/jobs/mediathumbnailjob.h>
|
||||
|
||||
#include <QReadWriteLock>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
/**
|
||||
* @class ThumbnailResponse
|
||||
*
|
||||
* A QQuickImageResponse for an mxc image.
|
||||
*
|
||||
* @sa QQuickImageResponse
|
||||
*/
|
||||
class ThumbnailResponse : public QQuickImageResponse
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
|
||||
~ThumbnailResponse() override = default;
|
||||
|
||||
private Q_SLOTS:
|
||||
void startRequest();
|
||||
void prepareResult();
|
||||
|
||||
private:
|
||||
QString mediaId;
|
||||
QSize requestedSize;
|
||||
const QString localFile;
|
||||
Quotient::MediaThumbnailJob *job = nullptr;
|
||||
NeoChatConnection *m_connection;
|
||||
|
||||
QImage image;
|
||||
QString errorStr;
|
||||
mutable QReadWriteLock lock; // Guards ONLY these two members above
|
||||
|
||||
QQuickTextureFactory *textureFactory() const override;
|
||||
QString errorString() const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class MatrixImageProvider
|
||||
*
|
||||
* A QQuickAsyncImageProvider for mxc images.
|
||||
*
|
||||
* @sa QQuickAsyncImageProvider
|
||||
*/
|
||||
class MatrixImageProvider : public QQuickAsyncImageProvider
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
|
||||
public:
|
||||
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
static MatrixImageProvider instance;
|
||||
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
|
||||
return &instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a job to provide the image with the given ID.
|
||||
*
|
||||
* @sa QQuickAsyncImageProvider::requestImageResponse
|
||||
*/
|
||||
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
|
||||
|
||||
private:
|
||||
NeoChatConnection *m_connection = nullptr;
|
||||
MatrixImageProvider() = default;
|
||||
};
|
||||
@@ -235,7 +235,7 @@ QList<ActionsModel::Action> actions{
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
Controller::instance().joinRoom(text);
|
||||
RoomManager::instance().resolveResource(text, "join"_ls);
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
@@ -290,7 +290,7 @@ QList<ActionsModel::Action> actions{
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
||||
Controller::instance().joinRoom(text);
|
||||
RoomManager::instance().resolveResource(text, "join"_ls);
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
||||
fetchEmojis();
|
||||
});
|
||||
});
|
||||
CustomEmojiModel::fetchEmojis();
|
||||
}
|
||||
|
||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
@@ -102,6 +102,9 @@ void ImagePacksModel::reloadImages()
|
||||
}
|
||||
auto packs = rooms[roomId].toObject();
|
||||
const auto &stickerRoom = m_room->connection()->room(roomId);
|
||||
if (!stickerRoom) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &packKey : packs.keys()) {
|
||||
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
||||
const auto packContent = pack->content();
|
||||
|
||||
@@ -45,10 +45,6 @@ LocationsModel::LocationsModel(QObject *parent)
|
||||
});
|
||||
|
||||
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
|
||||
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
});
|
||||
}
|
||||
|
||||
void LocationsModel::addLocation(const RoomMessageEvent *event)
|
||||
@@ -135,4 +131,12 @@ QRectF LocationsModel::boundingBox() const
|
||||
return bbox;
|
||||
}
|
||||
|
||||
bool LocationsModel::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ApplicationPaletteChange) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
return QObject::event(event);
|
||||
}
|
||||
|
||||
#include "moc_locationsmodel.cpp"
|
||||
|
||||
@@ -46,6 +46,9 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void boundingBoxChanged();
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
|
||||
|
||||
@@ -71,9 +71,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
MessageEventModel::MessageEventModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
|
||||
});
|
||||
}
|
||||
|
||||
NeoChatRoom *MessageEventModel::room() const
|
||||
@@ -95,6 +92,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
|
||||
m_currentRoom = room;
|
||||
Q_EMIT roomChanged();
|
||||
if (room) {
|
||||
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
|
||||
room->setDisplayed();
|
||||
@@ -392,13 +390,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto firstIt = m_currentRoom->messageEvents().crbegin();
|
||||
if (firstIt != m_currentRoom->messageEvents().crend()) {
|
||||
const auto &firstEvt = **firstIt;
|
||||
return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0);
|
||||
} else {
|
||||
return m_currentRoom->timelineSize();
|
||||
}
|
||||
return int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0);
|
||||
}
|
||||
|
||||
bool MessageEventModel::canFetchMore(const QModelIndex &parent) const
|
||||
@@ -425,7 +417,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
const auto row = idx.row();
|
||||
|
||||
if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) {
|
||||
if (!m_currentRoom || row < 0 || row >= rowCount()) {
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -440,6 +432,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
const KFormat format;
|
||||
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
||||
}
|
||||
case SpecialMarksRole:
|
||||
// Check if all the earlier events in the timeline are hidden. If so hide this.
|
||||
for (auto r = row - 1; r >= 0; --r) {
|
||||
const auto specialMark = index(r).data(SpecialMarksRole);
|
||||
if (!(specialMark == EventStatus::Hidden || specialMark == EventStatus::Replaced)) {
|
||||
return EventStatus::Normal;
|
||||
}
|
||||
}
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -459,7 +460,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
||||
}
|
||||
|
||||
return eventHandler.getRichBody();
|
||||
}
|
||||
|
||||
@@ -726,4 +726,12 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageEventModel::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ApplicationPaletteChange) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
|
||||
}
|
||||
return QObject::event(event);
|
||||
}
|
||||
|
||||
#include "moc_messageeventmodel.cpp"
|
||||
|
||||
@@ -118,6 +118,9 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
int refreshEvent(const QString &eventId);
|
||||
void refreshRow(int row);
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "timelinemodel.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceMessageModel);
|
||||
setSourceModel(sourceMessageModel);
|
||||
Q_ASSERT(sourceModel);
|
||||
setSourceModel(sourceModel);
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||
invalidateFilter();
|
||||
@@ -117,35 +118,37 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
|
||||
chunks += QString();
|
||||
int count = 1;
|
||||
auto part = parts.takeFirst();
|
||||
chunks.last() += part;
|
||||
while (!parts.isEmpty() && parts.first() == part) {
|
||||
parts.removeFirst();
|
||||
count++;
|
||||
}
|
||||
if (count > 1 && uniqueAuthors.length() == 1) {
|
||||
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
|
||||
}
|
||||
chunks.last() += i18ncp("%1: What's being done; %2: How often it is done.", " %1", " %1 %2 times", part, count);
|
||||
}
|
||||
chunks.removeDuplicates();
|
||||
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
|
||||
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
|
||||
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
|
||||
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
|
||||
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
|
||||
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
|
||||
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
|
||||
text += userText;
|
||||
text += chunks.takeFirst();
|
||||
|
||||
QString chunksText;
|
||||
chunksText += chunks.takeFirst();
|
||||
if (chunks.size() > 0) {
|
||||
while (chunks.size() > 1) {
|
||||
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
||||
text += chunks.takeFirst();
|
||||
chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
||||
chunksText += chunks.takeFirst();
|
||||
}
|
||||
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||
text += chunks.takeFirst();
|
||||
chunksText +=
|
||||
uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||
chunksText += chunks.takeFirst();
|
||||
}
|
||||
return text;
|
||||
return i18nc(
|
||||
"userText (%1) is either a Matrix username if a single user sent all the states or n users if they were sent by multiple users."
|
||||
"chunksText (%2) is a list of comma separated actions for each of the state events in the group.",
|
||||
"<style>a {text-decoration: none;}</style>%1 %2",
|
||||
userText,
|
||||
chunksText);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include "timelinemodel.h"
|
||||
|
||||
/**
|
||||
* @class MessageFilterModel
|
||||
@@ -36,7 +37,7 @@ public:
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, TimelineModel *sourceModel = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function to remove hidden messages.
|
||||
|
||||
166
src/models/notificationsmodel.cpp
Normal file
166
src/models/notificationsmodel.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "notificationsmodel.h"
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/events/event.h>
|
||||
#include <Quotient/uri.h>
|
||||
|
||||
#include "eventhandler.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NotificationsModel::NotificationsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int NotificationsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_notifications.count();
|
||||
}
|
||||
|
||||
QVariant NotificationsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
auto row = index.row();
|
||||
if (row < 0 || row >= m_notifications.count()) {
|
||||
return {};
|
||||
}
|
||||
if (role == TextRole) {
|
||||
return m_notifications[row].text;
|
||||
}
|
||||
if (role == RoomIdRole) {
|
||||
return m_notifications[row].roomId;
|
||||
}
|
||||
if (role == AuthorName) {
|
||||
return m_notifications[row].authorName;
|
||||
}
|
||||
if (role == AuthorAvatar) {
|
||||
return m_notifications[row].authorAvatar;
|
||||
}
|
||||
if (role == RoomRole) {
|
||||
return QVariant::fromValue(m_connection->room(m_notifications[row].roomId));
|
||||
}
|
||||
if (role == EventIdRole) {
|
||||
return m_notifications[row].eventId;
|
||||
}
|
||||
if (role == RoomDisplayNameRole) {
|
||||
return m_notifications[row].roomDisplayName;
|
||||
}
|
||||
if (role == UriRole) {
|
||||
return Uri(m_notifications[row].roomId.toLatin1(), m_notifications[row].eventId.toLatin1()).toUrl();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> NotificationsModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TextRole, "text"},
|
||||
{RoomIdRole, "roomId"},
|
||||
{AuthorName, "authorName"},
|
||||
{AuthorAvatar, "authorAvatar"},
|
||||
{RoomRole, "room"},
|
||||
{EventIdRole, "eventId"},
|
||||
{RoomDisplayNameRole, "roomDisplayName"},
|
||||
{UriRole, "uri"},
|
||||
};
|
||||
}
|
||||
|
||||
NeoChatConnection *NotificationsModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void NotificationsModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection) {
|
||||
// disconnect things...
|
||||
}
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
connect(connection, &Connection::syncDone, this, [=]() {
|
||||
loadData();
|
||||
});
|
||||
loadData();
|
||||
}
|
||||
|
||||
void NotificationsModel::loadData()
|
||||
{
|
||||
Q_ASSERT(m_connection);
|
||||
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
m_job = m_connection->callApi<GetNotificationsJob>(m_nextToken);
|
||||
Q_EMIT loadingChanged();
|
||||
connect(m_job, &BaseJob::finished, this, [this]() {
|
||||
m_nextToken = m_job->nextToken();
|
||||
Q_EMIT nextTokenChanged();
|
||||
for (const auto ¬ification : m_job->notifications()) {
|
||||
if (std::any_of(notification.actions.constBegin(), notification.actions.constEnd(), [](const QVariant &it) {
|
||||
if (it.canConvert<QVariantMap>()) {
|
||||
auto map = it.toMap();
|
||||
if (map["set_tweak"_ls] == "highlight"_ls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
const auto &authorId = notification.event->fullJson()["sender"_ls].toString();
|
||||
const auto &room = m_connection->room(notification.roomId);
|
||||
if (!room) {
|
||||
continue;
|
||||
}
|
||||
auto u = room->memberAvatarUrl(authorId);
|
||||
auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u);
|
||||
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
|
||||
|
||||
const auto &roomEvent = eventCast<const RoomEvent>(notification.event.get());
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(dynamic_cast<NeoChatRoom *>(room));
|
||||
eventHandler.setEvent(roomEvent);
|
||||
beginInsertRows({}, m_notifications.length(), m_notifications.length());
|
||||
m_notifications += Notification{
|
||||
.roomId = notification.roomId,
|
||||
.text = room->htmlSafeMemberName(authorId) + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": "))
|
||||
+ eventHandler.getPlainBody(true),
|
||||
.authorName = room->htmlSafeMemberName(authorId),
|
||||
.authorAvatar = authorAvatar,
|
||||
.eventId = roomEvent->id(),
|
||||
.roomDisplayName = room->displayName(),
|
||||
};
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
m_job = nullptr;
|
||||
Q_EMIT loadingChanged();
|
||||
});
|
||||
}
|
||||
|
||||
bool NotificationsModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return !m_nextToken.isEmpty();
|
||||
}
|
||||
|
||||
void NotificationsModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
loadData();
|
||||
}
|
||||
|
||||
bool NotificationsModel::loading() const
|
||||
{
|
||||
return m_job;
|
||||
}
|
||||
|
||||
QString NotificationsModel::nextToken() const
|
||||
{
|
||||
return m_nextToken;
|
||||
}
|
||||
68
src/models/notificationsmodel.h
Normal file
68
src/models/notificationsmodel.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatconnection.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QPointer>
|
||||
#include <QQmlEngine>
|
||||
#include <QVariant>
|
||||
#include <Quotient/csapi/notifications.h>
|
||||
|
||||
class NotificationsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(QString nextToken READ nextToken NOTIFY nextTokenChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
TextRole = Qt::DisplayRole,
|
||||
RoomIdRole,
|
||||
AuthorName,
|
||||
AuthorAvatar,
|
||||
RoomRole,
|
||||
EventIdRole,
|
||||
RoomDisplayNameRole,
|
||||
UriRole,
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
struct Notification {
|
||||
QString roomId;
|
||||
QString text;
|
||||
QString authorName;
|
||||
QUrl authorAvatar;
|
||||
QString eventId;
|
||||
QString roomDisplayName;
|
||||
};
|
||||
|
||||
NotificationsModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
NeoChatConnection *connection() const;
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
bool loading() const;
|
||||
QString nextToken() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void loadingChanged();
|
||||
void nextTokenChanged();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
void loadData();
|
||||
QList<Notification> m_notifications;
|
||||
QString m_nextToken;
|
||||
QPointer<Quotient::GetNotificationsJob> m_job;
|
||||
};
|
||||
@@ -21,9 +21,6 @@ using namespace Quotient;
|
||||
SearchModel::SearchModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
|
||||
});
|
||||
}
|
||||
|
||||
QString SearchModel::searchText() const
|
||||
@@ -219,6 +216,14 @@ bool SearchModel::searching() const
|
||||
return m_searching;
|
||||
}
|
||||
|
||||
bool SearchModel::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ApplicationPaletteChange) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
|
||||
}
|
||||
return QObject::event(event);
|
||||
}
|
||||
|
||||
void SearchModel::setSearching(bool searching)
|
||||
{
|
||||
m_searching = searching;
|
||||
|
||||
@@ -132,6 +132,9 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void searchingChanged();
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void setSearching(bool searching);
|
||||
|
||||
|
||||
@@ -16,41 +16,6 @@
|
||||
ServerListModel::ServerListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
const auto stateConfig = KSharedConfig::openStateConfig();
|
||||
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
|
||||
|
||||
QString domain = m_connection->domain();
|
||||
|
||||
// Add the user's homeserver
|
||||
m_servers.append(Server{
|
||||
domain,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
});
|
||||
// Add matrix.org
|
||||
m_servers.append(Server{
|
||||
QStringLiteral("matrix.org"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
});
|
||||
// Add each of the saved custom servers
|
||||
for (const auto &i : serverGroup.keyList()) {
|
||||
m_servers.append(Server{
|
||||
serverGroup.readEntry(i, QString()),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
});
|
||||
}
|
||||
// Add add server delegate entry
|
||||
m_servers.append(Server{
|
||||
QString(),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
});
|
||||
}
|
||||
|
||||
QVariant ServerListModel::data(const QModelIndex &index, int role) const
|
||||
@@ -165,6 +130,53 @@ void ServerListModel::setConnection(NeoChatConnection *connection)
|
||||
}
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
void ServerListModel::initialize()
|
||||
{
|
||||
if (m_connection == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
const auto stateConfig = KSharedConfig::openStateConfig();
|
||||
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
|
||||
|
||||
QString domain = m_connection->domain();
|
||||
|
||||
// Add the user's homeserver
|
||||
m_servers.append(Server{
|
||||
domain,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
});
|
||||
// Add matrix.org
|
||||
m_servers.append(Server{
|
||||
QStringLiteral("matrix.org"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
});
|
||||
// Add each of the saved custom servers
|
||||
for (const auto &i : serverGroup.keyList()) {
|
||||
m_servers.append(Server{
|
||||
serverGroup.readEntry(i, QString()),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
});
|
||||
}
|
||||
// Add add server delegate entry
|
||||
m_servers.append(Server{
|
||||
QString(),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
});
|
||||
beginResetModel();
|
||||
}
|
||||
|
||||
#include "moc_serverlistmodel.cpp"
|
||||
|
||||
@@ -111,4 +111,6 @@ private:
|
||||
QList<Server> m_servers;
|
||||
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
|
||||
NeoChatConnection *m_connection = nullptr;
|
||||
|
||||
void initialize();
|
||||
};
|
||||
|
||||
@@ -86,6 +86,7 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
||||
SpaceTreeItem *parentItem = getItem(parent);
|
||||
|
||||
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
|
||||
parentItem->setChildStates(std::move(children[0].childrenState));
|
||||
children.erase(children.begin());
|
||||
}
|
||||
|
||||
@@ -112,6 +113,13 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
||||
m_replacedRooms += successorId;
|
||||
}
|
||||
}
|
||||
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));
|
||||
});
|
||||
}
|
||||
parentItem->insertChild(insertRow,
|
||||
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
|
||||
parentItem,
|
||||
@@ -123,14 +131,8 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
||||
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));
|
||||
});
|
||||
}
|
||||
children[i].roomType == QLatin1String("m.space"),
|
||||
std::move(children[i].childrenState)));
|
||||
}
|
||||
}
|
||||
endInsertRows();
|
||||
@@ -194,6 +196,9 @@ QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
|
||||
if (role == IsSpaceRole) {
|
||||
return child->isSpace();
|
||||
}
|
||||
if (role == IsSuggestedRole) {
|
||||
return child->isSuggested();
|
||||
}
|
||||
if (role == CanAddChildrenRole) {
|
||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||
return room->canSendState(QLatin1String("m.space.child"));
|
||||
@@ -313,6 +318,7 @@ QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
|
||||
roles[IsJoinedRole] = "isJoined";
|
||||
roles[AliasRole] = "alias";
|
||||
roles[IsSpaceRole] = "isSpace";
|
||||
roles[IsSuggestedRole] = "isSuggested";
|
||||
roles[CanAddChildrenRole] = "canAddChildren";
|
||||
roles[ParentDisplayNameRole] = "parentDisplayName";
|
||||
roles[CanSetParentRole] = "canSetParent";
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
IsSpaceRole,
|
||||
IsSuggestedRole,
|
||||
CanAddChildrenRole,
|
||||
ParentDisplayNameRole,
|
||||
CanSetParentRole,
|
||||
|
||||
@@ -15,7 +15,8 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
|
||||
const QUrl &avatarUrl,
|
||||
bool allowGuests,
|
||||
bool worldReadable,
|
||||
bool isSpace)
|
||||
bool isSpace,
|
||||
Quotient::StateEvents childStates)
|
||||
: m_connection(connection)
|
||||
, m_parentItem(parent)
|
||||
, m_id(id)
|
||||
@@ -27,6 +28,7 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
|
||||
, m_allowGuests(allowGuests)
|
||||
, m_worldReadable(worldReadable)
|
||||
, m_isSpace(isSpace)
|
||||
, m_childStates(std::move(childStates))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ int SpaceTreeItem::row() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
SpaceTreeItem *SpaceTreeItem::parentItem()
|
||||
SpaceTreeItem *SpaceTreeItem::parentItem() const
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
@@ -138,3 +140,34 @@ bool SpaceTreeItem::isSpace() const
|
||||
{
|
||||
return m_isSpace;
|
||||
}
|
||||
|
||||
QJsonObject SpaceTreeItem::childStateContent(const SpaceTreeItem *child) const
|
||||
{
|
||||
if (child == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (child->parentItem() != this) {
|
||||
return {};
|
||||
}
|
||||
for (const auto &childState : m_childStates) {
|
||||
if (childState->stateKey() == child->id()) {
|
||||
return childState->contentJson();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void SpaceTreeItem::setChildStates(Quotient::StateEvents childStates)
|
||||
{
|
||||
m_childStates.clear();
|
||||
m_childStates = std::move(childStates);
|
||||
}
|
||||
|
||||
bool SpaceTreeItem::isSuggested() const
|
||||
{
|
||||
if (m_parentItem == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto childStateContent = m_parentItem->childStateContent(this);
|
||||
return childStateContent.value(QLatin1String("suggested")).toBool();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <Quotient/csapi/space_hierarchy.h>
|
||||
#include <Quotient/events/stateevent.h>
|
||||
|
||||
class NeoChatConnection;
|
||||
|
||||
@@ -30,7 +31,8 @@ public:
|
||||
const QUrl &avatarUrl = {},
|
||||
bool allowGuests = {},
|
||||
bool worldReadable = {},
|
||||
bool isSpace = {});
|
||||
bool isSpace = {},
|
||||
Quotient::StateEvents childStates = {});
|
||||
~SpaceTreeItem();
|
||||
|
||||
/**
|
||||
@@ -60,7 +62,7 @@ public:
|
||||
/**
|
||||
* @brief Return this item's parent.
|
||||
*/
|
||||
SpaceTreeItem *parentItem();
|
||||
SpaceTreeItem *parentItem() const;
|
||||
|
||||
/**
|
||||
* @brief Return the row number for this child relative to the parent.
|
||||
@@ -123,6 +125,23 @@ public:
|
||||
*/
|
||||
bool isSpace() const;
|
||||
|
||||
/**
|
||||
* @brief Return the m.space.child state event content for the given child.
|
||||
*/
|
||||
QJsonObject childStateContent(const SpaceTreeItem *child) const;
|
||||
|
||||
/**
|
||||
* @brief Set the list of m.space.child events.
|
||||
*
|
||||
* Overwrites existing states. Calling with no input will clear the existing states.
|
||||
*/
|
||||
void setChildStates(Quotient::StateEvents childStates = {});
|
||||
|
||||
/**
|
||||
* @brief Whether the room is suggested in the parent space.
|
||||
*/
|
||||
bool isSuggested() const;
|
||||
|
||||
private:
|
||||
NeoChatConnection *m_connection;
|
||||
QList<SpaceTreeItem *> m_children;
|
||||
@@ -137,4 +156,5 @@ private:
|
||||
bool m_allowGuests;
|
||||
bool m_worldReadable;
|
||||
bool m_isSpace;
|
||||
Quotient::StateEvents m_childStates;
|
||||
};
|
||||
|
||||
95
src/models/timelinemodel.cpp
Normal file
95
src/models/timelinemodel.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// SPDX-FileCopyrightText: 2022 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 "timelinemodel.h"
|
||||
|
||||
#include "delegatetype.h"
|
||||
|
||||
TimelineModel::TimelineModel(QObject *parent)
|
||||
: QConcatenateTablesProxyModel(parent)
|
||||
{
|
||||
m_messageEventModel = new MessageEventModel(this);
|
||||
addSourceModel(m_messageEventModel);
|
||||
m_timelineEndModel = new TimelineEndModel(this);
|
||||
addSourceModel(m_timelineEndModel);
|
||||
}
|
||||
|
||||
NeoChatRoom *TimelineModel::room() const
|
||||
{
|
||||
return m_messageEventModel->room();
|
||||
}
|
||||
|
||||
void TimelineModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
// Both models do their own null checking so just pass along.
|
||||
m_messageEventModel->setRoom(room);
|
||||
m_timelineEndModel->setRoom(room);
|
||||
}
|
||||
|
||||
MessageEventModel *TimelineModel::messageEventModel() const
|
||||
{
|
||||
return m_messageEventModel;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TimelineModel::roleNames() const
|
||||
{
|
||||
return m_messageEventModel->roleNames();
|
||||
}
|
||||
|
||||
TimelineEndModel::TimelineEndModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void TimelineEndModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
|
||||
if (m_room != nullptr) {
|
||||
m_room->disconnect(this);
|
||||
}
|
||||
|
||||
m_room = room;
|
||||
|
||||
if (m_room != nullptr) {
|
||||
connect(m_room, &Quotient::Room::eventsHistoryJobChanged, this, [this]() {
|
||||
if (m_room->allHistoryLoaded()) {
|
||||
// HACK: We have to do it this way because DelegateChooser doesn't update dynamically.
|
||||
beginRemoveRows({}, 0, 0);
|
||||
endRemoveRows();
|
||||
beginInsertRows({}, 0, 0);
|
||||
endInsertRows();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
|
||||
{
|
||||
Q_UNUSED(idx)
|
||||
if (m_room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == DelegateTypeRole) {
|
||||
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int TimelineEndModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 1;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TimelineEndModel::roleNames() const
|
||||
{
|
||||
return {{DelegateTypeRole, "delegateType"}};
|
||||
}
|
||||
112
src/models/timelinemodel.h
Normal file
112
src/models/timelinemodel.h
Normal file
@@ -0,0 +1,112 @@
|
||||
// SPDX-FileCopyrightText: 2022 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 <QAbstractListModel>
|
||||
#include <QConcatenateTablesProxyModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
/**
|
||||
* @class TimelineEndModel
|
||||
*
|
||||
* A model to provide a single delegate to mark the end of the timeline.
|
||||
*
|
||||
* The delegate will either be a loading delegate if more events are being loaded
|
||||
* or a timeline end delegate if all history is loaded.
|
||||
*/
|
||||
class TimelineEndModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit TimelineEndModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Set the room for the timeline.
|
||||
*/
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief 1, the answer is always 1.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a map with DelegateTypeRole it's the only one.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class TimelineModel
|
||||
*
|
||||
* A model to visualise a room timeline.
|
||||
*
|
||||
* This model combines a MessageEventModel with a TimelineEndModel.
|
||||
*
|
||||
* @sa MessageEventModel, TimelineEndModel
|
||||
*/
|
||||
class TimelineModel : public QConcatenateTablesProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The current room that the model is getting its messages from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The MessageEventModel for the timeline.
|
||||
*/
|
||||
Q_PROPERTY(MessageEventModel *messageEventModel READ messageEventModel CONSTANT)
|
||||
|
||||
public:
|
||||
TimelineModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
MessageEventModel *messageEventModel() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractProxyModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
|
||||
private:
|
||||
MessageEventModel *m_messageEventModel = nullptr;
|
||||
TimelineEndModel *m_timelineEndModel = nullptr;
|
||||
};
|
||||
@@ -141,12 +141,10 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
|
||||
return QStringLiteral("Unknown User");
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
auto avatarUrl = user.avatarUrl;
|
||||
|
||||
if (avatarUrl.isEmpty()) {
|
||||
return QString();
|
||||
if (user.avatarUrl.isEmpty() || user.avatarUrl.scheme() != QStringLiteral("mxc")) {
|
||||
return QUrl();
|
||||
}
|
||||
return avatarUrl.url().remove(0, 6);
|
||||
return m_connection->makeMediaUrl(user.avatarUrl);
|
||||
}
|
||||
if (role == UserIDRole) {
|
||||
return user.userId;
|
||||
|
||||
@@ -16,9 +16,6 @@ UserListModel::UserListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_currentRoom(nullptr)
|
||||
{
|
||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this]() {
|
||||
refreshAllUsers();
|
||||
});
|
||||
}
|
||||
|
||||
void UserListModel::setRoom(NeoChatRoom *room)
|
||||
@@ -121,6 +118,14 @@ int UserListModel::rowCount(const QModelIndex &parent) const
|
||||
return m_users.count();
|
||||
}
|
||||
|
||||
bool UserListModel::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ApplicationPaletteChange) {
|
||||
refreshAllUsers();
|
||||
}
|
||||
return QObject::event(event);
|
||||
}
|
||||
|
||||
void UserListModel::userAdded(Quotient::User *user)
|
||||
{
|
||||
auto pos = findUserPos(user);
|
||||
|
||||
@@ -86,6 +86,9 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void usersRefreshed();
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void userAdded(Quotient::User *user);
|
||||
void userRemoved(Quotient::User *user);
|
||||
|
||||
@@ -82,22 +82,22 @@
|
||||
</entry>
|
||||
<entry name="ShowRename" type="bool">
|
||||
<label>Show rename events in the timeline</label>
|
||||
<default>true</default>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowAvatarUpdate" type="bool">
|
||||
<label>Show avatar update events in the timeline</label>
|
||||
<default>true</default>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowDeletedMessages" type="bool">
|
||||
<label>Show deleted messages in the timeline</label>
|
||||
<default>true</default>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowLinkPreview" type="bool">
|
||||
<label>Show preview of the links in the chat messages</label>
|
||||
</entry>
|
||||
<entry name="SystemTray" type="bool">
|
||||
<label>Close NeoChat to system tray</label>
|
||||
<default>true</default>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="MinimizeToSystemTrayOnStartup" type="bool">
|
||||
<label>Minimize to system tray on startup</label>
|
||||
|
||||
@@ -21,7 +21,14 @@
|
||||
#include <Quotient/settings.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
#include <QCoroNetwork>
|
||||
#include <Quotient/csapi/pusher.h>
|
||||
#include <Quotient/networkaccessmanager.h>
|
||||
#endif
|
||||
|
||||
using namespace Quotient;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
NeoChatConnection::NeoChatConnection(QObject *parent)
|
||||
: Connection(parent)
|
||||
@@ -31,6 +38,12 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
|
||||
Q_EMIT labelChanged();
|
||||
}
|
||||
});
|
||||
connect(this, &NeoChatConnection::syncDone, this, [this] {
|
||||
setIsOnline(true);
|
||||
});
|
||||
connect(this, &NeoChatConnection::networkError, this, [this]() {
|
||||
setIsOnline(false);
|
||||
});
|
||||
}
|
||||
|
||||
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
|
||||
@@ -181,7 +194,7 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic, co
|
||||
}
|
||||
});
|
||||
}
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
connect(job, &CreateRoomJob::failure, this, [job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
@@ -213,7 +226,7 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
|
||||
}
|
||||
});
|
||||
}
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
connect(job, &CreateRoomJob::failure, this, [job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
@@ -235,6 +248,39 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
|
||||
requestDirectChat(user);
|
||||
}
|
||||
|
||||
QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
|
||||
{
|
||||
#ifdef HAVE_KUNIFIEDPUSH
|
||||
QUrl gatewayEndpoint(endpoint);
|
||||
gatewayEndpoint.setPath(QStringLiteral("/_matrix/push/v1/notify"));
|
||||
|
||||
QNetworkRequest checkGateway(gatewayEndpoint);
|
||||
auto reply = co_await NetworkAccessManager::instance()->get(checkGateway);
|
||||
|
||||
// We want to check if this UnifiedPush server has a Matrix gateway
|
||||
// This is because Matrix does not natively support UnifiedPush
|
||||
const auto &replyJson = QJsonDocument::fromJson(reply->readAll()).object();
|
||||
|
||||
if (replyJson["unifiedpush"_L1]["gateway"_L1].toString() == QStringLiteral("matrix")) {
|
||||
callApi<PostPusherJob>(endpoint,
|
||||
QStringLiteral("http"),
|
||||
QStringLiteral("org.kde.neochat"),
|
||||
QStringLiteral("NeoChat"),
|
||||
deviceId(),
|
||||
QString(), // profileTag is intentionally left empty for now, it's optional
|
||||
QStringLiteral("en-US"),
|
||||
PostPusherJob::PusherData{QUrl::fromUserInput(gatewayEndpoint.toString()), QStringLiteral(" ")},
|
||||
false);
|
||||
|
||||
qInfo() << "Registered for push notifications";
|
||||
} else {
|
||||
qWarning() << "There's no gateway, not setting up push notifications.";
|
||||
}
|
||||
#else
|
||||
co_return;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString NeoChatConnection::deviceKey() const
|
||||
{
|
||||
return edKeyForUserDevice(userId(), deviceId());
|
||||
@@ -252,4 +298,18 @@ QString NeoChatConnection::encryptionKey() const
|
||||
return query.value(0).toString();
|
||||
}
|
||||
|
||||
bool NeoChatConnection::isOnline() const
|
||||
{
|
||||
return m_isOnline;
|
||||
}
|
||||
|
||||
void NeoChatConnection::setIsOnline(bool isOnline)
|
||||
{
|
||||
if (isOnline == m_isOnline) {
|
||||
return;
|
||||
}
|
||||
m_isOnline = isOnline;
|
||||
Q_EMIT isOnlineChanged();
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <QCoroTask>
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
class NeoChatConnection : public Quotient::Connection
|
||||
@@ -26,6 +27,11 @@ class NeoChatConnection : public Quotient::Connection
|
||||
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
|
||||
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Whether NeoChat is currently able to connect to the server.
|
||||
*/
|
||||
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
|
||||
|
||||
public:
|
||||
NeoChatConnection(QObject *parent = nullptr);
|
||||
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
|
||||
@@ -70,9 +76,20 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
|
||||
|
||||
// note: this is intentionally a copied QString because
|
||||
// the reference could be destroyed before the task is finished
|
||||
QCoro::Task<void> setupPushNotifications(QString endpoint);
|
||||
|
||||
QString deviceKey() const;
|
||||
QString encryptionKey() const;
|
||||
|
||||
bool isOnline() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void labelChanged();
|
||||
void isOnlineChanged();
|
||||
|
||||
private:
|
||||
bool m_isOnline = true;
|
||||
void setIsOnline(bool isOnline);
|
||||
};
|
||||
|
||||
@@ -355,7 +355,7 @@ QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines
|
||||
body = eventHandler.getPlainBody(stripNewlines);
|
||||
}
|
||||
|
||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
||||
return eventHandler.singleLineAuthorDisplayname() + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -452,17 +452,18 @@ QVariantMap NeoChatRoom::getUser(User *user) const
|
||||
};
|
||||
}
|
||||
|
||||
QString NeoChatRoom::avatarMediaId() const
|
||||
QUrl NeoChatRoom::avatarMediaId() const
|
||||
{
|
||||
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
|
||||
return avatar;
|
||||
return connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(avatar)));
|
||||
}
|
||||
|
||||
// Use the first (excluding self) user's avatar for direct chats
|
||||
const auto dcUsers = directChatUsers();
|
||||
for (const auto u : dcUsers) {
|
||||
if (u != localUser()) {
|
||||
return u->avatarMediaId(this);
|
||||
const auto &avatar = u->avatarMediaId(this);
|
||||
return avatar.isEmpty() ? QUrl() : connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(avatar)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,7 +1305,7 @@ bool NeoChatRoom::isSpace()
|
||||
return creationEvent->roomType() == RoomType::Space;
|
||||
}
|
||||
|
||||
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical)
|
||||
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested)
|
||||
{
|
||||
if (!isSpace()) {
|
||||
return;
|
||||
@@ -1312,7 +1313,7 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
|
||||
if (!canSendEvent("m.space.child"_ls)) {
|
||||
return;
|
||||
}
|
||||
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}});
|
||||
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}});
|
||||
|
||||
if (setChildParent) {
|
||||
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
|
||||
@@ -1354,6 +1355,30 @@ void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent)
|
||||
}
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isSuggested(const QString &childId)
|
||||
{
|
||||
if (!currentState().contains("m.space.child"_ls, childId)) {
|
||||
return false;
|
||||
}
|
||||
const auto childEvent = currentState().get("m.space.child"_ls, childId);
|
||||
return childEvent->contentPart<bool>("suggested"_ls);
|
||||
}
|
||||
|
||||
void NeoChatRoom::toggleChildSuggested(const QString &childId)
|
||||
{
|
||||
if (!isSpace()) {
|
||||
return;
|
||||
}
|
||||
if (!canSendEvent("m.space.child"_ls)) {
|
||||
return;
|
||||
}
|
||||
if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) {
|
||||
auto content = childEvent->contentJson();
|
||||
content.insert("suggested"_ls, !childEvent->contentPart<bool>("suggested"_ls));
|
||||
setState("m.space.child"_ls, childId, content);
|
||||
}
|
||||
}
|
||||
|
||||
PushNotificationState::State NeoChatRoom::pushNotificationState() const
|
||||
{
|
||||
return m_currentPushNotificationState;
|
||||
|
||||
@@ -110,7 +110,7 @@ class NeoChatRoom : public Quotient::Room
|
||||
/**
|
||||
* @brief The avatar image to be used for the room.
|
||||
*/
|
||||
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
|
||||
Q_PROPERTY(QUrl avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
|
||||
|
||||
/**
|
||||
* @brief Get a user object for the other person in a direct chat.
|
||||
@@ -501,7 +501,7 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QString subtitleText();
|
||||
|
||||
[[nodiscard]] QString avatarMediaId() const;
|
||||
[[nodiscard]] QUrl avatarMediaId() const;
|
||||
|
||||
Quotient::User *directChatRemoteUser() const;
|
||||
|
||||
@@ -562,7 +562,7 @@ public:
|
||||
* Will fail if the user doesn't have the required privileges or this room is
|
||||
* not a space.
|
||||
*/
|
||||
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false);
|
||||
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false);
|
||||
|
||||
/**
|
||||
* @brief Remove the given room as a child.
|
||||
@@ -572,6 +572,19 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
|
||||
|
||||
/**
|
||||
* @brief Whether the given child is a suggested room in the space.
|
||||
*/
|
||||
Q_INVOKABLE bool isSuggested(const QString &childId);
|
||||
|
||||
/**
|
||||
* @brief Toggle whether the given child is a suggested room in the space.
|
||||
*
|
||||
* Will fail if the user doesn't have the required privileges, this room is
|
||||
* not a space or the given room is not a child of this space.
|
||||
*/
|
||||
Q_INVOKABLE void toggleChildSuggested(const QString &childId);
|
||||
|
||||
bool isInvite() const;
|
||||
|
||||
Q_INVOKABLE void clearInvitationNotification();
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
#include <Quotient/csapi/pushrules.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#ifdef HAVE_KIO
|
||||
#include <KIO/ApplicationLauncherJob>
|
||||
#endif
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatconnection.h"
|
||||
@@ -292,6 +296,56 @@ void NotificationsManager::clearInvitationNotification(const QString &roomId)
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationsManager::postPushNotification(const QByteArray &message)
|
||||
{
|
||||
const auto json = QJsonDocument::fromJson(message).object();
|
||||
|
||||
const auto type = json["notification"_ls]["type"_ls].toString();
|
||||
|
||||
// the only two types of push notifications we support right now
|
||||
if (type == QStringLiteral("m.room.message") || type == QStringLiteral("m.room.encrypted")) {
|
||||
auto notification = new KNotification("message"_ls);
|
||||
|
||||
const auto sender = json["notification"_ls]["sender_display_name"_ls].toString();
|
||||
const auto roomName = json["notification"_ls]["room_name"_ls].toString();
|
||||
const auto roomId = json["notification"_ls]["room_id"_ls].toString();
|
||||
|
||||
if (roomName.isEmpty() || sender == roomName) {
|
||||
notification->setTitle(sender);
|
||||
} else {
|
||||
notification->setTitle(i18n("%1 (%2)", sender, roomName));
|
||||
}
|
||||
|
||||
if (type == QStringLiteral("m.room.message")) {
|
||||
const auto text = json["notification"_ls]["content"_ls]["body"_ls].toString();
|
||||
notification->setText(text.toHtmlEscaped());
|
||||
} else if (type == QStringLiteral("m.room.encrypted")) {
|
||||
notification->setText(i18n("Encrypted Message"));
|
||||
}
|
||||
|
||||
#ifdef HAVE_KIO
|
||||
auto openAction = notification->addAction(i18n("Open NeoChat"));
|
||||
connect(openAction, &KNotificationAction::activated, this, [=]() {
|
||||
QString properId = roomId;
|
||||
properId = properId.replace(QStringLiteral("#"), QString());
|
||||
properId = properId.replace(QStringLiteral("!"), QString());
|
||||
|
||||
auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.neochat")));
|
||||
job->setUrls({QUrl::fromUserInput(QStringLiteral("matrix:r/%1").arg(properId))});
|
||||
job->start();
|
||||
});
|
||||
#endif
|
||||
|
||||
connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit);
|
||||
|
||||
notification->sendEvent();
|
||||
|
||||
m_notifications.insert(roomId, notification);
|
||||
} else {
|
||||
qWarning() << "Skipping unsupported push notification" << type;
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
|
||||
{
|
||||
// Handle avatars that are lopsided in one dimension
|
||||
|
||||
@@ -83,6 +83,11 @@ public:
|
||||
*/
|
||||
void clearInvitationNotification(const QString &roomId);
|
||||
|
||||
/**
|
||||
* @brief Display a native notification for the given push notification.
|
||||
*/
|
||||
void postPushNotification(const QByteArray &message);
|
||||
|
||||
/**
|
||||
* @brief Handle the notifications for the given connection.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user