Compare commits
97 Commits
work/fix_c
...
work/carl/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29cc585b06 | ||
|
|
cbab810a2e | ||
|
|
e78dfaec34 | ||
|
|
3cfa773820 | ||
|
|
94c0e8b6cd | ||
|
|
35b1f24cb7 | ||
|
|
de3072125e | ||
|
|
f7d2ffac66 | ||
|
|
ff0990bb7c | ||
|
|
8285961c42 | ||
|
|
cd39d5b129 | ||
|
|
83b3fefbf5 | ||
|
|
3718bd716a | ||
|
|
a18257ee17 | ||
|
|
def46d90a8 | ||
|
|
8afd7b2892 | ||
|
|
deb11367cb | ||
|
|
ffd1b06a82 | ||
|
|
2597f0aec9 | ||
|
|
33ca72efd9 | ||
|
|
2fbf659eb4 | ||
|
|
763198b2c3 | ||
|
|
50551541ed | ||
|
|
323cd4962e | ||
|
|
b2f592afeb | ||
|
|
f7f98c43e2 | ||
|
|
06134f96eb | ||
|
|
bb62849a19 | ||
|
|
d05385caed | ||
|
|
209ca747b5 | ||
|
|
2c6ab498ac | ||
|
|
597633f824 | ||
|
|
65da416b8e | ||
|
|
c528929b30 | ||
|
|
8650ce1755 | ||
|
|
8936b23da9 | ||
|
|
cf72fbea2e | ||
|
|
0d5929b4bc | ||
|
|
d43cc9a044 | ||
|
|
682e3967ba | ||
|
|
ceebee3a56 | ||
|
|
52ab052aef | ||
|
|
e6a2b2d125 | ||
|
|
92dadc2cc9 | ||
|
|
6ca9380a51 | ||
|
|
51d41ab5bf | ||
|
|
f1047fa474 | ||
|
|
011b11681f | ||
|
|
8baeb236bc | ||
|
|
b9d173d0b5 | ||
|
|
3790955c56 | ||
|
|
199772a013 | ||
|
|
54cc3ac761 | ||
|
|
1675c14c92 | ||
|
|
56f5ef2611 | ||
|
|
7ba63eb680 | ||
|
|
ddd690f6d5 | ||
|
|
d9dc5f48bc | ||
|
|
a3b40a5e6d | ||
|
|
be71a4349b | ||
|
|
ede2707767 | ||
|
|
5871903529 | ||
|
|
87745c360d | ||
|
|
40d2eb5aba | ||
|
|
cfd9f36a97 | ||
|
|
48f7e06c01 | ||
|
|
43ecd188f1 | ||
|
|
063056bebd | ||
|
|
dfba655527 | ||
|
|
23178b6224 | ||
|
|
a9a6e31b8c | ||
|
|
0afd74d96a | ||
|
|
c5501f2b5d | ||
|
|
68602fc177 | ||
|
|
8ce5a57499 | ||
|
|
bf3c5708c7 | ||
|
|
8359c1fe90 | ||
|
|
774121ea8c | ||
|
|
370bbf8a4b | ||
|
|
4fd36fb221 | ||
|
|
167349b9d2 | ||
|
|
296aa9ddad | ||
|
|
94c1b67ec9 | ||
|
|
a9965acdd3 | ||
|
|
35313271a3 | ||
|
|
e40179f641 | ||
|
|
dacf3ff8a0 | ||
|
|
0274d70f71 | ||
|
|
0c19052f32 | ||
|
|
a3423e5724 | ||
|
|
e09f31f891 | ||
|
|
e05e0f5489 | ||
|
|
002961f638 | ||
|
|
e28419ef88 | ||
|
|
0e4df58fc9 | ||
|
|
21a55885b3 | ||
|
|
6a8ac8f6e5 |
@@ -5,7 +5,7 @@ include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.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.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.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.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
|
||||
@@ -64,4 +64,5 @@ Dependencies:
|
||||
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
|
||||
|
||||
@@ -82,7 +82,7 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels SyntaxHighlighting)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
@@ -144,6 +144,7 @@ set_package_properties(cmark PROPERTIES
|
||||
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
||||
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
||||
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
|
||||
|
||||
find_package(KQuickImageEditor COMPONENTS)
|
||||
set_package_properties(KQuickImageEditor PROPERTIES
|
||||
|
||||
@@ -18,6 +18,6 @@ if(NOT SeleniumWebDriverATSPI_FOUND)
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
)
|
||||
|
||||
@@ -8,8 +8,10 @@ import unittest
|
||||
from appium import webdriver
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
class LoginTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -20,7 +22,7 @@ class LoginTest(unittest.TestCase):
|
||||
self.driver = webdriver.Remote(
|
||||
command_executor='http://127.0.0.1:4723',
|
||||
desired_capabilities=desired_caps)
|
||||
subprocess.Popen(["python","login-server.py"])
|
||||
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
@@ -31,9 +33,12 @@ class LoginTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
self.mockServerProcess.terminate()
|
||||
self.driver.quit()
|
||||
|
||||
def test_login(self):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
|
||||
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")
|
||||
|
||||
@@ -20,3 +20,15 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME delegatesizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
mediasizehelpertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME mediasizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
eventhandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME eventhandlertest
|
||||
)
|
||||
|
||||
729
autotests/eventhandlertest.cpp
Normal file
729
autotests/eventhandlertest.cpp
Normal file
@@ -0,0 +1,729 @@
|
||||
// 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 <QTest>
|
||||
|
||||
#include "eventhandler.h"
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "utils.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 EventHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
EventHandler eventHandler;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void eventId();
|
||||
void delegateType_data();
|
||||
void delegateType();
|
||||
void author();
|
||||
void authorDisplayName();
|
||||
void time();
|
||||
void timeString();
|
||||
void highlighted();
|
||||
void hidden();
|
||||
void body();
|
||||
void genericBody_data();
|
||||
void genericBody();
|
||||
void mediaInfo();
|
||||
void linkPreviewer();
|
||||
void reactions();
|
||||
void hasReply();
|
||||
void replyId();
|
||||
void replyDelegateType();
|
||||
void replyAuthor();
|
||||
void replyBody();
|
||||
void replyMediaInfo();
|
||||
void location();
|
||||
void readMarkers();
|
||||
};
|
||||
|
||||
void EventHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
const auto json = QJsonDocument::fromJson(R"EVENT({
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$153456789:example.org": {
|
||||
"m.read": {
|
||||
"@alice:matrix.org": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@bob:example.com": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tim:example.com": {
|
||||
"ts": 1436451550454
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@jeff:example.com": {
|
||||
"ts": 1436451550455
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tina:example.com": {
|
||||
"ts": 1436451550456
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@sally:example.com": {
|
||||
"ts": 1436451550457
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@fred:example.com": {
|
||||
"ts": 1436451550458
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example\ntext message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://kde.org/123456",
|
||||
"displayname": "after",
|
||||
"membership": "join"
|
||||
},
|
||||
"origin_server_ts": 1690651134736,
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"replaces_state": "$1234567890:example.org",
|
||||
"prev_content": {
|
||||
"avatar_url": "mxc://kde.org/12345",
|
||||
"displayname": "before",
|
||||
"membership": "join"
|
||||
},
|
||||
"prev_sender": "@example:example.orgg",
|
||||
"age": 1234
|
||||
},
|
||||
"event_id": "$143273583553PhrSn:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
|
||||
"format": "org.matrix.custom.html",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1532735824654:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$153456789:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1690322545182,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$163456789:example.org",
|
||||
"age": 390159120
|
||||
},
|
||||
{
|
||||
"age": 4926305285,
|
||||
"content": {
|
||||
"body": "video caption",
|
||||
"filename": "video.mp4",
|
||||
"info": {
|
||||
"duration": 10,
|
||||
"h": 1080,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 62650636,
|
||||
"w": 1920,
|
||||
"thumbnail_info": {
|
||||
"h": 450,
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 382249,
|
||||
"w": 800
|
||||
},
|
||||
"thumbnail_url": "mxc://kde.org/2234567"
|
||||
},
|
||||
"msgtype": "m.video",
|
||||
"url": "mxc://kde.org/1234567"
|
||||
},
|
||||
"event_id": "$263456789:example.org",
|
||||
"origin_server_ts": 1685793783330,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 4926305285
|
||||
},
|
||||
"user_id": "@example:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$153456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965572,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456789:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> video caption\n\nreply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$263456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965573,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456799:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"age": 96845207,
|
||||
"content": {
|
||||
"body": "Lat: 51.7035, Lon: -1.14394",
|
||||
"geo_uri": "geo:51.7035,-1.14394",
|
||||
"msgtype": "m.location",
|
||||
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
|
||||
"org.matrix.msc3488.asset": {
|
||||
"type": "m.pin"
|
||||
},
|
||||
"org.matrix.msc3488.location": {
|
||||
"uri": "geo:51.7035,-1.14394"
|
||||
}
|
||||
},
|
||||
"event_id": "$1544567999:example.org",
|
||||
"origin_server_ts": 1690821582876,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
})EVENT");
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
room->update(std::move(roomData));
|
||||
|
||||
eventHandler.setRoom(room);
|
||||
}
|
||||
|
||||
void EventHandlerTest::eventId()
|
||||
{
|
||||
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||
|
||||
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<DelegateType::Type>("delegateType");
|
||||
|
||||
QTest::newRow("message") << 0 << DelegateType::Message;
|
||||
QTest::newRow("state") << 1 << DelegateType::State;
|
||||
QTest::newRow("message 2") << 2 << DelegateType::Message;
|
||||
QTest::newRow("reaction") << 3 << DelegateType::Other;
|
||||
QTest::newRow("video") << 4 << DelegateType::Video;
|
||||
QTest::newRow("location") << 7 << DelegateType::Location;
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(DelegateType::Type, delegateType);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getDelegateType(), delegateType);
|
||||
}
|
||||
|
||||
void EventHandlerTest::author()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
auto author = room->user(event->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerAuthor = eventHandler.getAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
|
||||
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
|
||||
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
|
||||
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
|
||||
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
|
||||
}
|
||||
|
||||
void EventHandlerTest::authorDisplayName()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::time()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
|
||||
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
|
||||
}
|
||||
|
||||
void EventHandlerTest::timeString()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
KFormat format;
|
||||
|
||||
QCOMPARE(eventHandler.getTimeString(false),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
|
||||
}
|
||||
|
||||
void EventHandlerTest::highlighted()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hidden()
|
||||
{
|
||||
auto event = room->messageEvents().at(3).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::body()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<QString>("output");
|
||||
|
||||
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
|
||||
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
|
||||
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
|
||||
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
|
||||
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(QString, output);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getGenericBody(), output);
|
||||
}
|
||||
|
||||
void EventHandlerTest::mediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
void EventHandlerTest::linkPreviewer()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::reactions()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hasReply()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyId()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyDelegateType()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyAuthor()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
auto replyEvent = room->messageEvents().at(0).get();
|
||||
auto replyAuthor = room->user(replyEvent->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
|
||||
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyBody()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyMediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(6).get();
|
||||
auto replyEvent = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getReplyMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
void EventHandlerTest::location()
|
||||
{
|
||||
auto event = room->messageEvents().at(7).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
|
||||
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
|
||||
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::readMarkers()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
auto readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 1);
|
||||
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
|
||||
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
|
||||
|
||||
event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 5);
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
|
||||
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
|
||||
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
|
||||
}
|
||||
|
||||
QTEST_MAIN(EventHandlerTest)
|
||||
#include "eventhandlertest.moc"
|
||||
72
autotests/mediasizehelpertest.cpp
Normal file
72
autotests/mediasizehelpertest.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 <QTest>
|
||||
#include <qglobal.h>
|
||||
#include <qtestcase.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "mediasizehelper.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
class MediaSizeHelperTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void uninitialized();
|
||||
|
||||
void limits_data();
|
||||
void limits();
|
||||
};
|
||||
|
||||
void MediaSizeHelperTest::uninitialized()
|
||||
{
|
||||
MediaSizeHelper mediasizehelper;
|
||||
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("mediaWidth");
|
||||
QTest::addColumn<qreal>("mediaHeight");
|
||||
QTest::addColumn<qreal>("contentMaxWidth");
|
||||
QTest::addColumn<qreal>("contentMaxHeight");
|
||||
QTest::addColumn<QSize>("currentSize");
|
||||
|
||||
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
|
||||
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
|
||||
|
||||
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
|
||||
|
||||
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
|
||||
|
||||
QTest::newRow("limit by content width tall media")
|
||||
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
|
||||
QTest::newRow("limit by content height wide media")
|
||||
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits()
|
||||
{
|
||||
QFETCH(qreal, mediaWidth);
|
||||
QFETCH(qreal, mediaHeight);
|
||||
QFETCH(qreal, contentMaxWidth);
|
||||
QFETCH(qreal, contentMaxHeight);
|
||||
QFETCH(QSize, currentSize);
|
||||
|
||||
MediaSizeHelper mediasizehelper;
|
||||
mediasizehelper.setMediaWidth(mediaWidth);
|
||||
mediasizehelper.setMediaHeight(mediaHeight);
|
||||
mediasizehelper.setContentMaxWidth(contentMaxWidth);
|
||||
mediasizehelper.setContentMaxHeight(contentMaxHeight);
|
||||
|
||||
QCOMPARE(mediasizehelper.currentSize(), currentSize);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
|
||||
#include "mediasizehelpertest.moc"
|
||||
@@ -363,6 +363,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
|
||||
|
||||
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
||||
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
||||
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichInPlainOut()
|
||||
|
||||
@@ -159,6 +159,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ko">투표 - MSC3381</li>
|
||||
<li xml:lang="nl">Polls - MSC3381</li>
|
||||
<li xml:lang="nn">Avstemmingar – MSC3381</li>
|
||||
<li xml:lang="pl">Ankiety - MSC3381</li>
|
||||
<li xml:lang="pt">Inquéritos - MSC3381</li>
|
||||
<li xml:lang="sl">Polls - MSC3381</li>
|
||||
<li xml:lang="sv">Polls - MSC3381</li>
|
||||
@@ -183,6 +184,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ko">스티커 팩 - MSC2545</li>
|
||||
<li xml:lang="nl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="nn">Klistremerke-pakkar – MSC2545</li>
|
||||
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
|
||||
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="sv">Sticker Packs - MSC2545</li>
|
||||
@@ -207,6 +209,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
|
||||
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
|
||||
<li xml:lang="nn">Posisjonshendingar – MSC3488</li>
|
||||
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
|
||||
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
|
||||
<li xml:lang="sl">Location Events - MSC3488</li>
|
||||
<li xml:lang="sv">Location Events - MSC3488</li>
|
||||
@@ -294,6 +297,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
|
||||
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
|
||||
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
|
||||
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
|
||||
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
|
||||
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
|
||||
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
|
||||
@@ -319,6 +323,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<caption xml:lang="ko">로그인 화면</caption>
|
||||
<caption xml:lang="nl">Aanmeldscherm</caption>
|
||||
<caption xml:lang="nn">Innloggingsbilete</caption>
|
||||
<caption xml:lang="pl">Ekran logowania</caption>
|
||||
<caption xml:lang="pt">Ecrã de autenticação</caption>
|
||||
<caption xml:lang="sl">Prijavni zaslon</caption>
|
||||
<caption xml:lang="sv">Inloggningsfönster</caption>
|
||||
|
||||
1900
po/ar/neochat.po
1900
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1950
po/az/neochat.po
1950
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1968
po/ca/neochat.po
1968
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1989
po/cs/neochat.po
1989
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1898
po/da/neochat.po
1898
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1903
po/de/neochat.po
1903
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1939
po/el/neochat.po
1939
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1897
po/en_GB/neochat.po
1897
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1973
po/es/neochat.po
1973
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1976
po/eu/neochat.po
1976
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1899
po/fi/neochat.po
1899
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
2008
po/fr/neochat.po
2008
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1909
po/hu/neochat.po
1909
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1894
po/ia/neochat.po
1894
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1892
po/id/neochat.po
1892
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1908
po/ie/neochat.po
1908
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
2012
po/it/neochat.po
2012
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1841
po/ja/neochat.po
1841
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1968
po/ka/neochat.po
1968
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1894
po/ko/neochat.po
1894
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
1847
po/lt/neochat.po
1847
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1977
po/nl/neochat.po
1977
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1458
po/nn/neochat.po
1458
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1936
po/pa/neochat.po
1936
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
2115
po/pl/neochat.po
2115
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1888
po/pt/neochat.po
1888
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1950
po/pt_BR/neochat.po
1950
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1902
po/ru/neochat.po
1902
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1944
po/sk/neochat.po
1944
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1974
po/sl/neochat.po
1974
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1970
po/sv/neochat.po
1970
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
2008
po/ta/neochat.po
2008
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1875
po/tok/neochat.po
1875
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
1968
po/tr/neochat.po
1968
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1979
po/uk/neochat.po
1979
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
2015
po/zh_CN/neochat.po
2015
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1841
po/zh_TW/neochat.po
1841
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -126,6 +126,17 @@ add_library(neochat STATIC
|
||||
registration.cpp
|
||||
neochatconnection.cpp
|
||||
neochatconnection.h
|
||||
jobs/neochatdeactivateaccountjob.cpp
|
||||
jobs/neochatdeactivateaccountjob.h
|
||||
jobs/neochatdeletedevicejob.cpp
|
||||
jobs/neochatdeletedevicejob.h
|
||||
jobs/neochatchangepasswordjob.cpp
|
||||
jobs/neochatchangepasswordjob.h
|
||||
mediasizehelper.cpp
|
||||
mediasizehelper.h
|
||||
eventhandler.cpp
|
||||
enums/delegatetype.h
|
||||
messageformatter.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
@@ -137,6 +148,13 @@ ecm_qt_declare_logging_category(neochat
|
||||
EXPORT NEOCHAT
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "eventhandler_logging.h"
|
||||
IDENTIFIER "EventHandling"
|
||||
CATEGORY_NAME "org.kde.neochat.eventhandler"
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
main.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
|
||||
@@ -182,7 +200,7 @@ else()
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 Qt::Xml KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels KF${QT_MAJOR_VERSION}::SyntaxHighlighting Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
|
||||
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
class CustomEmojiModel;
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "models/completionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class QTextDocument;
|
||||
class NeoChatRoom;
|
||||
class SyntaxHighlighter;
|
||||
|
||||
|
||||
@@ -97,8 +97,6 @@ Controller::Controller(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
|
||||
|
||||
static int oldAccountCount = 0;
|
||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||
if (m_accountRegistry.size() > oldAccountCount) {
|
||||
@@ -165,15 +163,14 @@ void Controller::addConnection(NeoChatConnection *c)
|
||||
c->sync();
|
||||
|
||||
Q_EMIT connectionAdded(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
}
|
||||
|
||||
void Controller::dropConnection(NeoChatConnection *c)
|
||||
{
|
||||
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
|
||||
|
||||
m_accountRegistry.drop(c);
|
||||
Q_EMIT connectionDropped(c);
|
||||
Q_EMIT accountCountChanged();
|
||||
}
|
||||
|
||||
void Controller::invokeLogin()
|
||||
@@ -297,29 +294,6 @@ bool Controller::supportSystemTray() const
|
||||
#endif
|
||||
}
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(_data);
|
||||
}
|
||||
|
||||
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
|
||||
{
|
||||
QJsonObject data;
|
||||
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
|
||||
setRequestData(data);
|
||||
}
|
||||
|
||||
int Controller::accountCount() const
|
||||
{
|
||||
return m_accountRegistry.count();
|
||||
}
|
||||
|
||||
void Controller::setQuitOnLastWindowClosed()
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
@@ -397,44 +371,6 @@ void Controller::saveWindowGeometry()
|
||||
WindowController::instance().saveGeometry();
|
||||
}
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
|
||||
void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, QString(), name, topic, QStringList());
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Controller::createSpace(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
|
||||
{},
|
||||
name,
|
||||
topic,
|
||||
QStringList(),
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
{},
|
||||
{},
|
||||
QJsonObject{
|
||||
{"type"_ls, "m.space"_ls},
|
||||
});
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
bool Controller::isOnline() const
|
||||
{
|
||||
return m_isOnline;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
class NeoChatRoom;
|
||||
class TrayIcon;
|
||||
class QWindow;
|
||||
class QQuickTextDocument;
|
||||
|
||||
namespace Quotient
|
||||
@@ -42,11 +41,6 @@ class Controller : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The number of logged in accounts.
|
||||
*/
|
||||
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
|
||||
|
||||
/**
|
||||
* @brief The current connection for the rest of NeoChat to use.
|
||||
*/
|
||||
@@ -97,8 +91,6 @@ public:
|
||||
|
||||
static Controller &instance();
|
||||
|
||||
[[nodiscard]] int accountCount() const;
|
||||
|
||||
void setActiveConnection(NeoChatConnection *connection);
|
||||
[[nodiscard]] NeoChatConnection *activeConnection() const;
|
||||
|
||||
@@ -121,16 +113,6 @@ public:
|
||||
*/
|
||||
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
/**
|
||||
* @brief Create new room for a group chat.
|
||||
*/
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Create new space.
|
||||
*/
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Join a room.
|
||||
*/
|
||||
@@ -216,42 +198,15 @@ Q_SIGNALS:
|
||||
void syncDone();
|
||||
void connectionAdded(NeoChatConnection *connection);
|
||||
void connectionDropped(NeoChatConnection *connection);
|
||||
void accountCountChanged();
|
||||
void initiated();
|
||||
void notificationClicked(const QString &_t1, const QString &_t2);
|
||||
void quitOnLastWindowClosedChanged();
|
||||
void unreadCountChanged();
|
||||
void activeConnectionChanged();
|
||||
void passwordStatus(Controller::PasswordStatus _t1);
|
||||
void passwordStatus(Controller::PasswordStatus status);
|
||||
void userConsentRequired(QUrl url);
|
||||
void testConnectionResult(const QString &connection, bool usable);
|
||||
void isOnlineChanged(bool isOnline);
|
||||
void keyVerificationRequest(int timeLeft, NeoChatConnection *connection, const QString &transactionId, const QString &deviceId);
|
||||
void keyVerificationStart();
|
||||
void keyVerificationAccept(const QString &commitment);
|
||||
void keyVerificationKey(const QString &sas);
|
||||
void activeConnectionIndexChanged();
|
||||
void roomAdded(NeoChatRoom *room);
|
||||
|
||||
public Q_SLOTS:
|
||||
void saveWindowGeometry();
|
||||
};
|
||||
|
||||
// TODO libQuotient 0.7: Drop
|
||||
class NeochatChangePasswordJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
class NeochatDeleteDeviceJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
|
||||
43
src/enums/delegatetype.h
Normal file
43
src/enums/delegatetype.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
/**
|
||||
* @class DelegateType
|
||||
*
|
||||
* This class is designed to define the DelegateType enumeration.
|
||||
*/
|
||||
class DelegateType
|
||||
{
|
||||
Q_GADGET
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The type of delegate that is needed for the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
enum Type {
|
||||
Emote, /**< A message that begins with /me. */
|
||||
Notice, /**< A notice event. */
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
Video, /**< A message that is a video. */
|
||||
File, /**< A message that is a file. */
|
||||
Message, /**< A text message. */
|
||||
Sticker, /**< A message that is a sticker. */
|
||||
State, /**< A state event in the room. */
|
||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||
ReadMarker, /**< The local user read marker. */
|
||||
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). */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(Type);
|
||||
};
|
||||
988
src/eventhandler.cpp
Normal file
988
src/eventhandler.cpp
Normal file
@@ -0,0 +1,988 @@
|
||||
// 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 "eventhandler.h"
|
||||
|
||||
#include <QMovie>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <Quotient/eventitem.h>
|
||||
#include <Quotient/events/encryptionevent.h>
|
||||
#include <Quotient/events/reactionevent.h>
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roomavatarevent.h>
|
||||
#include <Quotient/events/roomcanonicalaliasevent.h>
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
#include <Quotient/events/roompowerlevelsevent.h>
|
||||
#include <Quotient/events/simplestateevents.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
|
||||
#include "eventhandler_logging.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
const NeoChatRoom *EventHandler::getRoom() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
void EventHandler::setRoom(const NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_room) {
|
||||
return;
|
||||
}
|
||||
m_room = room;
|
||||
}
|
||||
|
||||
const Quotient::Event *EventHandler::getEvent() const
|
||||
{
|
||||
return m_event;
|
||||
}
|
||||
|
||||
void EventHandler::setEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (event == m_event) {
|
||||
return;
|
||||
}
|
||||
m_event = event;
|
||||
}
|
||||
|
||||
QString EventHandler::getId() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
|
||||
{
|
||||
if (auto e = eventCast<const RoomMessageEvent>(event)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
return DelegateType::Emote;
|
||||
case MessageEventType::Notice:
|
||||
return DelegateType::Notice;
|
||||
case MessageEventType::Image:
|
||||
return DelegateType::Image;
|
||||
case MessageEventType::Audio:
|
||||
return DelegateType::Audio;
|
||||
case MessageEventType::Video:
|
||||
return DelegateType::Video;
|
||||
case MessageEventType::Location:
|
||||
return DelegateType::Location;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (e->hasFileContent()) {
|
||||
return DelegateType::File;
|
||||
}
|
||||
|
||||
return DelegateType::Message;
|
||||
}
|
||||
if (is<const StickerEvent>(*event)) {
|
||||
return DelegateType::Sticker;
|
||||
}
|
||||
if (event->isStateEvent()) {
|
||||
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||
return DelegateType::LiveLocation;
|
||||
}
|
||||
return DelegateType::State;
|
||||
}
|
||||
if (is<const EncryptedEvent>(*event)) {
|
||||
return DelegateType::Encrypted;
|
||||
}
|
||||
if (is<PollStartEvent>(*event)) {
|
||||
const auto pollEvent = eventCast<const PollStartEvent>(event);
|
||||
if (pollEvent->isRedacted()) {
|
||||
return DelegateType::Message;
|
||||
}
|
||||
return DelegateType::Poll;
|
||||
}
|
||||
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getDelegateType() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
|
||||
return DelegateType::Other;
|
||||
}
|
||||
|
||||
return getDelegateTypeForEvent(m_event);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getAuthor(bool isPending) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
return m_room->getUser(author);
|
||||
}
|
||||
|
||||
QString EventHandler::getAuthorDisplayName(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 {};
|
||||
}
|
||||
|
||||
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
|
||||
&& m_event->stateKey() == m_event->senderId()) {
|
||||
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
|
||||
if (previousDisplayName.isEmpty()) {
|
||||
previousDisplayName = m_event->senderId();
|
||||
}
|
||||
return previousDisplayName;
|
||||
} else {
|
||||
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||
return m_room->htmlSafeMemberName(author->id());
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (isPending && lastUpdated == QDateTime()) {
|
||||
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||
return {};
|
||||
}
|
||||
|
||||
return isPending ? lastUpdated : m_event->originTimestamp();
|
||||
}
|
||||
|
||||
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (isPending && lastUpdated == QDateTime()) {
|
||||
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ts = getTime(isPending, lastUpdated);
|
||||
if (ts.isValid()) {
|
||||
if (relative) {
|
||||
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
|
||||
} else {
|
||||
return QLocale().toString(ts.toLocalTime().time(), format);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool EventHandler::isHighlighted()
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
|
||||
return false;
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
|
||||
}
|
||||
|
||||
bool EventHandler::isHidden()
|
||||
{
|
||||
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
|
||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
return true;
|
||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||
return true;
|
||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// isReplacement?
|
||||
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||
if (!e->replacedEvent().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// hide ending live location beacons
|
||||
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString EventHandler::getRichBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return getBody(m_event, Qt::RichText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getPlainBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return getBody(m_event, Qt::PlainText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (event->isRedacted()) {
|
||||
auto reason = event->redactedBecause()->reason();
|
||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
|
||||
}
|
||||
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
return switchOnType(
|
||||
*event,
|
||||
[this, format, stripNewlines](const RoomMessageEvent &event) {
|
||||
return getMessageBody(event, format, stripNewlines);
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = m_room->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == Membership::Leave) {
|
||||
if (e.prevContent() && e.prevContent()->displayName) {
|
||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||
}
|
||||
}
|
||||
|
||||
if (prettyPrint) {
|
||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
|
||||
}
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18n(": %1", e.reason().toHtmlEscaped());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (!e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||
: i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
if (e.reason().isEmpty()) {
|
||||
return i18n("banned %1 from the room", subjectName);
|
||||
} else {
|
||||
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
|
||||
}
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
||||
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
||||
},
|
||||
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||
: i18n("set the topic to: %1",
|
||||
prettyPrint ? Quotient::prettyPrint(e.topic())
|
||||
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
||||
: e.topic());
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
|
||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
|
||||
}
|
||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||
return e.contentJson()["description"_ls].toString();
|
||||
}
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
TextHandler textHandler;
|
||||
|
||||
if (event.hasFileContent()) {
|
||||
auto fileCaption = event.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = event.plainBody();
|
||||
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
|
||||
fileCaption = event.plainBody() + " | "_ls + fileCaption;
|
||||
}
|
||||
textHandler.setData(fileCaption);
|
||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
||||
}
|
||||
|
||||
QString body;
|
||||
if (event.hasTextContent() && event.content()) {
|
||||
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
|
||||
} else {
|
||||
body = event.plainBody();
|
||||
}
|
||||
|
||||
textHandler.setData(body);
|
||||
|
||||
Qt::TextFormat inputFormat;
|
||||
if (event.mimeType().name() == "text/plain"_ls) {
|
||||
inputFormat = Qt::PlainText;
|
||||
} else {
|
||||
inputFormat = Qt::RichText;
|
||||
}
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getGenericBody() const
|
||||
{
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event->isRedacted()) {
|
||||
return i18n("<i>[This message was deleted]</i>");
|
||||
}
|
||||
|
||||
return switchOnType(
|
||||
*m_event,
|
||||
[](const RoomMessageEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a message");
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a sticker");
|
||||
},
|
||||
[](const RoomMemberEvent &e) {
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
return i18n("reinvited someone to the room");
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (!e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name");
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
return i18n("banned a user from the room");
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
return i18n("requested an invite");
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
||||
},
|
||||
[](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18n("added a widget");
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18n("removed a widget");
|
||||
}
|
||||
return i18n("configured a widget");
|
||||
}
|
||||
return i18n("updated the state");
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
Q_UNUSED(e);
|
||||
return i18n("started a poll");
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfo() const
|
||||
{
|
||||
return getMediaInfoForEvent(m_event);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
|
||||
{
|
||||
QString eventId = event->id();
|
||||
|
||||
// Get the file info for the event.
|
||||
const EventContent::FileInfo *fileInfo;
|
||||
if (event->is<RoomMessageEvent>()) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
|
||||
if (!roomMessageEvent->hasFileContent()) {
|
||||
return {};
|
||||
}
|
||||
fileInfo = roomMessageEvent->content()->fileInfo();
|
||||
} else if (event->is<StickerEvent>()) {
|
||||
auto stickerEvent = eventCast<const StickerEvent>(event);
|
||||
fileInfo = &stickerEvent->image();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
// Get the mxc URL for the media.
|
||||
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
||||
|
||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
||||
mediaInfo["source"_ls] = source;
|
||||
} else {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
}
|
||||
}
|
||||
|
||||
auto mimeType = fileInfo->mimeType;
|
||||
// Add the MIME type for the media if available.
|
||||
mediaInfo["mimeType"_ls] = mimeType.name();
|
||||
|
||||
// Add the MIME type icon if available.
|
||||
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
|
||||
|
||||
// Add media size if available.
|
||||
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
||||
|
||||
// Add parameter depending on media type.
|
||||
if (mimeType.name().contains(QStringLiteral("image"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
|
||||
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
|
||||
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("video"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("audio"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
}
|
||||
}
|
||||
|
||||
return mediaInfo;
|
||||
}
|
||||
|
||||
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
|
||||
{
|
||||
if (!m_event->is<RoomMessageEvent>()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString text;
|
||||
auto event = eventCast<const RoomMessageEvent>(m_event);
|
||||
if (event->hasTextContent()) {
|
||||
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
||||
if (textContent) {
|
||||
text = textContent->body;
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(text);
|
||||
|
||||
QList<QUrl> links = textHandler.getLinkPreviews();
|
||||
if (links.size() > 0) {
|
||||
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QSharedPointer<ReactionModel> EventHandler::getReactions() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
|
||||
return nullptr;
|
||||
}
|
||||
if (!m_event->is<RoomMessageEvent>()) {
|
||||
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eventId = m_event->id();
|
||||
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
|
||||
if (annotations.isEmpty()) {
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
QMap<QString, QList<User *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->key()].append(m_room->user(e->senderId()));
|
||||
}
|
||||
}
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QList<ReactionModel::Reaction> res;
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(m_room->getUser(author));
|
||||
}
|
||||
|
||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (res.size() > 0) {
|
||||
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool EventHandler::hasReply() const
|
||||
{
|
||||
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyId() const
|
||||
{
|
||||
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||
}
|
||||
|
||||
DelegateType::Type EventHandler::getReplyDelegateType() const
|
||||
{
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return DelegateType::Other;
|
||||
}
|
||||
return getDelegateTypeForEvent(replyEvent);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getReplyAuthor() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
|
||||
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||
|
||||
if (replyPtr) {
|
||||
auto replyUser = m_room->user(replyPtr->senderId());
|
||||
return m_room->getUser(replyUser);
|
||||
} else {
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyRichBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getBody(replyEvent, Qt::RichText, stripNewlines);
|
||||
}
|
||||
|
||||
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||
if (replyEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getBody(replyEvent, Qt::PlainText, stripNewlines);
|
||||
}
|
||||
|
||||
QVariantMap EventHandler::getReplyMediaInfo() const
|
||||
{
|
||||
if (m_room == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
if (m_event == nullptr) {
|
||||
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
return getMediaInfoForEvent(replyPtr);
|
||||
}
|
||||
|
||||
float EventHandler::getLatitude() const
|
||||
{
|
||||
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
float EventHandler::getLongitude() const
|
||||
{
|
||||
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
QString EventHandler::getLocationAssetType() const
|
||||
{
|
||||
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
||||
if (assetType.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return assetType;
|
||||
}
|
||||
|
||||
bool EventHandler::hasReadMarkers() const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
return userIds.size() > 0;
|
||||
}
|
||||
|
||||
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
|
||||
{
|
||||
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds_temp.remove(m_room->localUser()->id());
|
||||
|
||||
auto userIds = userIds_temp.values();
|
||||
if (userIds.count() > maxMarkers) {
|
||||
userIds = userIds.mid(0, maxMarkers);
|
||||
}
|
||||
|
||||
QVariantList users;
|
||||
users.reserve(userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_room->user(userId);
|
||||
users += m_room->getUser(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
|
||||
if (userIds.count() > maxMarkers) {
|
||||
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString EventHandler::getReadMarkersString() const
|
||||
{
|
||||
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||
userIds.remove(m_room->localUser()->id());
|
||||
|
||||
/**
|
||||
* The string ends up in the form
|
||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||
*/
|
||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_room->user(userId);
|
||||
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
|
||||
}
|
||||
readMarkersString.chop(2);
|
||||
return readMarkersString;
|
||||
}
|
||||
400
src/eventhandler.h
Normal file
400
src/eventhandler.h
Normal file
@@ -0,0 +1,400 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include <Quotient/eventitem.h>
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
|
||||
class LinkPreviewer;
|
||||
class NeoChatRoom;
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class EventHandler
|
||||
*
|
||||
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
|
||||
* in a form ready for the NeoChat UI.
|
||||
*
|
||||
* To use this properly both the room and the event should be set (and the event should
|
||||
* be from the given room).
|
||||
*
|
||||
* @note EventHandler will always try to return something even when not properly
|
||||
* initialised, this is usually the best empty value it can create with available
|
||||
* information. This is to minimize warnings from QML especially during startup
|
||||
* and room changes.
|
||||
*/
|
||||
class EventHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Return the current room the EventHandler is using.
|
||||
*/
|
||||
const NeoChatRoom *getRoom() const;
|
||||
|
||||
/**
|
||||
* @brief Set the current room the EventHandler to using.
|
||||
*/
|
||||
void setRoom(const NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief Return the current event the EventHandler is using.
|
||||
*/
|
||||
const Quotient::Event *getEvent() const;
|
||||
|
||||
/**
|
||||
* @brief Set the current event the EventHandler to using.
|
||||
*/
|
||||
void setEvent(const Quotient::RoomEvent *event);
|
||||
|
||||
/**
|
||||
* @brief Return the Matrix ID of the event.
|
||||
*/
|
||||
QString getId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
DelegateType::Type getDelegateType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event in context of the room.
|
||||
*
|
||||
* This is different to getting a Quotient::User object
|
||||
* as neither of those can provide details like the displayName or avatarMediaId
|
||||
* without the room context as these can vary from room to room. This function
|
||||
* uses the room context and outputs the result as QVariantMap.
|
||||
*
|
||||
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||
* but empty values) will be returned if the room has been set but not an event.
|
||||
*
|
||||
* @param isPending if the event is pending, i.e. has not been confirmed by
|
||||
* the server.
|
||||
*
|
||||
* @return a QVariantMap for the user with the following properties:
|
||||
* - isLocalUser - Whether the user is the local user.
|
||||
* - id - The matrix ID of the user.
|
||||
* - displayName - Display name in the context of this room.
|
||||
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||
* - avatarMediaId - Avatar id in the context of this room.
|
||||
* - color - Color for the user.
|
||||
* - object - The Quotient::User object for the user.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
QVariantMap getAuthor(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Get the display name of the event author.
|
||||
*
|
||||
* This method is separate from getAuthor() and special in that it will return
|
||||
* the old display name of the author if the current event is one that caused it
|
||||
* to change. This allows for scenarios where the UI wishes to notify that a
|
||||
* user's display name has changed and what it changed from.
|
||||
*
|
||||
* @param isPending whether the event is pending as this cannot be derived from
|
||||
* just the event object.
|
||||
*/
|
||||
QString getAuthorDisplayName(bool isPending = false) const;
|
||||
|
||||
/**
|
||||
* @brief Return a QDateTime object for the event timestamp.
|
||||
*/
|
||||
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||
|
||||
/**
|
||||
* @brief Return a QString for the event timestamp.
|
||||
*
|
||||
* This is intended to return a string that is read for display in the UI without
|
||||
* any further manipulation required.
|
||||
*
|
||||
* @param relative whether the string is realtive to the current date, i.e.
|
||||
* Yesterday or Wednesday, etc.
|
||||
* @param format the QLocale::FormatType to use.
|
||||
* @param isPending whether the event is pending as this cannot be derived from
|
||||
* just the event object.
|
||||
* @param lastUpdated the time the event was last updated locally as this cannot be
|
||||
* obtained from the event.
|
||||
*/
|
||||
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event should be highlighted in the timeline.
|
||||
*
|
||||
* @note Messages in direct chats are never highlighted.
|
||||
*/
|
||||
bool isHighlighted();
|
||||
|
||||
/**
|
||||
* @brief Whether the event should be hidden in the timeline.
|
||||
*
|
||||
* This could be for numerous reasons, e.g. if it's a replacement event, if the
|
||||
* user has hidden all state events or if the sender has been ignored by the local
|
||||
* user.
|
||||
*/
|
||||
bool isHidden();
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display in a rich text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getRichBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display in a plain text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getPlainBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a generic string for the message content ready for display.
|
||||
*
|
||||
* The output string is dependant upon the event type.
|
||||
*
|
||||
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
|
||||
* is the same for all events of the same type.
|
||||
*
|
||||
* E.g. For a message the text will be:
|
||||
* "sent a message"
|
||||
*
|
||||
* @sa getRichBody(), getPlainBody()
|
||||
*/
|
||||
QString getGenericBody() const;
|
||||
|
||||
/**
|
||||
* @brief Return the media info for the event.
|
||||
*
|
||||
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||
* media info.
|
||||
*
|
||||
* @return This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
QVariantMap getMediaInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Return a LinkPreviewer object for the event.
|
||||
*
|
||||
* A nullptr will be returned for any event that doesn't have any links so the
|
||||
* return should be null checked and an empty LinkPreviewer provided if null.
|
||||
*
|
||||
* @sa LinkPreviewer
|
||||
*/
|
||||
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
|
||||
|
||||
/**
|
||||
* @brief Return a ReactionModel object for the event.
|
||||
*
|
||||
* A nullptr will be returned for any event that doesn't have any links so the
|
||||
* return should be null checked and an empty QVariantList (or other suitable
|
||||
* empty mode) provided if null.
|
||||
*/
|
||||
QSharedPointer<ReactionModel> getReactions() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event is a reply to another in the timeline.
|
||||
*/
|
||||
bool hasReply() const;
|
||||
|
||||
/**
|
||||
* @brief Return the Matrix ID of the event replied to.
|
||||
*/
|
||||
QString getReplyId() const;
|
||||
|
||||
/**
|
||||
* @brief Return the DelegateType of the event replied to.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
DelegateType::Type getReplyDelegateType() const;
|
||||
|
||||
/**
|
||||
* @brief Get the author of the event replied to in context of the room.
|
||||
*
|
||||
* This is different to getting a Quotient::User object
|
||||
* as neither of those can provide details like the displayName or avatarMediaId
|
||||
* without the room context as these can vary from room to room. This function
|
||||
* uses the room context and outputs the result as QVariantMap.
|
||||
*
|
||||
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||
* but empty values) will be returned if the room has been set but not an event.
|
||||
*
|
||||
* @return a QVariantMap for the user with the following properties:
|
||||
* - isLocalUser - Whether the user is the local user.
|
||||
* - id - The matrix ID of the user.
|
||||
* - displayName - Display name in the context of this room.
|
||||
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||
* - avatarMediaId - Avatar id in the context of this room.
|
||||
* - color - Color for the user.
|
||||
* - object - The Quotient::User object for the user.
|
||||
*
|
||||
* @sa Quotient::User
|
||||
*/
|
||||
QVariantMap getReplyAuthor() const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content of the event replied to ready
|
||||
* for display in a rich text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getReplyRichBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content of the event replied to ready
|
||||
* for display in a plain text field.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* this will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
QString getReplyPlainBody(bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Return the media info for the event replied to.
|
||||
*
|
||||
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||
* media info.
|
||||
*
|
||||
* @return This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
QVariantMap getReplyMediaInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Return the latitude for the event.
|
||||
*
|
||||
* Returns -100.0 if the event doesn't have a location (latitudes are in the
|
||||
* range -90deg to +90deg so -100 is out of range).
|
||||
*/
|
||||
float getLatitude() const;
|
||||
|
||||
/**
|
||||
* @brief Return the longitude for the event.
|
||||
*
|
||||
* Returns -200.0 if the event doesn't have a location (latitudes are in the
|
||||
* range -180deg to +180deg so -200 is out of range).
|
||||
*/
|
||||
float getLongitude() const;
|
||||
|
||||
/**
|
||||
* @brief Return the type of location marker for the event.
|
||||
*/
|
||||
QString getLocationAssetType() const;
|
||||
|
||||
/**
|
||||
* @brief Whether the event has any read marker for other users.
|
||||
*/
|
||||
bool hasReadMarkers() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a list of user read marker for the event.
|
||||
*
|
||||
* @param maxMarkers the maximum number of users to return. Usually the number
|
||||
* of user read makers shown is limited to not clutter the UI.
|
||||
* This needs to be the same as used in getNumberExcessReadMarkers
|
||||
* so that the markers line up with the number displayed, i.e.
|
||||
* the number of users shown plus the excess number will be
|
||||
* the total number of other user read markers at an event.
|
||||
*/
|
||||
QVariantList getReadMarkers(int maxMarkers = 5) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the number of excess user read markers for the event.
|
||||
*
|
||||
* This returns a string in the form "+ x" ready for use in the UI.
|
||||
*
|
||||
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
|
||||
* be the same as used in getReadMarkers so that the value lines
|
||||
* up with the number displayed, i.e. the number of users shown
|
||||
* plus the excess number will be the total number of other user
|
||||
* read markers at an event.
|
||||
*/
|
||||
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
|
||||
|
||||
/**
|
||||
* @brief Returns a string with the names of the read markers at the event.
|
||||
*
|
||||
* This is in the form "x users: name 1, name 2, ...".
|
||||
*/
|
||||
QString getReadMarkersString() const;
|
||||
|
||||
private:
|
||||
const NeoChatRoom *m_room = nullptr;
|
||||
const Quotient::RoomEvent *m_event = nullptr;
|
||||
|
||||
KFormat m_format;
|
||||
|
||||
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
|
||||
|
||||
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
|
||||
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
||||
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
};
|
||||
16
src/jobs/neochatchangepasswordjob.cpp
Normal file
16
src/jobs/neochatchangepasswordjob.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatchangepasswordjob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<>(_data, QStringLiteral("new_password"), newPassword);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(_data);
|
||||
}
|
||||
13
src/jobs/neochatchangepasswordjob.h
Normal file
13
src/jobs/neochatchangepasswordjob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeochatChangePasswordJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
14
src/jobs/neochatdeactivateaccountjob.cpp
Normal file
14
src/jobs/neochatdeactivateaccountjob.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatdeactivateaccountjob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
|
||||
{
|
||||
QJsonObject data;
|
||||
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
|
||||
setRequestData(data);
|
||||
}
|
||||
13
src/jobs/neochatdeactivateaccountjob.h
Normal file
13
src/jobs/neochatdeactivateaccountjob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
14
src/jobs/neochatdeletedevicejob.cpp
Normal file
14
src/jobs/neochatdeletedevicejob.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "neochatdeletedevicejob.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
|
||||
{
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
}
|
||||
13
src/jobs/neochatdeletedevicejob.h
Normal file
13
src/jobs/neochatdeletedevicejob.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
|
||||
class NeochatDeleteDeviceJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
|
||||
};
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
|
||||
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
|
||||
: QObject(parent)
|
||||
, m_currentRoom(room)
|
||||
, m_loaded(false)
|
||||
@@ -93,4 +93,9 @@ void LinkPreviewer::loadUrlPreview()
|
||||
}
|
||||
}
|
||||
|
||||
bool LinkPreviewer::empty() const
|
||||
{
|
||||
return m_url.isEmpty();
|
||||
}
|
||||
|
||||
#include "moc_linkpreviewer.cpp"
|
||||
|
||||
@@ -44,8 +44,15 @@ class LinkPreviewer : public QObject
|
||||
*/
|
||||
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the there is a link to preview.
|
||||
*
|
||||
* A linkPreviwer is empty if the URL is empty.
|
||||
*/
|
||||
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
|
||||
|
||||
public:
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||
|
||||
[[nodiscard]] QUrl url() const;
|
||||
void setUrl(QUrl);
|
||||
@@ -53,9 +60,10 @@ public:
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] QString description() const;
|
||||
[[nodiscard]] QUrl imageSource() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_currentRoom = nullptr;
|
||||
const NeoChatRoom *m_currentRoom = nullptr;
|
||||
|
||||
bool m_loaded;
|
||||
QString m_title = QString();
|
||||
@@ -71,5 +79,6 @@ Q_SIGNALS:
|
||||
void descriptionChanged();
|
||||
void imageSourceChanged();
|
||||
void urlChanged();
|
||||
void emptyChanged();
|
||||
};
|
||||
Q_DECLARE_METATYPE(LinkPreviewer *)
|
||||
|
||||
20
src/main.cpp
20
src/main.cpp
@@ -47,12 +47,14 @@
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "delegatesizehelper.h"
|
||||
#include "enums/delegatetype.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "locationhelper.h"
|
||||
#include "logger.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "mediasizehelper.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
@@ -105,6 +107,8 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include "messageformatter.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
||||
@@ -146,6 +150,10 @@ int main(int argc, char *argv[])
|
||||
|
||||
#ifdef HAVE_WEBVIEW
|
||||
QtWebView::initialize();
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
|
||||
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -222,6 +230,12 @@ int main(int argc, char *argv[])
|
||||
Login *login = new Login();
|
||||
UrlHelper urlHelper;
|
||||
|
||||
MessageFormatter formatter;
|
||||
// formatter.formatInternal("<p>hrrejoire</p>\n<pre><code class=\"language-js\">var i = 0; i++; function\n</code></pre>\n<p>rekore</p>\n", new
|
||||
// QTextDocument);
|
||||
|
||||
// return 0;
|
||||
|
||||
#ifdef HAVE_COLORSCHEME
|
||||
ColorSchemer colorScheme;
|
||||
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
|
||||
@@ -230,6 +244,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
qmlRegisterSingletonInstance<MessageFormatter>("org.kde.neochat", 1, 0, "MessageFormatter", &formatter);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||
@@ -274,7 +289,8 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
|
||||
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
||||
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"_ls);
|
||||
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
|
||||
qmlRegisterUncreatableType<DelegateType>("org.kde.neochat", 1, 0, "DelegateType", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
|
||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
|
||||
@@ -362,7 +378,7 @@ int main(int argc, char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (parser.positionalArguments().length() > 0) {
|
||||
if (!parser.positionalArguments().isEmpty()) {
|
||||
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
|
||||
#include <QReadWriteLock>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ThumbnailResponse
|
||||
*
|
||||
|
||||
163
src/mediasizehelper.cpp
Normal file
163
src/mediasizehelper.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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 "mediasizehelper.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
MediaSizeHelper::MediaSizeHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxWidth() const
|
||||
{
|
||||
return m_contentMaxWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
|
||||
{
|
||||
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
|
||||
m_contentMaxWidth = -1.0;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxWidth = contentMaxWidth;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxHeight() const
|
||||
{
|
||||
return m_contentMaxHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
|
||||
{
|
||||
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
|
||||
m_contentMaxHeight = -1.0;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxHeight = contentMaxHeight;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaWidth() const
|
||||
{
|
||||
return m_mediaWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
|
||||
{
|
||||
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
|
||||
m_mediaWidth = -1.0;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
|
||||
return;
|
||||
}
|
||||
m_mediaWidth = mediaWidth;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaHeight() const
|
||||
{
|
||||
return m_mediaHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
|
||||
{
|
||||
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
|
||||
m_mediaHeight = -1.0;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
|
||||
return;
|
||||
}
|
||||
m_mediaHeight = mediaHeight;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaWidth() const
|
||||
{
|
||||
if (m_mediaWidth > 0.0) {
|
||||
return m_mediaWidth;
|
||||
}
|
||||
return widthLimit();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaHeight() const
|
||||
{
|
||||
if (m_mediaHeight > 0.0) {
|
||||
return m_mediaHeight;
|
||||
}
|
||||
return widthLimit() / 16.0 * 9.0;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::aspectRatio() const
|
||||
{
|
||||
return resolvedMediaWidth() / resolvedMediaHeight();
|
||||
}
|
||||
|
||||
bool MediaSizeHelper::limitWidth() const
|
||||
{
|
||||
// If actual data isn't available we'll be using a placeholder that is width
|
||||
// limited so return true.
|
||||
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
|
||||
return true;
|
||||
}
|
||||
return m_mediaWidth >= m_mediaHeight;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::widthLimit() const
|
||||
{
|
||||
if (m_contentMaxWidth < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxWidth();
|
||||
}
|
||||
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::heightLimit() const
|
||||
{
|
||||
if (m_contentMaxHeight < 0.0) {
|
||||
return NeoChatConfig::self()->mediaMaxHeight();
|
||||
}
|
||||
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
|
||||
}
|
||||
|
||||
QSize MediaSizeHelper::currentSize() const
|
||||
{
|
||||
if (limitWidth()) {
|
||||
qreal width = std::min(widthLimit(), resolvedMediaWidth());
|
||||
qreal height = width / aspectRatio();
|
||||
if (height > heightLimit()) {
|
||||
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
} else {
|
||||
qreal height = std::min(heightLimit(), resolvedMediaHeight());
|
||||
qreal width = height * aspectRatio();
|
||||
if (width > widthLimit()) {
|
||||
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_mediasizehelper.cpp"
|
||||
103
src/mediasizehelper.h
Normal file
103
src/mediasizehelper.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSize>
|
||||
|
||||
/**
|
||||
* @class MediaSizeHelper
|
||||
*
|
||||
* A class to help calculate the current width of a media item within a chat delegate.
|
||||
*
|
||||
* The only realistic way to guarantee that a media item (e.g. an image or video)
|
||||
* is the correct size in QML is to calculate the size manually.
|
||||
*
|
||||
* The rules for this component work as follows:
|
||||
* - The output will always try to keep the media size if no limits are breached.
|
||||
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
|
||||
* calcualated from either the configured max width or the contentMaxWidth, whichever
|
||||
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
|
||||
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
|
||||
* - The current size will never be larger than any of the limits in either direction.
|
||||
* - If any limit is breached the image size will be reduced while maintaining aspect
|
||||
* ration, i.e. no stretching or squashing. This can mean that the width or height
|
||||
* is reduced even if that parameter doesn't breach the limit itself.
|
||||
*/
|
||||
class MediaSizeHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The maximum width (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The maximum height (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The base width (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The base height (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The size (in px) of the component based on the current input.
|
||||
*
|
||||
* Will always try to return a value even if some of the inputs are not set to
|
||||
* account for being called before the parameters are intialised. For any parameters
|
||||
* not set these will just be left out of the calcs.
|
||||
*
|
||||
* If no input values are provided a default placeholder value will be returned.
|
||||
*/
|
||||
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
|
||||
|
||||
public:
|
||||
explicit MediaSizeHelper(QObject *parent = nullptr);
|
||||
|
||||
qreal contentMaxWidth() const;
|
||||
void setContentMaxWidth(qreal contentMaxWidth);
|
||||
|
||||
qreal contentMaxHeight() const;
|
||||
void setContentMaxHeight(qreal contentMaxHeight);
|
||||
|
||||
qreal mediaWidth() const;
|
||||
void setMediaWidth(qreal mediaWidth);
|
||||
|
||||
qreal mediaHeight() const;
|
||||
void setMediaHeight(qreal mediaHeight);
|
||||
|
||||
QSize currentSize() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void contentMaxWidthChanged();
|
||||
void contentMaxHeightChanged();
|
||||
void mediaWidthChanged();
|
||||
void mediaHeightChanged();
|
||||
void currentSizeChanged();
|
||||
|
||||
private:
|
||||
qreal m_contentMaxWidth = -1.0;
|
||||
qreal m_contentMaxHeight = -1.0;
|
||||
qreal m_mediaWidth = -1.0;
|
||||
qreal m_mediaHeight = -1.0;
|
||||
|
||||
qreal resolvedMediaWidth() const;
|
||||
qreal resolvedMediaHeight() const;
|
||||
qreal aspectRatio() const;
|
||||
bool limitWidth() const;
|
||||
qreal widthLimit() const;
|
||||
qreal heightLimit() const;
|
||||
};
|
||||
133
src/messageformatter.cpp
Normal file
133
src/messageformatter.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "messageformatter.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QGuiApplication>
|
||||
#include <QPalette>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlProperty>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include <KSyntaxHighlighting/definition.h>
|
||||
#include <KSyntaxHighlighting/repository.h>
|
||||
#include <KSyntaxHighlighting/syntaxhighlighter.h>
|
||||
#include <KSyntaxHighlighting/theme.h>
|
||||
|
||||
QTextDocumentFragment copyTextLayoutFrom(QTextDocument *document)
|
||||
{
|
||||
QTextCursor sourceCursor(document);
|
||||
sourceCursor.select(QTextCursor::Document);
|
||||
|
||||
QTextDocument helper;
|
||||
|
||||
// copy the content fragment from the source document into our helper document
|
||||
QTextCursor curs(&helper);
|
||||
curs.insertFragment(sourceCursor.selection());
|
||||
curs.select(QTextCursor::Document);
|
||||
|
||||
// not sure why, but fonts get lost. since this is for codeblocks, we can
|
||||
// just force the mono font. anyone copying this code would probably want
|
||||
// to fix the problem proper if it's not also for codeblocks.
|
||||
const auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
|
||||
const int docStart = sourceCursor.selectionStart();
|
||||
const int docEnd = helper.characterCount() - 1;
|
||||
|
||||
// since the copied fragment above lost the qsyntaxhighlighter stuff,
|
||||
// we gotta go through the qtextlayout and apply those styles to the
|
||||
// document
|
||||
const auto end = document->findBlock(sourceCursor.selectionEnd()).next();
|
||||
for (auto current = document->findBlock(docStart); current.isValid() && current != end; current = current.next()) {
|
||||
const auto layout = current.layout();
|
||||
|
||||
// iterate through the formats, applying them to our document
|
||||
for (const auto &span : layout->formats()) {
|
||||
const int start = current.position() + span.start - docStart;
|
||||
const int end = start + span.length;
|
||||
|
||||
curs.setPosition(qMax(start, 0));
|
||||
curs.setPosition(qMin(end, docEnd), QTextCursor::KeepAnchor);
|
||||
|
||||
auto fmt = span.format;
|
||||
fmt.setFont(fixedFont);
|
||||
|
||||
curs.setCharFormat(fmt);
|
||||
}
|
||||
}
|
||||
|
||||
return QTextDocumentFragment(&helper);
|
||||
}
|
||||
|
||||
QTextDocumentFragment highlight(const QString &code, const QString &language)
|
||||
{
|
||||
using namespace KSyntaxHighlighting;
|
||||
|
||||
static Repository repo;
|
||||
|
||||
auto theme = repo.themeForPalette(QGuiApplication::palette());
|
||||
auto definition = repo.definitionForFileName(QLatin1String("file.") + language);
|
||||
|
||||
QTextDocument doku(code);
|
||||
|
||||
QScopedPointer<SyntaxHighlighter> highlighter(new SyntaxHighlighter(&doku));
|
||||
highlighter->setTheme(theme);
|
||||
highlighter->setDefinition(definition);
|
||||
|
||||
return copyTextLayoutFrom(&doku);
|
||||
}
|
||||
|
||||
bool extractCodeBlock(QTextCursor cursor, QDomElement element)
|
||||
{
|
||||
const auto codeNode = element.firstChild();
|
||||
if (!codeNode.isNull()) {
|
||||
const auto code = codeNode.toElement();
|
||||
if (!code.isNull() && code.tagName() == QLatin1String("code")) {
|
||||
QString lang;
|
||||
auto langClass = code.attribute(QLatin1String("class"), QLatin1String("none"));
|
||||
if (langClass != QLatin1String("none") && langClass.startsWith(QLatin1String("language-"))) {
|
||||
lang = langClass.remove(0, 9);
|
||||
}
|
||||
|
||||
if (!lang.isNull()) {
|
||||
cursor.insertFragment(highlight(code.text(), lang));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString MessageFormatter::formatInternal(const QString &messageBody, QTextDocument *document)
|
||||
{
|
||||
QTextCursor curs(document);
|
||||
QDomDocument doc(QLatin1String("htmlement"));
|
||||
doc.setContent(QStringLiteral("<div>%1</div>").arg(messageBody));
|
||||
QDomElement docElem = doc.documentElement();
|
||||
QDomNode n = docElem.firstChild();
|
||||
while (!n.isNull()) {
|
||||
QDomElement e = n.toElement();
|
||||
if (!e.isNull()) {
|
||||
if (e.tagName() != QLatin1String("pre") || !extractCodeBlock(curs, e)) {
|
||||
QString outText;
|
||||
QTextStream out(&outText);
|
||||
e.save(out, 0);
|
||||
curs.insertHtml(outText);
|
||||
}
|
||||
}
|
||||
n = n.nextSibling();
|
||||
}
|
||||
|
||||
Q_EMIT document->contentsChanged();
|
||||
return document->toHtml();
|
||||
}
|
||||
|
||||
QString MessageFormatter::format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item)
|
||||
{
|
||||
QColor linkColor = QQmlProperty(item, QLatin1String("Kirigami.Theme.linkColor"), qmlContext(item)).read().value<QColor>();
|
||||
|
||||
return formatInternal(messageBody, doc->textDocument());
|
||||
}
|
||||
13
src/messageformatter.h
Normal file
13
src/messageformatter.h
Normal file
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
|
||||
class MessageFormatter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE QString format(const QString &messageBody, QQuickTextDocument *doc, QQuickItem *item);
|
||||
Q_INVOKABLE QString formatInternal(const QString &messageBody, QTextDocument *doc);
|
||||
};
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
class ImagePacksModel;
|
||||
|
||||
/**
|
||||
* @class AccountEmoticonModel
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "devicesmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "jobs/neochatdeletedevicejob.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLocale>
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
#include <QPointer>
|
||||
#include <QRectF>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class RoomMessageEvent;
|
||||
}
|
||||
|
||||
struct LiveLocationData {
|
||||
QString eventId;
|
||||
QString senderId;
|
||||
|
||||
@@ -2,20 +2,26 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
#include "mediamessagefiltermodel.h"
|
||||
#include "models/messageeventmodel.h"
|
||||
|
||||
#include <Quotient/room.h>
|
||||
|
||||
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent)
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagefiltermodel.h"
|
||||
|
||||
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent, MessageFilterModel *sourceMediaModel)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceMediaModel);
|
||||
setSourceModel(sourceMediaModel);
|
||||
}
|
||||
|
||||
bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
|
||||
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image
|
||||
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
|
||||
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|
||||
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -24,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
||||
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == SourceRole) {
|
||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
|
||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
|
||||
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
|
||||
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
||||
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
|
||||
|
||||
if (progressInfo.completed()) {
|
||||
@@ -42,10 +48,10 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
|
||||
}
|
||||
if (role == TypeRole) {
|
||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
|
||||
return 0;
|
||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
||||
return MediaType::Image;
|
||||
} else {
|
||||
return 1;
|
||||
return MediaType::Video;
|
||||
}
|
||||
}
|
||||
if (role == CaptionRole) {
|
||||
@@ -57,6 +63,13 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
if (role == SourceHeightRole) {
|
||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
|
||||
}
|
||||
// We need to catch this one and return true if the next media object was
|
||||
// on a different day.
|
||||
if (role == MessageEventModel::ShowSectionRole) {
|
||||
const auto day = mapToSource(index).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
|
||||
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
|
||||
return day != previousEventDay;
|
||||
}
|
||||
|
||||
return sourceModel()->data(mapToSource(index), role);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <qobjectdefs.h>
|
||||
|
||||
#include "models/messagefiltermodel.h"
|
||||
|
||||
class MessageFilterModel;
|
||||
|
||||
/**
|
||||
* @class MediaMessageFilterModel
|
||||
*
|
||||
@@ -18,6 +21,12 @@ class MediaMessageFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum MediaType {
|
||||
Image = 0,
|
||||
Video,
|
||||
};
|
||||
Q_ENUM(MediaType)
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
@@ -31,7 +40,7 @@ public:
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit MediaMessageFilterModel(QObject *parent = nullptr);
|
||||
explicit MediaMessageFilterModel(QObject *parent = nullptr, MessageFilterModel *sourceMediaModel = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter to show only image and video messages.
|
||||
@@ -43,7 +52,7 @@ public:
|
||||
*
|
||||
* @sa QSortFilterProxyModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
|
||||
@@ -8,25 +8,19 @@
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/csapi/rooms.h>
|
||||
#include <Quotient/events/reactionevent.h>
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roomavatarevent.h>
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
#include <Quotient/events/simplestateevents.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#include "events/pollevent.h"
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGuiApplication>
|
||||
#include <QMovie>
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -37,6 +31,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[PlainText] = "plainText";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[TimeStringRole] = "timeString";
|
||||
roles[SectionRole] = "section";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[ContentRole] = "content";
|
||||
@@ -48,8 +43,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[IsReplyRole] = "isReply";
|
||||
roles[ReplyAuthor] = "replyAuthor";
|
||||
roles[ReplyRole] = "reply";
|
||||
roles[ReplyIdRole] = "replyId";
|
||||
roles[ReplyDelegateTypeRole] = "replyDelegateType";
|
||||
roles[ReplyDisplayRole] = "replyDisplay";
|
||||
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
@@ -60,10 +56,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[ShowReactionsRole] = "showReactions";
|
||||
roles[SourceRole] = "jsonSource";
|
||||
roles[MimeTypeRole] = "mimeType";
|
||||
roles[AuthorIdRole] = "authorId";
|
||||
roles[VerifiedRole] = "verified";
|
||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||
roles[IsRedactedRole] = "isRedacted";
|
||||
roles[GenericDisplayRole] = "genericDisplay";
|
||||
@@ -97,7 +91,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (m_currentRoom) {
|
||||
m_currentRoom->disconnect(this);
|
||||
m_linkPreviewers.clear();
|
||||
qDeleteAll(m_reactionModels);
|
||||
m_reactionModels.clear();
|
||||
}
|
||||
|
||||
@@ -107,10 +100,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
room->setDisplayed();
|
||||
|
||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
|
||||
createLinkPreviewerForEvent(e);
|
||||
createReactionModelForEvent(e);
|
||||
}
|
||||
createEventObjects(&*event->viewAs<RoomMessageEvent>());
|
||||
}
|
||||
|
||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||
@@ -123,16 +113,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
if (row == -1) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
|
||||
});
|
||||
|
||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &&event : events) {
|
||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message != nullptr) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
|
||||
createEventObjects(message);
|
||||
|
||||
if (message != nullptr) {
|
||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||
QString planBody = message->plainBody();
|
||||
// snowflake
|
||||
@@ -169,10 +159,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &event : events) {
|
||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||
if (message) {
|
||||
createLinkPreviewerForEvent(message);
|
||||
createReactionModelForEvent(message);
|
||||
}
|
||||
createEventObjects(message);
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
rowBelowInserted = rowCount() - 1; // See #312
|
||||
@@ -241,7 +228,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
||||
if (eventIt != m_currentRoom->historyEdge()) {
|
||||
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
|
||||
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
||||
});
|
||||
@@ -340,7 +327,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
|
||||
return -1;
|
||||
}
|
||||
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
||||
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
|
||||
if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) {
|
||||
row++;
|
||||
}
|
||||
}
|
||||
@@ -458,6 +445,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
||||
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
||||
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(m_currentRoom);
|
||||
eventHandler.setEvent(&evt);
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (evt.isRedacted()) {
|
||||
auto reason = evt.redactedBecause()->reason();
|
||||
@@ -465,19 +456,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
||||
}
|
||||
|
||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
||||
return eventHandler.getRichBody();
|
||||
}
|
||||
|
||||
if (role == GenericDisplayRole) {
|
||||
if (evt.isRedacted()) {
|
||||
return i18n("<i>[This message was deleted]</i>");
|
||||
}
|
||||
|
||||
return m_currentRoom->eventToGenericString(evt);
|
||||
return eventHandler.getGenericBody();
|
||||
}
|
||||
|
||||
if (role == PlainText) {
|
||||
return m_currentRoom->eventToString(evt);
|
||||
return eventHandler.getPlainBody();
|
||||
}
|
||||
|
||||
if (role == SourceRole) {
|
||||
@@ -485,54 +472,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == DelegateTypeRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
return DelegateType::Emote;
|
||||
case MessageEventType::Notice:
|
||||
return DelegateType::Notice;
|
||||
case MessageEventType::Image:
|
||||
return DelegateType::Image;
|
||||
case MessageEventType::Audio:
|
||||
return DelegateType::Audio;
|
||||
case MessageEventType::Video:
|
||||
return DelegateType::Video;
|
||||
case MessageEventType::Location:
|
||||
return DelegateType::Location;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (e->hasFileContent()) {
|
||||
return DelegateType::File;
|
||||
}
|
||||
|
||||
return DelegateType::Message;
|
||||
}
|
||||
if (is<const StickerEvent>(evt)) {
|
||||
return DelegateType::Sticker;
|
||||
}
|
||||
if (evt.isStateEvent()) {
|
||||
if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||
return DelegateType::LiveLocation;
|
||||
}
|
||||
return DelegateType::State;
|
||||
}
|
||||
if (is<const EncryptedEvent>(evt)) {
|
||||
return DelegateType::Encrypted;
|
||||
}
|
||||
if (is<PollStartEvent>(evt)) {
|
||||
if (evt.isRedacted()) {
|
||||
return DelegateType::Message;
|
||||
}
|
||||
return DelegateType::Poll;
|
||||
}
|
||||
|
||||
return DelegateType::Other;
|
||||
return eventHandler.getDelegateType();
|
||||
}
|
||||
|
||||
if (role == AuthorRole) {
|
||||
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
||||
return m_currentRoom->getUser(author);
|
||||
return eventHandler.getAuthor(isPending);
|
||||
}
|
||||
|
||||
if (role == ContentRole) {
|
||||
@@ -558,21 +502,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == HighlightRole) {
|
||||
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
|
||||
}
|
||||
|
||||
if (role == MimeTypeRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (!e || !e->hasFileContent()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return e->content()->fileInfo()->mimeType.name();
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
||||
return e->image().mimeType.name();
|
||||
}
|
||||
return eventHandler.isHighlighted();
|
||||
}
|
||||
|
||||
if (role == SpecialMarksRole) {
|
||||
@@ -585,46 +515,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return pendingIt->deliveryStatus();
|
||||
}
|
||||
|
||||
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
|
||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
return EventStatus::Hidden;
|
||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||
return EventStatus::Hidden;
|
||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// isReplacement?
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt))
|
||||
if (!e->replacedEvent().isEmpty())
|
||||
return EventStatus::Hidden;
|
||||
|
||||
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_currentRoom->connection()->isIgnored(m_currentRoom->user(evt.senderId()))) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
// hide ending live location beacons
|
||||
if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) {
|
||||
if (eventHandler.isHidden()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
@@ -632,7 +523,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == EventIdRole) {
|
||||
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
|
||||
return eventHandler.getId();
|
||||
}
|
||||
|
||||
if (role == ProgressInfoRole) {
|
||||
@@ -646,9 +537,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
}
|
||||
|
||||
if (role == TimeRole || role == SectionRole) {
|
||||
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
|
||||
return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat);
|
||||
if (role == TimeRole) {
|
||||
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||
return eventHandler.getTime(isPending, lastUpdated);
|
||||
}
|
||||
|
||||
if (role == TimeStringRole) {
|
||||
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
|
||||
}
|
||||
|
||||
if (role == SectionRole) {
|
||||
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
|
||||
}
|
||||
|
||||
if (role == ShowLinkPreviewRole) {
|
||||
@@ -657,85 +558,38 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (role == LinkPreviewRole) {
|
||||
if (m_linkPreviewers.contains(evt.id())) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]);
|
||||
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()].data());
|
||||
} else {
|
||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||
}
|
||||
}
|
||||
|
||||
if (role == MediaInfoRole) {
|
||||
return getMediaInfoForEvent(evt);
|
||||
return eventHandler.getMediaInfo();
|
||||
}
|
||||
|
||||
if (role == IsReplyRole) {
|
||||
return !evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
||||
return eventHandler.hasReply();
|
||||
}
|
||||
|
||||
if (role == ReplyIdRole) {
|
||||
return evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||
return eventHandler.getReplyId();
|
||||
}
|
||||
|
||||
if (role == ReplyDelegateTypeRole) {
|
||||
return eventHandler.getReplyDelegateType();
|
||||
}
|
||||
|
||||
if (role == ReplyAuthor) {
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
return eventHandler.getReplyAuthor();
|
||||
}
|
||||
|
||||
if (replyPtr) {
|
||||
auto replyUser = m_currentRoom->user(replyPtr->senderId());
|
||||
return m_currentRoom->getUser(replyUser);
|
||||
} else {
|
||||
return m_currentRoom->getUser(nullptr);
|
||||
}
|
||||
if (role == ReplyDisplayRole) {
|
||||
return eventHandler.getReplyRichBody();
|
||||
}
|
||||
|
||||
if (role == ReplyMediaInfoRole) {
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
return getMediaInfoForEvent(*replyPtr);
|
||||
}
|
||||
|
||||
if (role == ReplyRole) {
|
||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
DelegateType type;
|
||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
type = DelegateType::Emote;
|
||||
break;
|
||||
case MessageEventType::Notice:
|
||||
type = DelegateType::Notice;
|
||||
break;
|
||||
case MessageEventType::Image:
|
||||
type = DelegateType::Image;
|
||||
break;
|
||||
case MessageEventType::Audio:
|
||||
type = DelegateType::Audio;
|
||||
break;
|
||||
case MessageEventType::Video:
|
||||
type = DelegateType::Video;
|
||||
break;
|
||||
default:
|
||||
if (e->hasFileContent()) {
|
||||
type = DelegateType::File;
|
||||
break;
|
||||
}
|
||||
type = DelegateType::Message;
|
||||
}
|
||||
|
||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
||||
type = DelegateType::Sticker;
|
||||
} else {
|
||||
type = DelegateType::Other;
|
||||
}
|
||||
|
||||
return QVariantMap{
|
||||
{"display"_ls, m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
|
||||
{"type"_ls, type},
|
||||
};
|
||||
return eventHandler.getReplyMediaInfo();
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
@@ -745,7 +599,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
|
||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == DelegateType::State
|
||||
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|
||||
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
|
||||
}
|
||||
@@ -771,87 +625,36 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == LatitudeRole) {
|
||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
||||
return latitude.toFloat();
|
||||
return eventHandler.getLatitude();
|
||||
}
|
||||
|
||||
if (role == LongitudeRole) {
|
||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
||||
return latitude.toFloat();
|
||||
return eventHandler.getLongitude();
|
||||
}
|
||||
|
||||
if (role == AssetRole) {
|
||||
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
||||
if (assetType.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return assetType;
|
||||
return eventHandler.getLocationAssetType();
|
||||
}
|
||||
|
||||
if (role == ReadMarkersRole) {
|
||||
auto userIds_temp = room()->userIdsAtEvent(evt.id());
|
||||
userIds_temp.remove(m_currentRoom->localUser()->id());
|
||||
|
||||
auto userIds = userIds_temp.values();
|
||||
if (userIds.count() > 5) {
|
||||
userIds = userIds.mid(0, 5);
|
||||
}
|
||||
|
||||
QVariantList users;
|
||||
users.reserve(userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_currentRoom->user(userId);
|
||||
users += m_currentRoom->getUser(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
return eventHandler.getReadMarkers();
|
||||
}
|
||||
|
||||
if (role == ExcessReadMarkersRole) {
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
|
||||
if (userIds.count() > 5) {
|
||||
return QStringLiteral("+ %1").arg(userIds.count() - 5);
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
return eventHandler.getNumberExcessReadMarkers();
|
||||
}
|
||||
|
||||
if (role == ReadMarkersStringRole) {
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
|
||||
/**
|
||||
* The string ends up in the form
|
||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||
*/
|
||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
auto user = m_currentRoom->user(userId);
|
||||
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
|
||||
}
|
||||
readMarkersString.chop(2);
|
||||
return readMarkersString;
|
||||
return eventHandler.getReadMarkersString();
|
||||
}
|
||||
|
||||
if (role == ShowReadMarkersRole) {
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
return userIds.size() > 0;
|
||||
return eventHandler.hasReadMarkers();
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
if (m_reactionModels.contains(evt.id())) {
|
||||
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
|
||||
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()].data());
|
||||
} else {
|
||||
return QVariantList();
|
||||
}
|
||||
@@ -874,22 +677,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == DisplayNameForInitialsRole) {
|
||||
auto user = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
||||
return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id()));
|
||||
}
|
||||
|
||||
if (role == AuthorDisplayNameRole) {
|
||||
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"_ls]["displayname"_ls].isNull() && evt.stateKey() == evt.senderId()) {
|
||||
auto previousDisplayName = evt.unsignedJson()["prev_content"_ls]["displayname"_ls].toString().toHtmlEscaped();
|
||||
if (previousDisplayName.isEmpty()) {
|
||||
previousDisplayName = evt.senderId();
|
||||
}
|
||||
return previousDisplayName;
|
||||
} else {
|
||||
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
||||
return m_currentRoom->htmlSafeMemberName(author->id());
|
||||
}
|
||||
return eventHandler.getAuthorDisplayName(isPending);
|
||||
}
|
||||
|
||||
if (role == IsRedactedRole) {
|
||||
@@ -913,195 +702,27 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
|
||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||
}
|
||||
|
||||
QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
QString eventId = event.id();
|
||||
|
||||
// Get the file info for the event.
|
||||
const EventContent::FileInfo *fileInfo;
|
||||
if (event.is<RoomMessageEvent>()) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event);
|
||||
if (!roomMessageEvent->hasFileContent()) {
|
||||
return {};
|
||||
}
|
||||
fileInfo = roomMessageEvent->content()->fileInfo();
|
||||
} else if (event.is<StickerEvent>()) {
|
||||
auto stickerEvent = eventCast<const StickerEvent>(&event);
|
||||
fileInfo = &stickerEvent->image();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
|
||||
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
||||
}
|
||||
|
||||
QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
||||
{
|
||||
QVariantMap mediaInfo;
|
||||
|
||||
// Get the mxc URL for the media.
|
||||
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url());
|
||||
|
||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
||||
mediaInfo["source"_ls] = source;
|
||||
} else {
|
||||
mediaInfo["source"_ls] = QUrl();
|
||||
}
|
||||
}
|
||||
|
||||
auto mimeType = fileInfo->mimeType;
|
||||
// Add the MIME type for the media if available.
|
||||
mediaInfo["mimeType"_ls] = mimeType.name();
|
||||
|
||||
// Add the MIME type icon if available.
|
||||
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
|
||||
|
||||
// Add media size if available.
|
||||
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
||||
|
||||
// Add parameter depending on media type.
|
||||
if (mimeType.name().contains(QStringLiteral("image"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
|
||||
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
|
||||
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("video"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
|
||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
|
||||
if (!isThumbnail) {
|
||||
QVariantMap tempInfo;
|
||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||
tempInfo = thumbnailInfo;
|
||||
} else {
|
||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||
if (blurhash.isEmpty()) {
|
||||
tempInfo["source"_ls] = QUrl();
|
||||
} else {
|
||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||
}
|
||||
}
|
||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mimeType.name().contains(QStringLiteral("audio"))) {
|
||||
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
|
||||
mediaInfo["duration"_ls] = castInfo->duration;
|
||||
}
|
||||
}
|
||||
|
||||
return mediaInfo;
|
||||
}
|
||||
|
||||
void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (m_linkPreviewers.contains(event->id())) {
|
||||
return;
|
||||
} else {
|
||||
QString text;
|
||||
if (event->hasTextContent()) {
|
||||
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
||||
if (textContent) {
|
||||
text = textContent->body;
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
} else {
|
||||
text = event->plainBody();
|
||||
}
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(text);
|
||||
|
||||
QList<QUrl> links = textHandler.getLinkPreviews();
|
||||
if (links.size() > 0) {
|
||||
m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
|
||||
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto eventId = event->id();
|
||||
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::AnnotationType);
|
||||
if (annotations.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
QMap<QString, QList<Quotient::User *>> reactions = {};
|
||||
for (const auto &a : annotations) {
|
||||
if (a->isRedacted()) { // Just in case?
|
||||
continue;
|
||||
}
|
||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||
reactions[e->key()].append(m_currentRoom->user(e->senderId()));
|
||||
}
|
||||
}
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(m_currentRoom);
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
if (reactions.isEmpty()) {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QList<ReactionModel::Reaction> res;
|
||||
auto i = reactions.constBegin();
|
||||
while (i != reactions.constEnd()) {
|
||||
QVariantList authors;
|
||||
for (const auto &author : i.value()) {
|
||||
authors.append(m_currentRoom->getUser(author));
|
||||
}
|
||||
|
||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
m_reactionModels[eventId]->setReactions(res);
|
||||
} else if (res.size() > 0) {
|
||||
m_reactionModels[eventId] = new ReactionModel(this, res, m_currentRoom->localUser());
|
||||
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
|
||||
m_linkPreviewers[eventId] = linkPreviewer;
|
||||
} else {
|
||||
if (m_reactionModels.contains(eventId)) {
|
||||
delete m_reactionModels[eventId];
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
m_linkPreviewers.remove(eventId);
|
||||
}
|
||||
if (auto reactionModel = eventHandler.getReactions()) {
|
||||
m_reactionModels[eventId] = reactionModel;
|
||||
} else {
|
||||
m_reactionModels.remove(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,32 +32,6 @@ class MessageEventModel : public QAbstractListModel
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The type of delegate that is needed for the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
enum DelegateType {
|
||||
Emote, /**< A message that begins with /me. */
|
||||
Notice, /**< A notice event. */
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
Video, /**< A message that is a video. */
|
||||
File, /**< A message that is a file. */
|
||||
Message, /**< A text message. */
|
||||
Sticker, /**< A message that is a sticker. */
|
||||
State, /**< A state event in the room. */
|
||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||
ReadMarker, /**< The local user read marker. */
|
||||
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). */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(DelegateType)
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
@@ -65,7 +39,8 @@ public:
|
||||
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||
PlainText, /**< Plain text representation of the message. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
TimeRole, /**< The timestamp for when the event was sent. */
|
||||
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ContentRole, /**< The full message content. */
|
||||
@@ -78,13 +53,13 @@ public:
|
||||
LinkPreviewRole, /**< The link preview details. */
|
||||
|
||||
MediaInfoRole, /**< The media info for the event. */
|
||||
MimeTypeRole, /**< The mime type of the message's file or media. */
|
||||
|
||||
IsReplyRole, /**< Is the message a reply to another event. */
|
||||
ReplyAuthor, /**< The author of the event that was replied to. */
|
||||
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
|
||||
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
|
||||
ReplyDisplayRole, /**< The body of the message that was replied to. */
|
||||
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
||||
ReplyRole, /**< The content data of the message that was replied to. */
|
||||
|
||||
ShowAuthorRole, /**< Whether the author's name should be shown. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
@@ -100,7 +75,6 @@ public:
|
||||
AuthorIdRole, /**< Matrix ID of the message author. */
|
||||
|
||||
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
||||
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
|
||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||
@@ -154,8 +128,8 @@ private:
|
||||
bool movingEvent = false;
|
||||
KFormat m_format;
|
||||
|
||||
QMap<QString, LinkPreviewer *> m_linkPreviewers;
|
||||
QMap<QString, ReactionModel *> m_reactionModels;
|
||||
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
||||
@@ -168,10 +142,7 @@ private:
|
||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||
void moveReadMarker(const QString &toEventId);
|
||||
|
||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
|
||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
|
||||
void createReactionModelForEvent(const Quotient::RoomMessageEvent *event);
|
||||
void createEventObjects(const Quotient::RoomMessageEvent *event);
|
||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||
bool m_initialized = false;
|
||||
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent)
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceMessageModel);
|
||||
setSourceModel(sourceMessageModel);
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||
invalidateFilter();
|
||||
});
|
||||
@@ -47,17 +51,18 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
||||
|
||||
// Don't show events with an unknown type.
|
||||
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
|
||||
if (eventType == MessageEventModel::Other) {
|
||||
if (eventType == DelegateType::Other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show state events that are not the first in a consecutive group on the
|
||||
// same day as they will be grouped as a single delegate.
|
||||
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
|
||||
const bool previousEventIsState =
|
||||
sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == MessageEventModel::DelegateType::State;
|
||||
const bool previousEventIsState = notLastRow
|
||||
? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State
|
||||
: false;
|
||||
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
|
||||
if (eventType == MessageEventModel::State && notLastRow && previousEventIsState && !newDay) {
|
||||
if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -80,7 +85,7 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
QHash<int, QByteArray> MessageFilterModel::roleNames() const
|
||||
{
|
||||
auto roles = sourceModel()->roleNames();
|
||||
auto roles = sourceModel() ? sourceModel()->roleNames() : QHash<int, QByteArray>();
|
||||
roles[AggregateDisplayRole] = "aggregateDisplay";
|
||||
roles[StateEventsRole] = "stateEvents";
|
||||
roles[AuthorListRole] = "authorList";
|
||||
@@ -99,8 +104,7 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
|
||||
uniqueAuthors.append(nextAuthor);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
@@ -158,8 +162,7 @@ QVariantList MessageFilterModel::stateEventsList(int sourceRow) const
|
||||
};
|
||||
stateEvents.append(nextState);
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
@@ -177,8 +180,7 @@ QVariantList MessageFilterModel::authorList(int sourceRow) const
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
@@ -200,8 +202,7 @@ QString MessageFilterModel::excessAuthors(int row) const
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (i > 0
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
)) {
|
||||
break;
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
explicit MessageFilterModel(QObject *parent = nullptr);
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function to remove hidden messages.
|
||||
|
||||
@@ -171,7 +171,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
return {};
|
||||
}
|
||||
auto room = rooms.at(index.row());
|
||||
if (role == NameRole) {
|
||||
if (role == DisplayNameRole) {
|
||||
auto displayName = room.name;
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
@@ -188,18 +188,17 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
return room.roomId;
|
||||
}
|
||||
if (role == AvatarRole) {
|
||||
if (role == AvatarUrlRole) {
|
||||
auto avatarUrl = room.avatarUrl;
|
||||
|
||||
if (avatarUrl.isEmpty()) {
|
||||
return {};
|
||||
if (avatarUrl.isEmpty() || !m_connection) {
|
||||
return QUrl();
|
||||
}
|
||||
return avatarUrl.url().remove(0, 6);
|
||||
return m_connection->makeMediaUrl(avatarUrl);
|
||||
}
|
||||
if (role == TopicRole) {
|
||||
return room.topic;
|
||||
}
|
||||
if (role == RoomIDRole) {
|
||||
if (role == RoomIdRole) {
|
||||
return room.roomId;
|
||||
}
|
||||
if (role == AliasRole) {
|
||||
@@ -232,10 +231,10 @@ QHash<int, QByteArray> PublicRoomListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[NameRole] = "name";
|
||||
roles[AvatarRole] = "avatar";
|
||||
roles[DisplayNameRole] = "displayName";
|
||||
roles[AvatarUrlRole] = "avatarUrl";
|
||||
roles[TopicRole] = "topic";
|
||||
roles[RoomIDRole] = "roomID";
|
||||
roles[RoomIdRole] = "roomId";
|
||||
roles[MemberCountRole] = "memberCount";
|
||||
roles[AllowGuestsRole] = "allowGuests";
|
||||
roles[WorldReadableRole] = "worldReadable";
|
||||
|
||||
@@ -58,10 +58,10 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
DisplayNameRole = Qt::DisplayRole + 1, /**< The name of the room. */
|
||||
AvatarUrlRole, /**< The source URL for the room's avatar. */
|
||||
TopicRole, /**< The room topic. */
|
||||
RoomIDRole, /**< The room matrix ID. */
|
||||
RoomIdRole, /**< The room matrix ID. */
|
||||
AliasRole, /**< The room canonical alias. */
|
||||
MemberCountRole, /**< The number of members in the room. */
|
||||
AllowGuestsRole, /**< Whether the room allows guest users. */
|
||||
@@ -76,7 +76,7 @@ public:
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = DisplayNameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
|
||||
@@ -23,6 +23,9 @@ static const QHash<QString, QString> defaultRuleNames = {
|
||||
{QStringLiteral(".m.rule.encrypted"), QStringLiteral("Messages in encrypted group chats")},
|
||||
{QStringLiteral(".m.rule.tombstone"), QStringLiteral("Room upgrade messages")},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), QStringLiteral("Messages containing my display name")},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), QStringLiteral("Messages which mention my Matrix user ID.")},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), QStringLiteral("Messages which mention a room.")},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), QStringLiteral("Messages containing the local part of my Matrix ID.")},
|
||||
{QStringLiteral(".m.rule.roomnotif"), QStringLiteral("Whole room (@room) notifications")},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), QStringLiteral("Invites to a room")},
|
||||
{QStringLiteral(".m.rule.call"), QStringLiteral("Call invitation")},
|
||||
@@ -37,6 +40,9 @@ static const QHash<QString, PushNotificationSection::Section> defaultSections =
|
||||
{QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room},
|
||||
{QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_user_mention"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.is_room_mention"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.contains_user_name"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions},
|
||||
{QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites},
|
||||
{QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added.
|
||||
@@ -140,6 +146,9 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
|
||||
if (defaultSections.contains(ruleId)) {
|
||||
return defaultSections.value(ruleId);
|
||||
} else {
|
||||
if (rule.ruleId.startsWith(u'.')) {
|
||||
return PushNotificationSection::Unknown;
|
||||
}
|
||||
/**
|
||||
* If the rule name resolves to a matrix id for a room that the user is part
|
||||
* of it shouldn't appear in the global list as it's overriding the global
|
||||
|
||||
@@ -83,12 +83,16 @@ public:
|
||||
Keywords, /**< Global Keyword push rules. */
|
||||
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
|
||||
Invites, /**< Push rules relating to invites. */
|
||||
Unknown, /**< New default push rules that have not been added to the model yet. */
|
||||
/**
|
||||
* @brief Push rules that should never be shown.
|
||||
*
|
||||
* There are numerous rules that get set that shouldn't be shown in the general
|
||||
* list e.g. The array of rules used to override global settings in individual
|
||||
* rooms.
|
||||
*
|
||||
* This is specifically different to unknown which are just new default push
|
||||
* rule that haven't been added to the model yet.
|
||||
*/
|
||||
Undefined,
|
||||
};
|
||||
|
||||
@@ -36,13 +36,13 @@ RoomListModel::RoomListModel(QObject *parent)
|
||||
m_categoryVisibility[collapsedSection] = false;
|
||||
}
|
||||
|
||||
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
|
||||
connect(this, &RoomListModel::highlightCountChanged, this, [this]() {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop"_ls;
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(m_notificationCount, 9999);
|
||||
const qint64 counterSlice = std::min(m_highlightCount, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
|
||||
if (counterSlice > 0) {
|
||||
@@ -59,7 +59,7 @@ RoomListModel::RoomListModel(QObject *parent)
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
#endif // Q_OS_ANDROID
|
||||
#else
|
||||
qGuiApp->setBadgeNumber(m_notificationCount);
|
||||
qGuiApp->setBadgeNumber(m_highlightCount);
|
||||
#endif // QT_VERSION_CHECK(6, 6, 0)
|
||||
});
|
||||
connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
|
||||
@@ -144,7 +144,6 @@ void RoomListModel::doAddRoom(Room *r)
|
||||
m_rooms.append(room);
|
||||
connectRoomSignals(room);
|
||||
Q_EMIT roomAdded(room);
|
||||
Q_EMIT Controller::instance().roomAdded(room);
|
||||
} else {
|
||||
qCritical() << "Attempt to add nullptr to the room list";
|
||||
Q_ASSERT(false);
|
||||
@@ -162,6 +161,9 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
connect(room, &Room::notificationCountChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::highlightCountChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::avatarChanged, this, [this, room] {
|
||||
refresh(room, {AvatarRole});
|
||||
});
|
||||
@@ -178,6 +180,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
});
|
||||
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
connect(room, &Room::highlightCountChanged, this, &RoomListModel::refreshHighlightCount);
|
||||
}
|
||||
|
||||
int RoomListModel::notificationCount() const
|
||||
@@ -185,6 +188,11 @@ int RoomListModel::notificationCount() const
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
int RoomListModel::highlightCount() const
|
||||
{
|
||||
return m_highlightCount;
|
||||
}
|
||||
|
||||
void RoomListModel::refreshNotificationCount()
|
||||
{
|
||||
int count = 0;
|
||||
@@ -198,6 +206,19 @@ void RoomListModel::refreshNotificationCount()
|
||||
Q_EMIT notificationCountChanged();
|
||||
}
|
||||
|
||||
void RoomListModel::refreshHighlightCount()
|
||||
{
|
||||
int count = 0;
|
||||
for (auto room : std::as_const(m_rooms)) {
|
||||
count += room->highlightCount();
|
||||
}
|
||||
if (m_highlightCount == count) {
|
||||
return;
|
||||
}
|
||||
m_highlightCount = count;
|
||||
Q_EMIT highlightCountChanged();
|
||||
}
|
||||
|
||||
void RoomListModel::updateRoom(Room *room, Room *prev)
|
||||
{
|
||||
// There are two cases when this method is called:
|
||||
|
||||
@@ -85,6 +85,7 @@ public:
|
||||
void setConnection(Quotient::Connection *connection);
|
||||
|
||||
[[nodiscard]] int notificationCount() const;
|
||||
[[nodiscard]] int highlightCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
@@ -155,6 +156,7 @@ private Q_SLOTS:
|
||||
void deleteRoom(Quotient::Room *room);
|
||||
void refresh(NeoChatRoom *room, const QVector<int> &roles = {});
|
||||
void refreshNotificationCount();
|
||||
void refreshHighlightCount();
|
||||
|
||||
private:
|
||||
Quotient::Connection *m_connection = nullptr;
|
||||
@@ -163,6 +165,7 @@ private:
|
||||
QMap<int, bool> m_categoryVisibility;
|
||||
|
||||
int m_notificationCount = 0;
|
||||
int m_highlightCount = 0;
|
||||
QString m_activeSpaceId;
|
||||
|
||||
void connectRoomSignals(NeoChatRoom *room);
|
||||
@@ -170,6 +173,7 @@ private:
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void notificationCountChanged();
|
||||
void highlightCountChanged();
|
||||
|
||||
void roomAdded(NeoChatRoom *_t1);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "searchmodel.h"
|
||||
|
||||
#include "eventhandler.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
@@ -91,90 +92,55 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
auto row = index.row();
|
||||
const auto &event = *m_result->results[row].result;
|
||||
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(m_room);
|
||||
eventHandler.setEvent(&event);
|
||||
|
||||
switch (role) {
|
||||
case DisplayRole:
|
||||
return m_room->eventToString(*m_result->results[row].result);
|
||||
return eventHandler.getRichBody();
|
||||
case ShowAuthorRole:
|
||||
return true;
|
||||
case AuthorRole:
|
||||
return m_room->getUser(event.senderId());
|
||||
return eventHandler.getAuthor();
|
||||
case ShowSectionRole:
|
||||
if (row == 0) {
|
||||
return true;
|
||||
}
|
||||
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
|
||||
case SectionRole:
|
||||
return renderDate(event.originTimestamp());
|
||||
return eventHandler.getTimeString(true);
|
||||
case TimeRole:
|
||||
return event.originTimestamp();
|
||||
return eventHandler.getTime();
|
||||
case TimeStringRole:
|
||||
return eventHandler.getTimeString(false);
|
||||
case ShowReactionsRole:
|
||||
return false;
|
||||
case ShowReadMarkersRole:
|
||||
return false;
|
||||
case IsReplyRole:
|
||||
return eventHandler.hasReply();
|
||||
case ReplyIdRole:
|
||||
return eventHandler.hasReply();
|
||||
case ReplyAuthorRole:
|
||||
if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
|
||||
return m_room->getUser(m_room->user(replyPtr->senderId()));
|
||||
} else {
|
||||
return m_room->getUser(nullptr);
|
||||
}
|
||||
case ReplyRole:
|
||||
if (role == ReplyRole) {
|
||||
auto replyPtr = m_room->getReplyForEvent(event);
|
||||
if (!replyPtr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
MessageEventModel::DelegateType type;
|
||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
type = MessageEventModel::DelegateType::Emote;
|
||||
break;
|
||||
case MessageEventType::Notice:
|
||||
type = MessageEventModel::DelegateType::Notice;
|
||||
break;
|
||||
case MessageEventType::Image:
|
||||
type = MessageEventModel::DelegateType::Image;
|
||||
break;
|
||||
case MessageEventType::Audio:
|
||||
type = MessageEventModel::DelegateType::Audio;
|
||||
break;
|
||||
case MessageEventType::Video:
|
||||
type = MessageEventModel::DelegateType::Video;
|
||||
break;
|
||||
default:
|
||||
if (e->hasFileContent()) {
|
||||
type = MessageEventModel::DelegateType::File;
|
||||
break;
|
||||
}
|
||||
type = MessageEventModel::DelegateType::Message;
|
||||
}
|
||||
|
||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
||||
type = MessageEventModel::DelegateType::Sticker;
|
||||
} else {
|
||||
type = MessageEventModel::DelegateType::Other;
|
||||
}
|
||||
|
||||
return QVariantMap{
|
||||
{"display"_ls, m_room->eventToString(*replyPtr, Qt::RichText)},
|
||||
{"type"_ls, type},
|
||||
};
|
||||
}
|
||||
return eventHandler.getReplyAuthor();
|
||||
case ReplyDelegateTypeRole:
|
||||
return eventHandler.getReplyDelegateType();
|
||||
case ReplyDisplayRole:
|
||||
return eventHandler.getReplyRichBody();
|
||||
case ReplyMediaInfoRole:
|
||||
return eventHandler.getReplyMediaInfo();
|
||||
case IsPendingRole:
|
||||
return false;
|
||||
case ShowLinkPreviewRole:
|
||||
return false;
|
||||
case IsReplyRole:
|
||||
return !event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
||||
case HighlightRole:
|
||||
return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
|
||||
return eventHandler.isHighlighted();
|
||||
case EventIdRole:
|
||||
return event.id();
|
||||
case ReplyIdRole:
|
||||
return event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||
return eventHandler.getId();
|
||||
}
|
||||
return MessageEventModel::DelegateType::Message;
|
||||
return DelegateType::Message;
|
||||
}
|
||||
|
||||
int SearchModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -195,6 +161,7 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{ShowSectionRole, "showSection"},
|
||||
{SectionRole, "section"},
|
||||
{TimeRole, "time"},
|
||||
{TimeStringRole, "timeString"},
|
||||
{ShowAuthorRole, "showAuthor"},
|
||||
{EventIdRole, "eventId"},
|
||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||
@@ -202,21 +169,22 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{ReadMarkersString, "readMarkersString"},
|
||||
{PlainTextRole, "plainText"},
|
||||
{VerifiedRole, "verified"},
|
||||
{ReplyAuthorRole, "replyAuthor"},
|
||||
{ProgressInfoRole, "progressInfo"},
|
||||
{IsReplyRole, "isReply"},
|
||||
{ShowReactionsRole, "showReactions"},
|
||||
{ReplyRole, "reply"},
|
||||
{ReactionRole, "reaction"},
|
||||
{IsReplyRole, "isReply"},
|
||||
{ReplyAuthorRole, "replyAuthor"},
|
||||
{ReplyIdRole, "replyId"},
|
||||
{ReplyDelegateTypeRole, "replyDelegateType"},
|
||||
{ReplyDisplayRole, "replyDisplay"},
|
||||
{ReplyMediaInfoRole, "replyMediaInfo"},
|
||||
{ReactionRole, "reaction"},
|
||||
{ReadMarkersRole, "readMarkers"},
|
||||
{IsPendingRole, "isPending"},
|
||||
{ShowReadMarkersRole, "showReadMarkers"},
|
||||
{ReplyIdRole, "replyId"},
|
||||
{MimeTypeRole, "mimeType"},
|
||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
||||
{LinkPreviewRole, "linkPreview"},
|
||||
{SourceRole, "source"},
|
||||
{SourceRole, "jsonSource"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,30 +211,10 @@ void SearchModel::setRoom(NeoChatRoom *room)
|
||||
return;
|
||||
}
|
||||
auto row = it - results.begin();
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO deduplicate with messageeventmodel
|
||||
QString renderDate(const QDateTime ×tamp)
|
||||
{
|
||||
auto date = timestamp.toLocalTime().date();
|
||||
if (date == QDate::currentDate()) {
|
||||
return i18n("Today");
|
||||
}
|
||||
if (date == QDate::currentDate().addDays(-1)) {
|
||||
return i18n("Yesterday");
|
||||
}
|
||||
if (date == QDate::currentDate().addDays(-2)) {
|
||||
return i18n("The day before yesterday");
|
||||
}
|
||||
if (date > QDate::currentDate().addDays(-7)) {
|
||||
return date.toString("dddd"_ls);
|
||||
}
|
||||
|
||||
return QLocale::system().toString(date, QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
bool SearchModel::searching() const
|
||||
{
|
||||
return m_searching;
|
||||
|
||||
@@ -61,23 +61,25 @@ public:
|
||||
ShowSectionRole,
|
||||
SectionRole,
|
||||
TimeRole,
|
||||
TimeStringRole,
|
||||
EventIdRole,
|
||||
ExcessReadMarkersRole,
|
||||
HighlightRole,
|
||||
ReadMarkersString,
|
||||
PlainTextRole,
|
||||
VerifiedRole,
|
||||
ReplyAuthorRole,
|
||||
ProgressInfoRole,
|
||||
IsReplyRole,
|
||||
ShowReactionsRole,
|
||||
ReplyRole,
|
||||
ReactionRole,
|
||||
IsReplyRole,
|
||||
ReplyAuthorRole,
|
||||
ReplyIdRole,
|
||||
ReplyDelegateTypeRole,
|
||||
ReplyDisplayRole,
|
||||
ReplyMediaInfoRole,
|
||||
ReactionRole,
|
||||
ReadMarkersRole,
|
||||
IsPendingRole,
|
||||
ShowReadMarkersRole,
|
||||
ReplyIdRole,
|
||||
MimeTypeRole,
|
||||
ShowLinkPreviewRole,
|
||||
LinkPreviewRole,
|
||||
@@ -139,5 +141,3 @@ private:
|
||||
Quotient::SearchJob *m_job = nullptr;
|
||||
bool m_searching = false;
|
||||
};
|
||||
|
||||
QString renderDate(const QDateTime &dateTime);
|
||||
|
||||
@@ -109,7 +109,7 @@ void StickerModel::setRoom(NeoChatRoom *room)
|
||||
void StickerModel::postSticker(int index)
|
||||
{
|
||||
const auto &image = m_images[index];
|
||||
const auto &body = image.body ? *image.body : QString();
|
||||
const auto &body = image.body ? *image.body : image.shortcode;
|
||||
QJsonObject infoJson;
|
||||
if (image.info) {
|
||||
infoJson["w"_ls] = image.info->imageSize.width();
|
||||
|
||||
@@ -107,6 +107,14 @@
|
||||
<label>Show Fancy Effects</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="MediaMaxWidth" type="int">
|
||||
<label>The maximum width any media item in the timeline can be.</label>
|
||||
<default>540</default>
|
||||
</entry>
|
||||
<entry name="MediaMaxHeight" type="int">
|
||||
<label>The maximum height any media item in the timeline can be.</label>
|
||||
<default>540</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="RoomDrawer">
|
||||
<entry name="ShowAvatarInRoomDrawer" type="bool">
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include <QImageReader>
|
||||
|
||||
#include "controller.h"
|
||||
#include "jobs/neochatchangepasswordjob.h"
|
||||
#include "jobs/neochatdeactivateaccountjob.h"
|
||||
#include "roommanager.h"
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <qt5keychain/keychain.h>
|
||||
@@ -13,8 +16,11 @@
|
||||
#include <qt6keychain/keychain.h>
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <Quotient/csapi/content-repo.h>
|
||||
#include <Quotient/csapi/profile.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#include <Quotient/settings.h>
|
||||
#include <Quotient/user.h>
|
||||
|
||||
@@ -153,3 +159,27 @@ void NeoChatConnection::deactivateAccount(const QString &password)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
const auto job = Connection::createRoom(Connection::PublishRoom, {}, name, topic, {});
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(room));
|
||||
});
|
||||
}
|
||||
|
||||
void NeoChatConnection::createSpace(const QString &name, const QString &topic)
|
||||
{
|
||||
const auto job = Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, {}, {}, QJsonObject{{"type"_ls, "m.space"_ls}});
|
||||
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
|
||||
});
|
||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(room));
|
||||
});
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -48,6 +48,16 @@ public:
|
||||
|
||||
Q_INVOKABLE void deactivateAccount(const QString &password);
|
||||
|
||||
/**
|
||||
* @brief Create new room for a group chat.
|
||||
*/
|
||||
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
|
||||
|
||||
/**
|
||||
* @brief Create new space.
|
||||
*/
|
||||
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
|
||||
|
||||
Q_SIGNALS:
|
||||
void labelChanged();
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "eventhandler.h"
|
||||
#include "events/joinrulesevent.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "filetransferpseudojob.h"
|
||||
@@ -344,8 +345,18 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
||||
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
|
||||
+ eventToString(*event, format, stripNewlines);
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(this);
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QString body;
|
||||
if (format == Qt::TextFormat::RichText) {
|
||||
body = eventHandler.getRichBody(stripNewlines);
|
||||
} else {
|
||||
body = eventHandler.getPlainBody(stripNewlines);
|
||||
}
|
||||
|
||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -483,318 +494,6 @@ QString NeoChatRoom::avatarMediaId() const
|
||||
return {};
|
||||
}
|
||||
|
||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
using namespace Quotient;
|
||||
return switchOnType(
|
||||
evt,
|
||||
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
|
||||
TextHandler textHandler;
|
||||
|
||||
if (e.hasFileContent()) {
|
||||
auto fileCaption = e.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = e.plainBody();
|
||||
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
||||
fileCaption = e.plainBody() + " | "_ls + fileCaption;
|
||||
}
|
||||
textHandler.setData(fileCaption);
|
||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
||||
}
|
||||
|
||||
QString body;
|
||||
if (e.hasTextContent() && e.content()) {
|
||||
body = static_cast<const TextContent *>(e.content())->body;
|
||||
} else {
|
||||
body = e.plainBody();
|
||||
}
|
||||
|
||||
textHandler.setData(body);
|
||||
|
||||
Qt::TextFormat inputFormat;
|
||||
if (e.mimeType().name() == "text/plain"_ls) {
|
||||
inputFormat = Qt::PlainText;
|
||||
} else {
|
||||
inputFormat = Qt::RichText;
|
||||
}
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines);
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == Membership::Leave) {
|
||||
if (e.prevContent() && e.prevContent()->displayName) {
|
||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||
}
|
||||
}
|
||||
|
||||
if (prettyPrint) {
|
||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||
.arg(e.userId(), Utils::getUserColor(user(e.userId())->hueF()).name(), subjectName);
|
||||
}
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
if (!e.reason().isEmpty()) {
|
||||
text += i18n(": %1", e.reason().toHtmlEscaped());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (!e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId())
|
||||
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||
: i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
if (e.reason().isEmpty()) {
|
||||
return i18n("banned %1 from the room", subjectName);
|
||||
} else {
|
||||
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
|
||||
}
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
||||
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
||||
},
|
||||
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||
: i18n("set the topic to: %1",
|
||||
prettyPrint ? Quotient::prettyPrint(e.topic())
|
||||
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
||||
: e.topic());
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
|
||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
|
||||
}
|
||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
|
||||
}
|
||||
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||
return e.contentJson()["description"_ls].toString();
|
||||
}
|
||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
return e.question();
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
{
|
||||
return switchOnType(
|
||||
evt,
|
||||
[](const RoomMessageEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a message");
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
Q_UNUSED(e)
|
||||
return i18n("sent a sticker");
|
||||
},
|
||||
[](const RoomMemberEvent &e) {
|
||||
switch (e.membership()) {
|
||||
case Membership::Invite:
|
||||
if (e.repeatsState()) {
|
||||
return i18n("reinvited someone to the room");
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case Membership::Join: {
|
||||
QString text{};
|
||||
// Part 1: invites and joins
|
||||
if (e.repeatsState()) {
|
||||
text = i18n("joined the room (repeated)");
|
||||
} else if (e.changesMembership()) {
|
||||
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||
}
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
// Part 2: profile changes of joined members
|
||||
if (e.isRename()) {
|
||||
if (!e.newDisplayName()) {
|
||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||
} else {
|
||||
text = i18nc("their refers to a singular user", "changed their display name");
|
||||
}
|
||||
}
|
||||
if (e.isAvatarUpdate()) {
|
||||
if (!text.isEmpty()) {
|
||||
text += i18n(" and ");
|
||||
}
|
||||
if (!e.newAvatarUrl()) {
|
||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||
} else if (!e.prevContent()->avatarUrl) {
|
||||
text += i18n("set an avatar");
|
||||
} else {
|
||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||
}
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
text = i18nc("<user> changed nothing", "changed nothing");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
case Membership::Leave:
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||
}
|
||||
|
||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||
}
|
||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||
case Membership::Ban:
|
||||
if (e.senderId() != e.userId()) {
|
||||
return i18n("banned a user from the room");
|
||||
} else {
|
||||
return i18n("self-banned from the room");
|
||||
}
|
||||
case Membership::Knock: {
|
||||
return i18n("requested an invite");
|
||||
}
|
||||
default:;
|
||||
}
|
||||
return i18n("made something unknown");
|
||||
},
|
||||
[](const RoomCanonicalAliasEvent &e) {
|
||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
||||
},
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
||||
},
|
||||
[](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
},
|
||||
[](const EncryptionEvent &) {
|
||||
return i18n("activated End-to-End Encryption");
|
||||
},
|
||||
[](const RoomCreateEvent &e) {
|
||||
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
||||
},
|
||||
[](const RoomPowerLevelsEvent &) {
|
||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||
},
|
||||
[](const StateEvent &e) {
|
||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||
return i18n("changed the server access control lists for this room");
|
||||
}
|
||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||
return i18n("added a widget");
|
||||
}
|
||||
if (e.contentJson().isEmpty()) {
|
||||
return i18n("removed a widget");
|
||||
}
|
||||
return i18n("configured a widget");
|
||||
}
|
||||
return i18n("updated the state");
|
||||
},
|
||||
[](const PollStartEvent &e) {
|
||||
Q_UNUSED(e);
|
||||
return i18n("started a poll");
|
||||
},
|
||||
i18n("Unknown event"));
|
||||
}
|
||||
|
||||
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||
{
|
||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||
@@ -861,10 +560,14 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
if (isReply) {
|
||||
const auto &replyEvt = **replyIt;
|
||||
|
||||
EventHandler eventHandler;
|
||||
eventHandler.setRoom(this);
|
||||
eventHandler.setEvent(&**replyIt);
|
||||
|
||||
// clang-format off
|
||||
QJsonObject json{
|
||||
{"msgtype"_ls, msgTypeToString(type)},
|
||||
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventToString(replyEvt), text)},
|
||||
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventHandler.getPlainBody(), text)},
|
||||
{"format"_ls, "org.matrix.custom.html"_ls},
|
||||
{"m.relates_to"_ls,
|
||||
QJsonObject {
|
||||
@@ -876,7 +579,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
||||
}
|
||||
},
|
||||
{"formatted_body"_ls,
|
||||
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventToString(replyEvt, Qt::RichText), html)
|
||||
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventHandler.getRichBody(), html)
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
@@ -1586,6 +1289,11 @@ void NeoChatRoom::updatePushNotificationState(QString type)
|
||||
for (const auto &i : roomRuleArray) {
|
||||
QJsonObject roomRule = i.toObject();
|
||||
if (roomRule["rule_id"_ls] == id()) {
|
||||
if (roomRule["actions"_ls].toArray().size() == 0) {
|
||||
m_currentPushNotificationState = PushNotificationState::MentionKeyword;
|
||||
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
|
||||
return;
|
||||
}
|
||||
QString notifyAction = roomRule["actions"_ls].toArray()[0].toString();
|
||||
if (notifyAction == "notify"_ls) {
|
||||
m_currentPushNotificationState = PushNotificationState::All;
|
||||
@@ -1604,6 +1312,11 @@ void NeoChatRoom::updatePushNotificationState(QString type)
|
||||
for (const auto &i : overrideRuleArray) {
|
||||
QJsonObject overrideRule = i.toObject();
|
||||
if (overrideRule["rule_id"_ls] == id()) {
|
||||
if (overrideRule["actions"_ls].toArray().isEmpty()) {
|
||||
m_currentPushNotificationState = PushNotificationState::Mute;
|
||||
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
|
||||
return;
|
||||
}
|
||||
QString notifyAction = overrideRule["actions"_ls].toArray()[0].toString();
|
||||
if (notifyAction == "dont_notify"_ls) {
|
||||
m_currentPushNotificationState = PushNotificationState::Mute;
|
||||
@@ -1692,7 +1405,11 @@ QString NeoChatRoom::chatBoxReplyMessage() const
|
||||
if (m_chatBoxReplyId.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxReplyId)));
|
||||
|
||||
EventHandler eventhandler;
|
||||
eventhandler.setRoom(this);
|
||||
eventhandler.setEvent(&**findInTimeline(m_chatBoxReplyId));
|
||||
return eventhandler.getPlainBody();
|
||||
}
|
||||
|
||||
QVariantMap NeoChatRoom::chatBoxEditUser() const
|
||||
@@ -1708,7 +1425,11 @@ QString NeoChatRoom::chatBoxEditMessage() const
|
||||
if (m_chatBoxEditId.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxEditId)));
|
||||
|
||||
EventHandler eventhandler;
|
||||
eventhandler.setRoom(this);
|
||||
eventhandler.setEvent(&**findInTimeline(m_chatBoxEditId));
|
||||
return eventhandler.getPlainBody();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::chatBoxAttachmentPath() const
|
||||
|
||||
@@ -475,39 +475,6 @@ public:
|
||||
*/
|
||||
[[nodiscard]] const Quotient::RoomEvent *lastEvent() const;
|
||||
|
||||
/**
|
||||
* @brief Output a string for the message content ready for display.
|
||||
*
|
||||
* The output string is dependant upon the event type and the desired output format.
|
||||
*
|
||||
* For most messages this is the body content of the message. For media messages
|
||||
* This will be the caption and for state events it will be a string specific
|
||||
* to that event with some dynamic details about the event added.
|
||||
*
|
||||
* E.g. For a room topic state event the text will be:
|
||||
* "set the topic to: <new topic text>"
|
||||
*
|
||||
* @param evt the event for which a string is desired.
|
||||
* @param format the output format, usually Qt::PlainText or Qt::RichText.
|
||||
* @param stripNewlines whether the output should have new lines in it.
|
||||
*/
|
||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
|
||||
|
||||
/**
|
||||
* @brief Output a generic string for the message content ready for display.
|
||||
*
|
||||
* The output string is dependant upon the event type.
|
||||
*
|
||||
* Unlike NeoChatRoom::eventToString the string is the same for all events of
|
||||
* the same type
|
||||
*
|
||||
* E.g. For a message the text will be:
|
||||
* "sent a message"
|
||||
*
|
||||
* @sa eventToString()
|
||||
*/
|
||||
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
||||
|
||||
/**
|
||||
* @brief Convenient way to call eventToString on the last event.
|
||||
*
|
||||
|
||||
@@ -217,7 +217,7 @@ QQC2.Control {
|
||||
quickFormatBar.close()
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
Keys.onEnterPressed: event => {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete()
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||
@@ -226,7 +226,7 @@ QQC2.Control {
|
||||
root.postMessage();
|
||||
}
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
Keys.onReturnPressed: event => {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete()
|
||||
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
|
||||
@@ -261,7 +261,7 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
Keys.onShortcutOverride: {
|
||||
Keys.onShortcutOverride: event => {
|
||||
// Accept the event only when there was something to cancel. Otherwise, let the event go to the RoomPage.
|
||||
if (cancelButton.visible && event.key === Qt.Key_Escape) {
|
||||
cancelButton.action.trigger();
|
||||
@@ -357,38 +357,26 @@ QQC2.Control {
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
Row {
|
||||
RowLayout {
|
||||
id: actionsRow
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
anchors.right: parent.right
|
||||
property var requiredMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
|
||||
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Kirigami.Units.largeSpacing - 2
|
||||
anchors.bottomMargin: Kirigami.Units.smallSpacing
|
||||
spacing: 0
|
||||
property var requiredMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
|
||||
|
||||
Repeater {
|
||||
model: root.actions
|
||||
Kirigami.Icon {
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
delegate: QQC2.ToolButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||
onClicked: modelData.trigger()
|
||||
|
||||
source: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||
active: actionArea.containsPress
|
||||
visible: modelData.visible
|
||||
enabled: modelData.enabled
|
||||
MouseArea {
|
||||
id: actionArea
|
||||
anchors.fill: parent
|
||||
onClicked: modelData.trigger()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
QQC2.ToolTip.visible: modelData.tooltip !== "" && hoverHandler.hovered
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: modelData.tooltip
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
PieProgressBar {
|
||||
visible: modelData.isBusy
|
||||
@@ -411,7 +399,7 @@ QQC2.Control {
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
onChosen: insertText(emoji)
|
||||
onChosen: emoji => insertText(emoji)
|
||||
onClosed: if (emojiAction.checked) emojiAction.checked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
@@ -66,9 +65,10 @@ ColumnLayout {
|
||||
visible: root.currentRoom.canSendEvent("m.room.message")
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Math.max(Kirigami.Units.gridUnit * 2, implicitHeight + Kirigami.Units.largeSpacing)
|
||||
Layout.minimumHeight: Math.max(Kirigami.Units.gridUnit * 2, Math.round(implicitHeight) + Kirigami.Units.largeSpacing)
|
||||
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
|
||||
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding
|
||||
Layout.preferredHeight: Math.round(implicitHeight)
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
QQC2.Popup {
|
||||
id: completionMenu
|
||||
id: root
|
||||
width: parent.width
|
||||
|
||||
visible: completions.count > 0
|
||||
@@ -38,7 +38,7 @@ QQC2.Popup {
|
||||
}
|
||||
|
||||
function complete() {
|
||||
completionMenu.chatDocumentHandler.complete(completions.currentIndex)
|
||||
root.chatDocumentHandler.complete(completions.currentIndex)
|
||||
}
|
||||
|
||||
leftPadding: 0
|
||||
@@ -52,7 +52,7 @@ QQC2.Popup {
|
||||
id: completions
|
||||
|
||||
anchors.fill: parent
|
||||
model: completionMenu.chatDocumentHandler.completionModel
|
||||
model: root.chatDocumentHandler.completionModel
|
||||
currentIndex: 0
|
||||
keyNavigationWraps: true
|
||||
highlightMoveDuration: 100
|
||||
@@ -81,7 +81,7 @@ QQC2.Popup {
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
onClicked: completionMenu.chatDocumentHandler.complete(completionDelegate.index)
|
||||
onClicked: root.chatDocumentHandler.complete(completionDelegate.index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,113 +2,99 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
|
||||
import org.kde.kitemmodels 1.0
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
ColumnLayout {
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormComboBoxDelegate {
|
||||
text: i18n("Room")
|
||||
textRole: "displayName"
|
||||
valueRole: "id"
|
||||
model: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
Component.onCompleted: currentIndex = indexOfValue(room.id)
|
||||
onCurrentValueChanged: room = roomListModel.roomByAliasOrId(currentValue)
|
||||
}
|
||||
MobileForm.FormCheckDelegate {
|
||||
text: i18n("Show m.room.member events")
|
||||
checked: true
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
stateEventFilterModel.removeStateEventTypeFiltered("m.room.member");
|
||||
} else {
|
||||
stateEventFilterModel.addStateEventTypeFiltered("m.room.member");
|
||||
}
|
||||
}
|
||||
}
|
||||
MobileForm.FormCheckDelegate {
|
||||
id: roomAccoutnDataVisibleCheck
|
||||
text: i18n("Show room account data")
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
}
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
visible: roomAccoutnDataVisibleCheck.checked
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormCardHeader {
|
||||
title: i18n("Room Account Data for %1 - %2", room.displayName, room.id)
|
||||
}
|
||||
id: root
|
||||
|
||||
Repeater {
|
||||
model: room.accountDataEventTypes
|
||||
delegate: MobileForm.FormTextDelegate {
|
||||
text: modelData
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/MessageSourceSheet.qml", {
|
||||
"sourceText": room.roomAcountDataJson(text)
|
||||
}, {
|
||||
"title": i18n("Event Source"),
|
||||
"width": Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
required property NeoChatRoom room
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: i18nc("@title", "Choose Room")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: roomComboBox
|
||||
text: i18n("Room")
|
||||
textRole: "displayName"
|
||||
valueRole: "roomId"
|
||||
model: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
currentIndex: -1
|
||||
Component.onCompleted: currentIndex = roomListModel.rowForRoom(root.room)
|
||||
onCurrentValueChanged: root.room = roomListModel.roomByAliasOrId(roomComboBox.currentValue)
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("Room Id: %1", root.room.id)
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
text: i18n("Show m.room.member events")
|
||||
checked: true
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
stateEventFilterModel.removeStateEventTypeFiltered("m.room.member");
|
||||
} else {
|
||||
stateEventFilterModel.addStateEventTypeFiltered("m.room.member");
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormCheckDelegate {
|
||||
id: roomAccountDataVisibleCheck
|
||||
text: i18n("Show room account data")
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormCardHeader {
|
||||
id: stateEventListHeader
|
||||
title: i18n("Room State for %1", room.displayName)
|
||||
subtitle: room.id
|
||||
FormCard.FormHeader {
|
||||
visible: roomAccountDataVisibleCheck.checked
|
||||
title: i18n("Room Account Data")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
visible: roomAccountDataVisibleCheck.checked
|
||||
Repeater {
|
||||
model: root.room.accountDataEventTypes
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: modelData
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer("qrc:/MessageSourceSheet.qml", {
|
||||
"sourceText": root.room.roomAcountDataJson(text)
|
||||
}, {
|
||||
"title": i18n("Event Source"),
|
||||
"width": Kirigami.Units.gridUnit * 25
|
||||
})
|
||||
}
|
||||
QQC2.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: stateEventListView
|
||||
clip: true
|
||||
|
||||
model: StateFilterModel {
|
||||
id: stateEventFilterModel
|
||||
sourceModel: StateModel {
|
||||
id: stateModel
|
||||
room: devtoolsPage.room
|
||||
}
|
||||
}
|
||||
|
||||
delegate: MobileForm.FormTextDelegate {
|
||||
text: model.type
|
||||
description: model.stateKey
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
|
||||
sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0)))
|
||||
}, {
|
||||
title: i18n("Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
id: stateEventListHeader
|
||||
title: i18n("Room State")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
Repeater {
|
||||
model: StateFilterModel {
|
||||
id: stateEventFilterModel
|
||||
sourceModel: StateModel {
|
||||
id: stateModel
|
||||
room: root.room
|
||||
}
|
||||
}
|
||||
|
||||
delegate: FormCard.FormButtonDelegate {
|
||||
text: model.type
|
||||
description: model.stateKey
|
||||
onClicked: applicationWindow().pageStack.pushDialogLayer('qrc:/MessageSourceSheet.qml', {
|
||||
sourceText: stateModel.stateEventJson(stateEventFilterModel.mapToSource(stateEventFilterModel.index(model.index, 0)))
|
||||
}, {
|
||||
title: i18n("Event Source"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,58 +6,43 @@ import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm
|
||||
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
|
||||
import org.kde.kitemmodels 1.0
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
ColumnLayout {
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormCardHeader {
|
||||
title: i18n("Server Capabilities")
|
||||
}
|
||||
MobileForm.FormTextDelegate {
|
||||
text: i18n("Can change password")
|
||||
description: Controller.activeConnection.canChangePassword
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Server Capabilities")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
text: i18n("Can change password")
|
||||
description: Controller.activeConnection.canChangePassword
|
||||
}
|
||||
}
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormCardHeader {
|
||||
title: i18n("Default Room Version")
|
||||
}
|
||||
MobileForm.FormTextDelegate {
|
||||
text: Controller.activeConnection.defaultRoomVersion
|
||||
}
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Default Room Version")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextDelegate {
|
||||
text: Controller.activeConnection.defaultRoomVersion
|
||||
}
|
||||
}
|
||||
MobileForm.FormCard {
|
||||
Layout.fillWidth: true
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
MobileForm.FormCardHeader {
|
||||
title: i18n("Available Room Versions")
|
||||
}
|
||||
Repeater {
|
||||
model: Controller.getSupportedRoomVersions(room.connection)
|
||||
FormCard.FormHeader {
|
||||
title: i18n("Available Room Versions")
|
||||
}
|
||||
FormCard.FormCard {
|
||||
Repeater {
|
||||
model: room.connection.getSupportedRoomVersions()
|
||||
|
||||
delegate: MobileForm.FormTextDelegate {
|
||||
text: modelData.id
|
||||
contentItem.children: QQC2.Label {
|
||||
text: modelData.status
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
text: modelData.id
|
||||
contentItem.children: QQC2.Label {
|
||||
text: modelData.status
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@ import QtQuick.Controls 2.15 as QQC2
|
||||
import org.kde.kirigami 2.20 as Kirigami
|
||||
|
||||
QQC2.ItemDelegate {
|
||||
id: emojiDelegate
|
||||
id: root
|
||||
|
||||
property string name
|
||||
property string emoji
|
||||
property bool showTones: false
|
||||
property bool isImage: false
|
||||
|
||||
QQC2.ToolTip.text: emojiDelegate.name
|
||||
QQC2.ToolTip.visible: hovered && emojiDelegate.name !== ""
|
||||
QQC2.ToolTip.text: root.name
|
||||
QQC2.ToolTip.visible: hovered && root.name !== ""
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
leftInset: Kirigami.Units.smallSpacing
|
||||
topInset: Kirigami.Units.smallSpacing
|
||||
@@ -24,8 +24,8 @@ QQC2.ItemDelegate {
|
||||
contentItem: Item {
|
||||
Kirigami.Heading {
|
||||
anchors.fill: parent
|
||||
visible: !emojiDelegate.emoji.startsWith("image") && !emojiDelegate.isImage
|
||||
text: emojiDelegate.emoji
|
||||
visible: !root.emoji.startsWith("image") && !root.isImage
|
||||
text: root.emoji
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: "emoji"
|
||||
@@ -36,25 +36,25 @@ QQC2.ItemDelegate {
|
||||
source: "arrow-down"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
visible: emojiDelegate.showTones
|
||||
visible: root.showTones
|
||||
}
|
||||
}
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: emojiDelegate.emoji.startsWith("image") || emojiDelegate.isImage
|
||||
source: visible ? emojiDelegate.emoji : ""
|
||||
visible: root.emoji.startsWith("image") || root.isImage
|
||||
source: visible ? root.emoji : ""
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: emojiDelegate.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
|
||||
color: root.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
anchors.fill: parent
|
||||
color: Kirigami.Theme.highlightColor
|
||||
opacity: emojiDelegate.hovered && !emojiDelegate.pressed ? 0.2 : 0
|
||||
opacity: root.hovered && !root.pressed ? 0.2 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.kde.kirigami 2.20 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: emojiGrid
|
||||
id: root
|
||||
|
||||
property alias model: emojis.model
|
||||
property alias count: emojis.count
|
||||
@@ -40,10 +40,10 @@ QQC2.ScrollView {
|
||||
}
|
||||
onModelChanged: currentIndex = -1
|
||||
|
||||
cellWidth: emojis.width / emojiGrid.emojisPerRow
|
||||
cellHeight: emojiGrid.targetIconSize
|
||||
cellWidth: emojis.width / root.emojisPerRow
|
||||
cellHeight: root.targetIconSize
|
||||
|
||||
KeyNavigation.up: emojiGrid.header
|
||||
KeyNavigation.up: root.header
|
||||
|
||||
clip: true
|
||||
|
||||
@@ -56,14 +56,14 @@ QQC2.ScrollView {
|
||||
width: emojis.cellWidth
|
||||
height: emojis.cellHeight
|
||||
|
||||
isImage: emojiGrid.stickers
|
||||
isImage: root.stickers
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onReturnPressed: clicked()
|
||||
onClicked: {
|
||||
if (emojiGrid.stickers) {
|
||||
emojiGrid.stickerChosen(model.index)
|
||||
if (root.stickers) {
|
||||
root.stickerChosen(model.index)
|
||||
}
|
||||
emojiGrid.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode)
|
||||
root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode)
|
||||
EmojiModel.emojiUsed(modelData)
|
||||
}
|
||||
Keys.onSpacePressed: pressAndHold()
|
||||
@@ -71,7 +71,7 @@ QQC2.ScrollView {
|
||||
if (EmojiModel.tones(modelData.shortName).length === 0) {
|
||||
return;
|
||||
}
|
||||
let tones = tonesPopupComponent.createObject(emojiDelegate, {shortName: modelData.shortName, unicode: modelData.unicode, categoryIconSize: emojiGrid.targetIconSize})
|
||||
let tones = tonesPopupComponent.createObject(emojiDelegate, {shortName: modelData.shortName, unicode: modelData.unicode, categoryIconSize: root.targetIconSize})
|
||||
tones.open()
|
||||
tones.forceActiveFocus()
|
||||
}
|
||||
@@ -80,14 +80,14 @@ QQC2.ScrollView {
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
text: emojiGrid.stickers ? i18n("No stickers") : i18n("No emojis")
|
||||
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
|
||||
visible: emojis.count === 0
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: tonesPopupComponent
|
||||
EmojiTonesPicker {
|
||||
onChosen: emojiGrid.chosen(emoji)
|
||||
onChosen: root.chosen(emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user