Compare commits
94 Commits
work/nvrwh
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a94579e0cf | ||
|
|
66343ba11e | ||
|
|
684cd85a7a | ||
|
|
ef9a80e76f | ||
|
|
fbb5f02379 | ||
|
|
5f4bde96e9 | ||
|
|
f8c8a68840 | ||
|
|
bf6f4a951e | ||
|
|
58c9366548 | ||
|
|
f410ecac2b | ||
|
|
1d1a43ade2 | ||
|
|
37adb56233 | ||
|
|
aca0669bf6 | ||
|
|
b33ab76ff8 | ||
|
|
38a391b7fa | ||
|
|
82434fe87c | ||
|
|
8bf7c36249 | ||
|
|
cff3557a24 | ||
|
|
2c476c4351 | ||
|
|
82c8ab511d | ||
|
|
486ed6edd2 | ||
|
|
a4b0a9ed36 | ||
|
|
bba9c37ba5 | ||
|
|
febc7d1630 | ||
|
|
1ed071949b | ||
|
|
3878c264ef | ||
|
|
21daf0b664 | ||
|
|
5efaa72cea | ||
|
|
191cd7cbba | ||
|
|
c583e31b16 | ||
|
|
590fba7deb | ||
|
|
a8f22003cb | ||
|
|
54596e3fe6 | ||
|
|
2ee4d110a0 | ||
|
|
7a949dccbb | ||
|
|
111a45ab38 | ||
|
|
2bcf59c225 | ||
|
|
d542033125 | ||
|
|
44c72828e1 | ||
|
|
99d3ee32fa | ||
|
|
7df0ff309e | ||
|
|
856a751fcb | ||
|
|
da99bcae5d | ||
|
|
1b0c6c2847 | ||
|
|
c315e817b2 | ||
|
|
6fde07a20d | ||
|
|
8a86159fd7 | ||
|
|
1d532a1fc1 | ||
|
|
a67ce75924 | ||
|
|
b3d845ea32 | ||
|
|
008e12cb42 | ||
|
|
ceaed8be51 | ||
|
|
f0e0979366 | ||
|
|
c43563a804 | ||
|
|
8ec3b2d05d | ||
|
|
39a95c727f | ||
|
|
a2f5a585e3 | ||
|
|
aa95bc62bd | ||
|
|
ae7bfa5bcb | ||
|
|
37de1ec583 | ||
|
|
bb8f0eae1b | ||
|
|
571d3c14c8 | ||
|
|
d7202ae0a7 | ||
|
|
2a9c75e24f | ||
|
|
7231662f94 | ||
|
|
df83927ed7 | ||
|
|
f14dfc5de8 | ||
|
|
188d0c9d5c | ||
|
|
8d68c64fdf | ||
|
|
d796ab350e | ||
|
|
334a1b5bef | ||
|
|
3304ee0985 | ||
|
|
d7b3523544 | ||
|
|
1a8d346064 | ||
|
|
42f9b36667 | ||
|
|
c21e9f2114 | ||
|
|
3dbe605de8 | ||
|
|
41a6dd6175 | ||
|
|
09af6fe0a7 | ||
|
|
3b8c3afa3e | ||
|
|
7b7f4d264c | ||
|
|
c454a4942e | ||
|
|
44cd52af6c | ||
|
|
0d01339b02 | ||
|
|
e5b4ca53f8 | ||
|
|
703b03b33c | ||
|
|
73cdad66ac | ||
|
|
4b1afdbe2d | ||
|
|
64bfc0f29a | ||
|
|
28c4c0b48c | ||
|
|
40ccde9f06 | ||
|
|
baa7d02ba5 | ||
|
|
a391df1e67 | ||
|
|
1f26485208 |
@@ -2,5 +2,7 @@
|
||||
; SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/frameworks/extra-cmake-modules.version=master
|
||||
kde/unreleased/kirigami-addons.version=master
|
||||
kde/applications/neochat.packageAppx=True
|
||||
libs/qt.qtMajorVersion=6
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "org.kde.neochat",
|
||||
"branch": "master",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.7",
|
||||
"runtime-version": "6.8",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "neochat",
|
||||
"tags": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"name": "kirigamiaddons",
|
||||
"config-opts": [ "-DBUILD_TESTING=OFF" ],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "commit": "34d311219e8b7209746a98b3a29b91ded05ff936" } ]
|
||||
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "tag": "v1.7.0" } ]
|
||||
},
|
||||
{
|
||||
"name": "kquickimageeditor",
|
||||
|
||||
@@ -5,10 +5,13 @@ include:
|
||||
- project: sysadmin/ci-utilities
|
||||
file:
|
||||
- /gitlab-templates/reuse-lint.yml
|
||||
- /gitlab-templates/json-validation.yml
|
||||
- /gitlab-templates/xml-lint.yml
|
||||
- /gitlab-templates/yaml-lint.yml
|
||||
- /gitlab-templates/android-qt6.yml
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
# - /gitlab-templates/freebsd-qt6.yml
|
||||
# - /gitlab-templates/freebsd-qt6.yml
|
||||
- /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/snap-snapcraft-lxd.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
|
||||
71
.kde-ci.yml
71
.kde-ci.yml
@@ -2,42 +2,43 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
Dependencies:
|
||||
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@latest-kf6'
|
||||
'frameworks/kcoreaddons': '@latest-kf6'
|
||||
'frameworks/kirigami': '@latest-kf6'
|
||||
'frameworks/ki18n': '@latest-kf6'
|
||||
'frameworks/kconfig': '@latest-kf6'
|
||||
'frameworks/syntax-highlighting': '@latest-kf6'
|
||||
'frameworks/kitemmodels': '@latest-kf6'
|
||||
'frameworks/kquickcharts': '@latest-kf6'
|
||||
'frameworks/knotifications': '@latest-kf6'
|
||||
'frameworks/kcolorscheme': '@latest-kf6'
|
||||
'libraries/kquickimageeditor': '@latest-kf6'
|
||||
'frameworks/sonnet': '@latest-kf6'
|
||||
'frameworks/prison': '@latest-kf6'
|
||||
'libraries/kirigami-addons': '@latest-kf6'
|
||||
'third-party/libquotient': '@latest'
|
||||
'third-party/qtkeychain': '@latest'
|
||||
'third-party/cmark': '@latest'
|
||||
'third-party/qcoro': '@latest'
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
'frameworks/kcrash': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
'frameworks/purpose': '@latest-kf6'
|
||||
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@latest-kf6'
|
||||
'frameworks/kcoreaddons': '@latest-kf6'
|
||||
'frameworks/kirigami': '@latest-kf6'
|
||||
'frameworks/ki18n': '@latest-kf6'
|
||||
'frameworks/kconfig': '@latest-kf6'
|
||||
'frameworks/syntax-highlighting': '@latest-kf6'
|
||||
'frameworks/kitemmodels': '@latest-kf6'
|
||||
'frameworks/kquickcharts': '@latest-kf6'
|
||||
'frameworks/knotifications': '@latest-kf6'
|
||||
'frameworks/kcolorscheme': '@latest-kf6'
|
||||
'frameworks/kiconthemes': '@latest-kf6'
|
||||
'libraries/kquickimageeditor': '@latest-kf6'
|
||||
'frameworks/sonnet': '@latest-kf6'
|
||||
'frameworks/prison': '@latest-kf6'
|
||||
'libraries/kirigami-addons': '@latest-kf6'
|
||||
'third-party/libquotient': '@latest'
|
||||
'third-party/qtkeychain': '@latest'
|
||||
'third-party/cmark': '@latest'
|
||||
'third-party/qcoro': '@latest'
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
'frameworks/kcrash': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
'frameworks/purpose': '@latest-kf6'
|
||||
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD', 'Windows' ]
|
||||
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
|
||||
@@ -66,7 +66,7 @@ if (QT_KNOWN_POLICY_QTP0004)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
endif ()
|
||||
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme)
|
||||
set_package_properties(KF6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
@@ -174,6 +174,9 @@ if (BUILD_TESTING)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
|
||||
add_subdirectory(autotests)
|
||||
add_subdirectory(appiumtests)
|
||||
if (NOT ANDROID)
|
||||
add_subdirectory(memorytests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(KF6DocTools_FOUND)
|
||||
|
||||
@@ -82,3 +82,9 @@ path = "src/purpose/purposeplugin.json"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
|
||||
SPDX-License-Identifier = "BSD-2-Clause"
|
||||
|
||||
[[annotations]]
|
||||
path = "memorytests/memtest-sync.json"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
|
||||
SPDX-License-Identifier = "BSD-2-Clause"
|
||||
|
||||
@@ -137,7 +137,11 @@ void TimelineMessageModelTest::pendingEvent()
|
||||
model->setRoom(room);
|
||||
QCOMPARE(model->rowCount(), 0);
|
||||
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
auto txnId = room->postText("New plain message"_L1);
|
||||
#else
|
||||
auto txnId = room->postPlainText("New plain message"_L1);
|
||||
#endif
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(spyInsert.count(), 1);
|
||||
|
||||
@@ -145,7 +149,11 @@ void TimelineMessageModelTest::pendingEvent()
|
||||
QCOMPARE(model->rowCount(), 0);
|
||||
QCOMPARE(spyRemove.count(), 1);
|
||||
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
txnId = room->postText("New plain message"_L1);
|
||||
#else
|
||||
txnId = room->postPlainText("New plain message"_L1);
|
||||
#endif
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(spyInsert.count(), 2);
|
||||
|
||||
|
||||
34
memorytests/CMakeLists.txt
Normal file
34
memorytests/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
|
||||
|
||||
qt_add_executable(timeline-memtest
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(timeline-memtest PRIVATE neochatplugin timelineplugin)
|
||||
target_link_libraries(timeline-memtest PUBLIC
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
Qt::Qml
|
||||
Qt::Gui
|
||||
Qt::QuickControls2
|
||||
KF6::Kirigami
|
||||
QuotientQt6
|
||||
neochat
|
||||
)
|
||||
|
||||
ecm_add_qml_module(timeline-memtest URI org.kde.neochat.timeline-memtest GENERATE_PLUGIN_SOURCE
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline-memtest
|
||||
QML_FILES
|
||||
Main.qml
|
||||
SOURCES
|
||||
memtesttimelinemodel.cpp
|
||||
memtesttimelinemodel.h
|
||||
DEPENDENCIES
|
||||
QtCore
|
||||
QtQuick
|
||||
IMPORTS
|
||||
org.kde.neochat
|
||||
)
|
||||
33
memorytests/Main.qml
Normal file
33
memorytests/Main.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
title: "Timeline Memory Test"
|
||||
|
||||
minimumWidth: Kirigami.Units.gridUnit * 30
|
||||
minimumHeight: Kirigami.Units.gridUnit * 30
|
||||
|
||||
visible: true
|
||||
|
||||
QQC2.ScrollView {
|
||||
width: root.width
|
||||
height: root.height
|
||||
|
||||
contentItem: ListView {
|
||||
model: messageFilterModel
|
||||
|
||||
delegate: EventDelegate {
|
||||
room: memTestTimelineModel.room
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
memorytests/main.cpp
Normal file
30
memorytests/main.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "memtesttimelinemodel.h"
|
||||
#include "models/messagefiltermodel.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
|
||||
|
||||
MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel;
|
||||
MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel);
|
||||
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
|
||||
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
655
memorytests/memtest-sync.json
Normal file
655
memorytests/memtest-sync.json
Normal file
@@ -0,0 +1,655 @@
|
||||
{
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"$1000000000000:example.org": {
|
||||
"m.read": {
|
||||
"@alice:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000000:example.org": {
|
||||
"m.read": {
|
||||
"@bob:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@tim:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@example:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@jeff:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@tina:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@sally:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1000000000003:example.org": {
|
||||
"m.read": {
|
||||
"@fred:example.org": {
|
||||
"ts": 1000000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582555PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "alice:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Bob",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "bob:example.org",
|
||||
"state_key": "@bob:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Tim",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "tim:example.org",
|
||||
"state_key": "@tim:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Jeff",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "jeff:example.org",
|
||||
"state_key": "@jeff:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Tina",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "tina:example.org",
|
||||
"state_key": "@tina:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Sally",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "sally:example.org",
|
||||
"state_key": "@sally:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Fred",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "fred:example.org",
|
||||
"state_key": "@fred:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an example<br>text message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000000:example.org",
|
||||
"origin_server_ts": 1000000000000,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:example.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000001:example.org",
|
||||
"origin_server_ts": 1000000000001,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$1000000000001:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1000000000002,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$1000000000002:example.org",
|
||||
"age": 390159120
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "reply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$1000000000000:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1000000000003,
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$1000000000003: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": "$1000000000004:example.org",
|
||||
"origin_server_ts": 1000000000004,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");\n\n return app.exec();\n}\n</code></pre>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000005:example.org",
|
||||
"origin_server_ts": 1000000000005,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000006:example.org",
|
||||
"origin_server_ts": 1000000000006,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000007:example.org",
|
||||
"origin_server_ts": 1000000000007,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an example<br>text message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000008:example.org",
|
||||
"origin_server_ts": 1000000000008,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:example.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000009:example.org",
|
||||
"origin_server_ts": 1000000000009,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$1000000000009:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1000000000010,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$1000000000010:example.org",
|
||||
"age": 390159120
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "reply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$1000000000008:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1000000000011,
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$1000000000011: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": "$1000000000012:example.org",
|
||||
"origin_server_ts": 1000000000012,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");\n\n return app.exec();\n}\n</code></pre>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000013:example.org",
|
||||
"origin_server_ts": 1000000000013,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000014:example.org",
|
||||
"origin_server_ts": 1000000000014,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000015:example.org",
|
||||
"origin_server_ts": 1000000000015,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an example<br>text message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000016:example.org",
|
||||
"origin_server_ts": 1000000000016,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:example.org",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000017:example.org",
|
||||
"origin_server_ts": 1000000000017,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$1000000000017:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1000000000018,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$1000000000018:example.org",
|
||||
"age": 390159120
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "reply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$1000000000016:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1000000000019,
|
||||
"sender": "@alice:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$1000000000019: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": "$1000000000020:example.org",
|
||||
"origin_server_ts": 1000000000020,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");\n\n return app.exec();\n}\n</code></pre>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000021:example.org",
|
||||
"origin_server_ts": 1000000000021,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000022:example.org",
|
||||
"origin_server_ts": 1000000000022,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1000000000023:example.org",
|
||||
"origin_server_ts": 1000000000023,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
36
memorytests/memtesttimelinemodel.cpp
Normal file
36
memorytests/memtesttimelinemodel.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "memtesttimelinemodel.h"
|
||||
|
||||
#include <Quotient/events/eventcontent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MemTestTimelineModel::MemTestTimelineModel(QObject *parent)
|
||||
: MessageModel(parent)
|
||||
{
|
||||
beginResetModel();
|
||||
m_connection = Connection::makeMockConnection(u"@bob:example.org"_s);
|
||||
m_room = new MemTestRoom(m_connection, u"#memtestroom:example.org"_s, u"memtest-sync.json"_s);
|
||||
|
||||
for (const auto &eventIt : m_room->messageEvents()) {
|
||||
Q_EMIT newEventAdded(eventIt.event());
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<const RoomEvent>> MemTestTimelineModel::getEventForIndex(QModelIndex index) const
|
||||
{
|
||||
return *m_room->messageEvents().at(index.row()).event();
|
||||
}
|
||||
|
||||
int MemTestTimelineModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_room->messageEvents().size();
|
||||
}
|
||||
|
||||
#include "moc_memtesttimelinemodel.cpp"
|
||||
75
memorytests/memtesttimelinemodel.h
Normal file
75
memorytests/memtesttimelinemodel.h
Normal file
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "models/messagemodel.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
class MemTestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
MemTestRoom(Quotient::Connection *connection, const QString &roomName, const QString &syncFileName = {})
|
||||
: NeoChatRoom(connection, roomName, Quotient::JoinState::Join)
|
||||
{
|
||||
syncNewEvents(syncFileName);
|
||||
}
|
||||
|
||||
void update(Quotient::SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
|
||||
void syncNewEvents(const QString &syncFileName)
|
||||
{
|
||||
if (!syncFileName.isEmpty()) {
|
||||
QFile testSyncFile;
|
||||
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
|
||||
testSyncFile.open(QIODevice::ReadOnly);
|
||||
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
|
||||
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson);
|
||||
update(std::move(roomData));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class MemTestTimelineModel
|
||||
*
|
||||
* This is a special version of the MessageModel design to load an unchanging set
|
||||
* of events from a json file so that timeline memory optimisations can be measured.
|
||||
*/
|
||||
class MemTestTimelineModel : public MessageModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit MemTestTimelineModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
private:
|
||||
QPointer<Quotient::Connection> m_connection;
|
||||
|
||||
std::vector<Quotient::RoomEventPtr> m_events;
|
||||
|
||||
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
|
||||
};
|
||||
@@ -473,6 +473,8 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="24.12.2" date="2025-02-06"/>
|
||||
<release version="24.12.1" date="2025-01-09"/>
|
||||
<release version="24.12.0" date="2024-12-12"/>
|
||||
<release version="24.08.3" date="2024-11-07"/>
|
||||
<release version="24.08.2" date="2024-10-10"/>
|
||||
@@ -648,6 +650,9 @@
|
||||
<url>https://carlschwan.eu/2020/12/23/announcing-neochat-1.0-the-kde-matrix-client/</url>
|
||||
</release>
|
||||
</releases>
|
||||
<requires>
|
||||
<display_length compare="ge">360</display_length>
|
||||
</requires>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#a6e4f3</color>
|
||||
<color type="primary" scheme_preference="dark">#235670</color>
|
||||
|
||||
@@ -119,6 +119,7 @@ Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவு
|
||||
Comment[tr]=Matrix üzerinde sohbet edin
|
||||
Comment[uk]=Спілкування у Matrix
|
||||
Comment[x-test]=xxChat on Matrixxx
|
||||
Comment[zh_CN]=在 Matrix 上聊天
|
||||
Comment[zh_TW]=在 Matrix 上聊天
|
||||
MimeType=x-scheme-handler/matrix;
|
||||
Exec=neochat %u
|
||||
|
||||
874
po/ar/neochat.po
874
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
927
po/az/neochat.po
927
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
868
po/ca/neochat.po
868
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
915
po/cs/neochat.po
915
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
886
po/da/neochat.po
886
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1143
po/de/neochat.po
1143
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1000
po/el/neochat.po
1000
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
915
po/eo/neochat.po
915
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
868
po/es/neochat.po
868
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
879
po/eu/neochat.po
879
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1099
po/fi/neochat.po
1099
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
886
po/fr/neochat.po
886
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
912
po/gl/neochat.po
912
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
905
po/hi/neochat.po
905
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
912
po/hu/neochat.po
912
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
869
po/ia/neochat.po
869
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
958
po/id/neochat.po
958
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
907
po/ie/neochat.po
907
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
874
po/it/neochat.po
874
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
818
po/ja/neochat.po
818
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
868
po/ka/neochat.po
868
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
927
po/ko/neochat.po
927
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
821
po/lt/neochat.po
821
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1174
po/lv/neochat.po
1174
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
874
po/nl/neochat.po
874
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1036
po/nn/neochat.po
1036
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
925
po/pa/neochat.po
925
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
918
po/pl/neochat.po
918
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
958
po/pt/neochat.po
958
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
911
po/ru/neochat.po
911
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
907
po/sa/neochat.po
907
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
946
po/sk/neochat.po
946
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
869
po/sl/neochat.po
869
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
985
po/sv/neochat.po
985
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
913
po/ta/neochat.po
913
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
862
po/tr/neochat.po
862
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
870
po/uk/neochat.po
870
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,8 +28,8 @@ apps:
|
||||
compression: lzo
|
||||
|
||||
package-repositories:
|
||||
- type: apt
|
||||
ppa: ubuntu-toolchain-r/test
|
||||
- type: apt
|
||||
ppa: ubuntu-toolchain-r/test
|
||||
|
||||
slots:
|
||||
session-dbus-interface:
|
||||
@@ -66,7 +66,7 @@ parts:
|
||||
- -Dcrypto=disabled
|
||||
- -Dgtk_doc=false
|
||||
build-packages:
|
||||
- meson
|
||||
- meson
|
||||
- libglib2.0-dev
|
||||
- libgcrypt20-dev
|
||||
prime:
|
||||
@@ -81,7 +81,8 @@ parts:
|
||||
plugin: cmake
|
||||
build-environment:
|
||||
- PATH: /snap/bin:${PATH}
|
||||
- PKG_CONFIG_PATH: $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH
|
||||
- PKG_CONFIG_PATH: "$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET\
|
||||
/pkgconfig:$PKG_CONFIG_PATH"
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
@@ -115,7 +116,9 @@ parts:
|
||||
- -DQuotient_ENABLE_E2EE=ON
|
||||
- -DBUILD_WITH_QT6=ON
|
||||
override-build: |
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13
|
||||
"update-alternatives --install /usr/bin/gcc gcc\
|
||||
/usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ \
|
||||
/usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13"
|
||||
craftctl default
|
||||
prime:
|
||||
- -usr/include
|
||||
@@ -172,4 +175,3 @@ parts:
|
||||
- libcmark0.30.2
|
||||
prime:
|
||||
- usr/lib/*/libcmark.so*
|
||||
|
||||
|
||||
@@ -126,8 +126,6 @@ add_library(neochat STATIC
|
||||
registration.cpp
|
||||
neochatconnection.cpp
|
||||
neochatconnection.h
|
||||
jobs/neochatdeactivateaccountjob.cpp
|
||||
jobs/neochatdeactivateaccountjob.h
|
||||
jobs/neochatgetcommonroomsjob.cpp
|
||||
jobs/neochatgetcommonroomsjob.h
|
||||
mediasizehelper.cpp
|
||||
@@ -194,6 +192,12 @@ add_library(neochat STATIC
|
||||
models/roomsortparametermodel.h
|
||||
models/messagemodel.cpp
|
||||
models/messagemodel.h
|
||||
models/messagecontentfiltermodel.cpp
|
||||
models/messagecontentfiltermodel.h
|
||||
models/pinnedmessagemodel.cpp
|
||||
models/pinnedmessagemodel.h
|
||||
models/commonroomsmodel.cpp
|
||||
models/commonroomsmodel.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
@@ -213,7 +217,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/AccountMenu.qml
|
||||
qml/ExploreComponent.qml
|
||||
qml/ExploreComponentMobile.qml
|
||||
qml/ContextMenu.qml
|
||||
qml/RoomContextMenu.qml
|
||||
qml/CollapsedRoomDelegate.qml
|
||||
qml/RoomDelegate.qml
|
||||
qml/RoomListPage.qml
|
||||
@@ -249,6 +253,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/MessageSourceSheet.qml
|
||||
qml/ConfirmEncryptionDialog.qml
|
||||
qml/RoomSearchPage.qml
|
||||
qml/RoomPinnedMessagesPage.qml
|
||||
qml/LocationChooser.qml
|
||||
qml/TimelineView.qml
|
||||
qml/InvitationView.qml
|
||||
@@ -418,6 +423,7 @@ target_link_libraries(neochat PUBLIC
|
||||
KF6::ConfigGui
|
||||
KF6::CoreAddons
|
||||
KF6::SonnetCore
|
||||
KF6::IconThemes
|
||||
KF6::ColorScheme
|
||||
KF6::ItemModels
|
||||
QuotientQt6
|
||||
@@ -491,6 +497,7 @@ if(ANDROID)
|
||||
"network-connect"
|
||||
"list-remove-user"
|
||||
"org.kde.neochat"
|
||||
"org.kde.neochat.tray"
|
||||
"preferences-system-users"
|
||||
"preferences-desktop-theme-global"
|
||||
"notifications"
|
||||
@@ -528,12 +535,16 @@ if(ANDROID)
|
||||
"object-rotate-left"
|
||||
"object-rotate-right"
|
||||
"add-subtitle"
|
||||
"security-high"
|
||||
"security-low"
|
||||
"security-low-symbolic"
|
||||
"kde"
|
||||
"list-remove-symbolic"
|
||||
"edit-delete"
|
||||
"user-home-symbolic"
|
||||
"pin-symbolic"
|
||||
"kt-restore-defaults-symbolic"
|
||||
"user-symbolic"
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
|
||||
@@ -237,20 +237,6 @@ QQC2.Control {
|
||||
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd)
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: {
|
||||
if (selectedText.length > 0) {
|
||||
remove(selectionStart, selectionEnd);
|
||||
} else {
|
||||
remove(cursorPosition, cursorPosition + 1);
|
||||
}
|
||||
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||
root.currentRoom.sendTypingNotification(false);
|
||||
repeatTimer.stop();
|
||||
}
|
||||
if (quickFormatBar.visible) {
|
||||
quickFormatBar.close();
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
const controlIsPressed = event.modifiers & Qt.ControlModifier;
|
||||
if (completionMenu.visible) {
|
||||
@@ -289,7 +275,7 @@ QQC2.Control {
|
||||
completionMenu.decrementIndex();
|
||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||
completionMenu.incrementIndex();
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
} else if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) {
|
||||
if (textField.text == selectedText || textField.text.length <= 1) {
|
||||
root.currentRoom.sendTypingNotification(false);
|
||||
repeatTimer.stop();
|
||||
|
||||
@@ -168,7 +168,6 @@ void Controller::addConnection(NeoChatConnection *c)
|
||||
connect(c, &NeoChatConnection::syncDone, this, [this, c]() {
|
||||
m_notificationsManager.handleNotifications(c);
|
||||
});
|
||||
connect(c, &NeoChatConnection::showInviteNotification, &m_notificationsManager, &NotificationsManager::postInviteNotification);
|
||||
|
||||
c->sync();
|
||||
|
||||
|
||||
@@ -53,9 +53,13 @@ public:
|
||||
LinkPreview, /**< A preview of a URL in the message. */
|
||||
LinkPreviewLoad, /**< A loading dialog for a link preview. */
|
||||
ChatBar, /**< A text edit for editing a message. */
|
||||
ThreadRoot, /**< The root message of the thread. */
|
||||
ThreadBody, /**< The other messages in the thread. */
|
||||
ReplyButton, /**< A button to reply in the current thread. */
|
||||
FetchButton, /**< A button to fetch more messages in the current thread. */
|
||||
Verification, /**< A user verification session start message. */
|
||||
Loading, /**< The component is loading. */
|
||||
Separator, /**< A horizontal separator. */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(Type);
|
||||
|
||||
@@ -12,6 +12,7 @@ FileTransferPseudoJob::FileTransferPseudoJob(Operation operation, const QString
|
||||
, m_eventId(eventId)
|
||||
, m_operation(operation)
|
||||
{
|
||||
setCapabilities(KJob::Killable);
|
||||
}
|
||||
|
||||
void FileTransferPseudoJob::fileTransferProgress(const QString &id, qint64 progress, qint64 total)
|
||||
@@ -41,6 +42,15 @@ void FileTransferPseudoJob::fileTransferFailed(const QString &id, const QString
|
||||
emitResult();
|
||||
}
|
||||
|
||||
void FileTransferPseudoJob::fileTransferCanceled(const QString &id)
|
||||
{
|
||||
if (id != m_eventId) {
|
||||
return;
|
||||
}
|
||||
setError(KJob::KilledJobError);
|
||||
emitResult();
|
||||
}
|
||||
|
||||
void FileTransferPseudoJob::start()
|
||||
{
|
||||
setTotalAmount(Unit::Files, 1);
|
||||
@@ -49,3 +59,9 @@ void FileTransferPseudoJob::start()
|
||||
{i18nc("The URL being downloaded/uploaded", "Source"), m_path},
|
||||
{i18nc("The location being downloaded to", "Destination"), m_path});
|
||||
}
|
||||
|
||||
bool FileTransferPseudoJob::doKill()
|
||||
{
|
||||
Q_EMIT cancelRequested(m_eventId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
class FileTransferPseudoJob : public KJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Operation {
|
||||
Download,
|
||||
@@ -38,11 +39,22 @@ public:
|
||||
*/
|
||||
void fileTransferFailed(const QString &id, const QString &errorMessage = {});
|
||||
|
||||
/**
|
||||
* @brief Set the file transfer as canceled.
|
||||
*/
|
||||
void fileTransferCanceled(const QString &id);
|
||||
|
||||
/**
|
||||
* @brief Start the file transfer.
|
||||
*/
|
||||
void start() override;
|
||||
|
||||
protected:
|
||||
bool doKill() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void cancelRequested(const QString &id);
|
||||
|
||||
private:
|
||||
QString m_path;
|
||||
QString m_eventId;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// 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 std::optional<QJsonObject> &auth)
|
||||
: BaseJob(HttpVerb::Post, u"DisableDeviceJob"_s, "_matrix/client/v3/account/deactivate")
|
||||
{
|
||||
QJsonObject data;
|
||||
addParam<IfNotEmpty>(data, u"auth"_s, auth);
|
||||
setRequestData(data);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
|
||||
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
|
||||
{
|
||||
public:
|
||||
explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = {});
|
||||
};
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <KCrash>
|
||||
#endif
|
||||
|
||||
#include <KIconTheme>
|
||||
#include <KLocalizedContext>
|
||||
#include <KLocalizedString>
|
||||
|
||||
@@ -101,6 +102,7 @@ Q_DECL_EXPORT
|
||||
#endif
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
KIconTheme::initTheme();
|
||||
QNetworkProxyFactory::setUseSystemConfiguration(true);
|
||||
|
||||
#ifdef HAVE_WEBVIEW
|
||||
|
||||
@@ -136,7 +136,11 @@ QList<ActionsModel::Action> actions{
|
||||
Action{
|
||||
u"plain"_s,
|
||||
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
room->postText(text.toHtmlEscaped());
|
||||
#else
|
||||
room->postPlainText(text.toHtmlEscaped());
|
||||
#endif
|
||||
return QString();
|
||||
},
|
||||
std::nullopt,
|
||||
|
||||
74
src/models/commonroomsmodel.cpp
Normal file
74
src/models/commonroomsmodel.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "commonroomsmodel.h"
|
||||
#include "jobs/neochatgetcommonroomsjob.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
CommonRoomsModel::CommonRoomsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
NeoChatConnection *CommonRoomsModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void CommonRoomsModel::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
reload();
|
||||
}
|
||||
|
||||
QString CommonRoomsModel::userId() const
|
||||
{
|
||||
return m_userId;
|
||||
}
|
||||
|
||||
void CommonRoomsModel::setUserId(const QString &userId)
|
||||
{
|
||||
m_userId = userId;
|
||||
Q_EMIT userIdChanged();
|
||||
reload();
|
||||
}
|
||||
|
||||
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
Q_UNUSED(roleName)
|
||||
return {};
|
||||
}
|
||||
|
||||
int CommonRoomsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_commonRooms.size();
|
||||
}
|
||||
|
||||
void CommonRoomsModel::reload()
|
||||
{
|
||||
if (!m_connection || m_userId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_connection->canCheckMutualRooms()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
|
||||
const auto &replyData = job->jsonData();
|
||||
beginResetModel();
|
||||
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
|
||||
m_commonRooms.push_back(roomId.toString());
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
});
|
||||
}
|
||||
|
||||
#include "moc_commonroomsmodel.cpp"
|
||||
58
src/models/commonroomsmodel.h
Normal file
58
src/models/commonroomsmodel.h
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/roommember.h>
|
||||
|
||||
/**
|
||||
* @brief Model to show the common or mutual rooms between you and another user.
|
||||
*/
|
||||
class CommonRoomsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
|
||||
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
TextRole = Qt::DisplayRole,
|
||||
LongitudeRole,
|
||||
LatitudeRole,
|
||||
AssetRole,
|
||||
AuthorRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit CommonRoomsModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] NeoChatConnection *connection() const;
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
|
||||
[[nodiscard]] QString userId() const;
|
||||
void setUserId(const QString &userId);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
|
||||
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void userIdChanged();
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
void reload();
|
||||
|
||||
QPointer<NeoChatConnection> m_connection;
|
||||
QString m_userId;
|
||||
QList<QString> m_commonRooms;
|
||||
};
|
||||
@@ -39,14 +39,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date();
|
||||
return day != previousEventDay;
|
||||
}
|
||||
// Catch and force the author to be shown for all rows
|
||||
if (role == TimelineMessageModel::ContentModelRole) {
|
||||
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
|
||||
if (model != nullptr) {
|
||||
model->setShowAuthor(true);
|
||||
}
|
||||
return QVariant::fromValue<MessageContentModel *>(model);
|
||||
}
|
||||
|
||||
QVariantMap mediaInfo = mapToSource(index).data(TimelineMessageModel::MediaInfoRole).toMap();
|
||||
|
||||
|
||||
37
src/models/messagecontentfiltermodel.cpp
Normal file
37
src/models/messagecontentfiltermodel.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "messagecontentfiltermodel.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
|
||||
MessageContentFilterModel::MessageContentFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool MessageContentFilterModel::showAuthor() const
|
||||
{
|
||||
return m_showAuthor;
|
||||
}
|
||||
|
||||
void MessageContentFilterModel::setShowAuthor(bool showAuthor)
|
||||
{
|
||||
if (showAuthor == m_showAuthor) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_showAuthor = showAuthor;
|
||||
Q_EMIT showAuthorChanged();
|
||||
}
|
||||
|
||||
bool MessageContentFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
if (m_showAuthor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto index = sourceModel()->index(source_row, 0, source_parent);
|
||||
auto contentType = static_cast<MessageComponentType::Type>(index.data(MessageContentModel::ComponentTypeRole).toInt());
|
||||
return contentType != MessageComponentType::Author;
|
||||
}
|
||||
43
src/models/messagecontentfiltermodel.h
Normal file
43
src/models/messagecontentfiltermodel.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class MessageContentFilterModel
|
||||
*
|
||||
* This model filters a message's contents.
|
||||
*/
|
||||
class MessageContentFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief Whether the author component should be shown.
|
||||
*/
|
||||
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
||||
|
||||
public:
|
||||
explicit MessageContentFilterModel(QObject *parent = nullptr);
|
||||
|
||||
bool showAuthor() const;
|
||||
void setShowAuthor(bool showAuthor);
|
||||
|
||||
Q_SIGNALS:
|
||||
void showAuthorChanged();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Whether a row should be shown out or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
private:
|
||||
bool m_showAuthor = true;
|
||||
};
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
#include <Quotient/thread.h>
|
||||
#endif
|
||||
|
||||
@@ -135,14 +135,14 @@ void MessageContentModel::initializeModel()
|
||||
});
|
||||
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr) {
|
||||
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
|
||||
if (senderId().isEmpty() || senderId() == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr) {
|
||||
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
|
||||
if (senderId().isEmpty() || senderId() == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
@@ -178,16 +178,6 @@ void MessageContentModel::initializeEvent()
|
||||
} else {
|
||||
m_currentState = Available;
|
||||
}
|
||||
|
||||
if (m_eventSenderObject == nullptr) {
|
||||
auto senderId = eventResult.first->senderId();
|
||||
// A pending event might not have a sender ID set yet but in that case it must
|
||||
// be the local member.
|
||||
if (senderId.isEmpty()) {
|
||||
senderId = m_room->localMember().id();
|
||||
}
|
||||
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
|
||||
}
|
||||
Q_EMIT eventUpdated();
|
||||
}
|
||||
|
||||
@@ -218,30 +208,29 @@ void MessageContentModel::getEvent()
|
||||
m_room->downloadEventFromServer(m_eventId);
|
||||
}
|
||||
|
||||
bool MessageContentModel::showAuthor() const
|
||||
QString MessageContentModel::senderId() const
|
||||
{
|
||||
return m_showAuthor;
|
||||
const auto eventResult = m_room->getEvent(m_eventId);
|
||||
if (eventResult.first == nullptr) {
|
||||
return {};
|
||||
}
|
||||
auto senderId = eventResult.first->senderId();
|
||||
if (senderId.isEmpty()) {
|
||||
senderId = m_room->localMember().id();
|
||||
}
|
||||
return senderId;
|
||||
}
|
||||
|
||||
void MessageContentModel::setShowAuthor(bool showAuthor)
|
||||
NeochatRoomMember *MessageContentModel::senderObject() const
|
||||
{
|
||||
if (showAuthor == m_showAuthor) {
|
||||
return;
|
||||
const auto eventResult = m_room->getEvent(m_eventId);
|
||||
if (eventResult.first == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
m_showAuthor = showAuthor;
|
||||
|
||||
if (m_room->connection()->isIgnored(m_eventSenderId)) {
|
||||
if (showAuthor) {
|
||||
beginInsertRows({}, 0, 0);
|
||||
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
|
||||
endInsertRows();
|
||||
} else {
|
||||
beginRemoveRows({}, 0, 0);
|
||||
m_components.remove(0, 1);
|
||||
endRemoveRows();
|
||||
}
|
||||
if (eventResult.first->senderId().isEmpty()) {
|
||||
return m_room->qmlSafeMember(m_room->localMember().id());
|
||||
}
|
||||
Q_EMIT showAuthorChanged();
|
||||
return m_room->qmlSafeMember(eventResult.first->senderId());
|
||||
}
|
||||
|
||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||
@@ -275,7 +264,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
|
||||
if (role == DisplayRole) {
|
||||
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(m_eventSenderId)) {
|
||||
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(senderId())) {
|
||||
Kirigami::Platform::PlatformTheme *theme =
|
||||
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
|
||||
@@ -317,7 +306,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending);
|
||||
}
|
||||
if (role == AuthorRole) {
|
||||
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
|
||||
return QVariant::fromValue<NeochatRoomMember *>(senderObject());
|
||||
}
|
||||
if (role == MediaInfoRole) {
|
||||
return EventHandler::mediaInfo(m_room, event.first);
|
||||
@@ -351,6 +340,17 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
if (role == ReplyContentModelRole) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
||||
}
|
||||
if (role == ThreadRootRole) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first);
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
|
||||
#else
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
#endif
|
||||
return roomMessageEvent->threadRootEventId();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (role == LinkPreviewerRole) {
|
||||
if (component.type == MessageComponentType::LinkPreview) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(
|
||||
@@ -377,27 +377,32 @@ int MessageContentModel::rowCount(const QModelIndex &parent) const
|
||||
|
||||
QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||
roles[DisplayRole] = "display";
|
||||
roles[ComponentTypeRole] = "componentType";
|
||||
roles[ComponentAttributesRole] = "componentAttributes";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
roles[TimeStringRole] = "timeString";
|
||||
roles[AuthorRole] = "author";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[FileTransferInfoRole] = "fileTransferInfo";
|
||||
roles[ItineraryModelRole] = "itineraryModel";
|
||||
roles[LatitudeRole] = "latitude";
|
||||
roles[LongitudeRole] = "longitude";
|
||||
roles[AssetRole] = "asset";
|
||||
roles[PollHandlerRole] = "pollHandler";
|
||||
roles[ReplyEventIdRole] = "replyEventId";
|
||||
roles[ReplyAuthorRole] = "replyAuthor";
|
||||
roles[ReplyContentModelRole] = "replyContentModel";
|
||||
roles[ThreadRootRole] = "threadRoot";
|
||||
roles[LinkPreviewerRole] = "linkPreviewer";
|
||||
roles[ChatBarCacheRole] = "chatBarCache";
|
||||
return roleNamesStatic();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[MessageContentModel::DisplayRole] = "display";
|
||||
roles[MessageContentModel::ComponentTypeRole] = "componentType";
|
||||
roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes";
|
||||
roles[MessageContentModel::EventIdRole] = "eventId";
|
||||
roles[MessageContentModel::TimeRole] = "time";
|
||||
roles[MessageContentModel::TimeStringRole] = "timeString";
|
||||
roles[MessageContentModel::AuthorRole] = "author";
|
||||
roles[MessageContentModel::MediaInfoRole] = "mediaInfo";
|
||||
roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo";
|
||||
roles[MessageContentModel::ItineraryModelRole] = "itineraryModel";
|
||||
roles[MessageContentModel::LatitudeRole] = "latitude";
|
||||
roles[MessageContentModel::LongitudeRole] = "longitude";
|
||||
roles[MessageContentModel::AssetRole] = "asset";
|
||||
roles[MessageContentModel::PollHandlerRole] = "pollHandler";
|
||||
roles[MessageContentModel::ReplyEventIdRole] = "replyEventId";
|
||||
roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor";
|
||||
roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel";
|
||||
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
|
||||
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
|
||||
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -406,7 +411,7 @@ void MessageContentModel::resetModel()
|
||||
beginResetModel();
|
||||
m_components.clear();
|
||||
|
||||
if (m_room->connection()->isIgnored(m_eventSenderId) || m_currentState == UnAvailable) {
|
||||
if (m_room->connection()->isIgnored(senderId()) || m_currentState == UnAvailable) {
|
||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
endResetModel();
|
||||
return;
|
||||
@@ -419,9 +424,7 @@ void MessageContentModel::resetModel()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_showAuthor) {
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
}
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
|
||||
m_components += messageContentComponents();
|
||||
endResetModel();
|
||||
@@ -477,8 +480,18 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
||||
newComponents = addLinkPreviews(newComponents);
|
||||
}
|
||||
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
|
||||
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||
#else
|
||||
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||
#endif
|
||||
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
|
||||
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
|
||||
}
|
||||
|
||||
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
|
||||
#else
|
||||
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
|
||||
@@ -25,11 +25,6 @@ class MessageContentModel : public QAbstractListModel
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief Whether the author component is being shown.
|
||||
*/
|
||||
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
||||
|
||||
public:
|
||||
enum MessageState {
|
||||
Unknown, /**< The message state is unknown. */
|
||||
@@ -75,9 +70,6 @@ public:
|
||||
bool isPending = false,
|
||||
MessageContentModel *parent = nullptr);
|
||||
|
||||
bool showAuthor() const;
|
||||
void setShowAuthor(bool showAuthor);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
@@ -99,6 +91,8 @@ public:
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
static QHash<int, QByteArray> roleNamesStatic();
|
||||
|
||||
/**
|
||||
* @brief Close the link preview at the given index.
|
||||
*
|
||||
@@ -113,11 +107,10 @@ Q_SIGNALS:
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QString m_eventId;
|
||||
QString m_eventSenderId;
|
||||
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
|
||||
QString senderId() const;
|
||||
NeochatRoomMember *senderObject() const;
|
||||
|
||||
MessageState m_currentState = Unknown;
|
||||
bool m_showAuthor = true;
|
||||
bool m_isReply;
|
||||
|
||||
void initializeModel();
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
#include "enums/delegatetype.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroommember.h"
|
||||
#include "timelinemessagemodel.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent, QAbstractItemModel *sourceModel)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
Q_ASSERT(sourceModel);
|
||||
@@ -93,12 +92,8 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
|
||||
return authorList(mapToSource(index).row());
|
||||
} else if (role == ExcessAuthorsRole) {
|
||||
return excessAuthors(mapToSource(index).row());
|
||||
} else if (role == TimelineMessageModel::ContentModelRole) {
|
||||
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
|
||||
if (model != nullptr && !showAuthor(index)) {
|
||||
model->setShowAuthor(false);
|
||||
}
|
||||
return QVariant::fromValue<MessageContentModel *>(model);
|
||||
} else if (role == MessageModel::ShowAuthorRole) {
|
||||
return showAuthor(index);
|
||||
}
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, TimelineModel *sourceModel = nullptr);
|
||||
explicit MessageFilterModel(QObject *parent = nullptr, QAbstractItemModel *sourceModel = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function to remove hidden messages.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
#include <Quotient/thread.h>
|
||||
#endif
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "eventhandler.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroommember.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -54,6 +55,9 @@ void MessageModel::setRoom(NeoChatRoom *room)
|
||||
|
||||
beginResetModel();
|
||||
m_room = room;
|
||||
if (m_room != nullptr) {
|
||||
m_room->setVisible(true);
|
||||
}
|
||||
Q_EMIT roomChanged();
|
||||
endResetModel();
|
||||
}
|
||||
@@ -116,16 +120,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ContentModelRole) {
|
||||
QString modelId;
|
||||
if (!event->get().id().isEmpty() && m_contentModels.contains(event->get().id())) {
|
||||
modelId = event.value().get().id();
|
||||
} else if (!event.value().get().transactionId().isEmpty() && m_contentModels.contains(event.value().get().transactionId())) {
|
||||
modelId = event.value().get().transactionId();
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(roomMessageEvent->threadRootEventId()));
|
||||
}
|
||||
if (!modelId.isEmpty()) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
|
||||
}
|
||||
return {};
|
||||
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(&event->get()));
|
||||
}
|
||||
|
||||
if (role == GenericDisplayRole) {
|
||||
@@ -144,11 +143,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
mId = event.value().get().senderId();
|
||||
}
|
||||
|
||||
if (!m_memberObjects.contains(mId)) {
|
||||
if (!m_room->isMember(mId)) {
|
||||
return QVariant::fromValue<NeochatRoomMember *>(emptyNeochatRoomMember);
|
||||
}
|
||||
|
||||
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(mId).get());
|
||||
return QVariant::fromValue<NeochatRoomMember *>(m_room->qmlSafeMember(mId));
|
||||
}
|
||||
|
||||
if (role == HighlightRole) {
|
||||
@@ -174,7 +173,7 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
|
||||
#if Quotient_VERSION_MINOR > 9
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(event.value().get().id()))) {
|
||||
const auto &thread = m_room->threads().value(roomMessageEvent->isThreaded() ? roomMessageEvent->threadRootEventId() : event.value().get().id());
|
||||
if (thread.latestEventId != event.value().get().id()) {
|
||||
@@ -197,13 +196,15 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (role == ProgressInfoRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&event.value().get())) {
|
||||
if (e->has<EventContent::FileContent>()) {
|
||||
if (e->has<EventContent::FileContent>() || e->has<EventContent::ImageContent>() || e->has<EventContent::VideoContent>()
|
||||
|| e->has<EventContent::AudioContent>()) {
|
||||
return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get()));
|
||||
}
|
||||
}
|
||||
if (eventCast<const StickerEvent>(&event.value().get())) {
|
||||
return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == TimeRole) {
|
||||
@@ -299,6 +300,10 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
&& event.value().get().senderId() == m_room->localMember().id();
|
||||
}
|
||||
|
||||
if (role == ShowAuthorRole) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -328,6 +333,7 @@ QHash<int, QByteArray> MessageModel::roleNames() const
|
||||
roles[ContentModelRole] = "contentModel";
|
||||
roles[MediaInfoRole] = "mediaInfo";
|
||||
roles[IsEditableRole] = "isEditable";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -424,21 +430,6 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
|
||||
senderId = m_room->localMember().id();
|
||||
}
|
||||
|
||||
if (!m_memberObjects.contains(senderId)) {
|
||||
m_memberObjects[senderId] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
|
||||
}
|
||||
|
||||
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
|
||||
if (!event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
||||
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_room, eventId, false, isPending));
|
||||
}
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) {
|
||||
m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room));
|
||||
}
|
||||
|
||||
// ReadMarkerModel handles updates to add and remove markers, we only need to
|
||||
// handle adding and removing whole models here.
|
||||
if (m_readMarkerModels.contains(eventId)) {
|
||||
@@ -493,12 +484,17 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
|
||||
void MessageModel::clearModel()
|
||||
{
|
||||
if (m_room) {
|
||||
const auto oldRoom = m_room;
|
||||
|
||||
// HACK: Reset the model to a null room first to make sure QML dismantles
|
||||
// last room's objects before the room is actually changed
|
||||
beginResetModel();
|
||||
m_room->disconnect(this);
|
||||
m_room = nullptr;
|
||||
endResetModel();
|
||||
|
||||
// Because we don't want any of the object deleted before the model is cleared.
|
||||
oldRoom->setVisible(false);
|
||||
}
|
||||
|
||||
// Don't clear the member objects until the model has been fully reset and all
|
||||
@@ -508,8 +504,6 @@ void MessageModel::clearModel()
|
||||
|
||||
void MessageModel::clearEventObjects()
|
||||
{
|
||||
m_memberObjects.clear();
|
||||
m_contentModels.clear();
|
||||
m_reactionModels.clear();
|
||||
m_readMarkerModels.clear();
|
||||
}
|
||||
@@ -524,7 +518,7 @@ bool MessageModel::event(QEvent *event)
|
||||
|
||||
ThreadModel *MessageModel::threadModelForRootId(const QString &threadRootId) const
|
||||
{
|
||||
return m_threadModels[threadRootId].data();
|
||||
return m_room->modelForThread(threadRootId);
|
||||
}
|
||||
|
||||
#include "moc_messagemodel.cpp"
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
#include <QQmlEngine>
|
||||
#include <functional>
|
||||
|
||||
#include "messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatroommember.h"
|
||||
#include "pollhandler.h"
|
||||
#include "readmarkermodel.h"
|
||||
#include "threadmodel.h"
|
||||
@@ -19,11 +17,27 @@ class ReactionModel;
|
||||
/**
|
||||
* @class MessageModel
|
||||
*
|
||||
* This class defines a model for visualising the room events.
|
||||
* This class defines a base model for visualising the room events.
|
||||
*
|
||||
* This model covers all event types in the room with many of the roles being
|
||||
* specific to a subset of events. This means the user needs to understand which
|
||||
* roles will return useful information for a given event type.
|
||||
* On its own MessageModel will result in an empty timeline as there is no mechanism
|
||||
* to retrieve events. This is by design as it allows the model to be inherited from
|
||||
* and the user can specify their own source of events, e.g. a room timeline or a
|
||||
* search result.
|
||||
*
|
||||
* The inherited model MUST do the following:
|
||||
* - Define methods for retrieving events
|
||||
* - Call newEventAdded() for each new event in the model so that all the required
|
||||
* event objects are created.
|
||||
* - Override getEventForIndex() so that the data() function can get an event for a
|
||||
* given index
|
||||
* - Override rowCount()
|
||||
*
|
||||
* Optionally the new model can:
|
||||
* - override timelineServerIndex() if dealing with pending events otherwise the default
|
||||
* function returns 0 which is correct for other use cases.
|
||||
* - m_lastReadEventIndex is available to track a read marker location (so that the
|
||||
* data function can output the appropriate values). The new class must implement
|
||||
* the functionality to add, move, remove, etc though.
|
||||
*
|
||||
* @sa NeoChatRoom
|
||||
*/
|
||||
@@ -31,6 +45,7 @@ class MessageModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The current room that the model is getting its messages from.
|
||||
@@ -70,6 +85,7 @@ public:
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||
IsEditableRole, /**< Whether the event can be edited by the user. */
|
||||
ShowAuthorRole, /**< Whether the author of a message should be shown. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
@@ -98,10 +114,23 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
|
||||
/**
|
||||
* @brief Get a ThreadModel for the give thread root Matrix ID.
|
||||
*/
|
||||
Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* @brief Emitted when the room is changed.
|
||||
*/
|
||||
void roomChanged();
|
||||
|
||||
/**
|
||||
* @brief A signal to tell the MessageModel that a new event has been added.
|
||||
*
|
||||
* Any model inheriting from MessageModel needs to emit this signal for every
|
||||
* new event it adds.
|
||||
*/
|
||||
void newEventAdded(const Quotient::RoomEvent *event, bool isPending = false);
|
||||
|
||||
protected:
|
||||
@@ -125,10 +154,7 @@ private:
|
||||
bool resetting = false;
|
||||
bool movingEvent = false;
|
||||
|
||||
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
||||
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
|
||||
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
|
||||
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false);
|
||||
|
||||
67
src/models/pinnedmessagemodel.cpp
Normal file
67
src/models/pinnedmessagemodel.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "pinnedmessagemodel.h"
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "eventhandler.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
PinnedMessageModel::PinnedMessageModel(QObject *parent)
|
||||
: MessageModel(parent)
|
||||
{
|
||||
connect(this, &MessageModel::roomChanged, this, &PinnedMessageModel::fill);
|
||||
}
|
||||
|
||||
bool PinnedMessageModel::loading() const
|
||||
{
|
||||
return m_loading;
|
||||
}
|
||||
|
||||
int PinnedMessageModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_pinnedEvents.size();
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> PinnedMessageModel::getEventForIndex(const QModelIndex index) const
|
||||
{
|
||||
if (static_cast<size_t>(index.row()) >= m_pinnedEvents.size() || index.row() < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::reference_wrapper{*m_pinnedEvents[index.row()].get()};
|
||||
}
|
||||
|
||||
void PinnedMessageModel::setLoading(bool loading)
|
||||
{
|
||||
m_loading = loading;
|
||||
Q_EMIT loadingChanged();
|
||||
}
|
||||
|
||||
void PinnedMessageModel::fill()
|
||||
{
|
||||
if (!m_room) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto events = m_room->pinnedEventIds();
|
||||
|
||||
for (const auto &event : std::as_const(events)) {
|
||||
auto job = m_room->connection()->callApi<GetOneRoomEventJob>(m_room->id(), event);
|
||||
connect(job, &BaseJob::success, this, [this, job] {
|
||||
beginInsertRows({}, m_pinnedEvents.size(), m_pinnedEvents.size());
|
||||
m_pinnedEvents.push_back(std::move(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData())));
|
||||
Q_EMIT newEventAdded(m_pinnedEvents.back().get(), false);
|
||||
endInsertRows();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_pinnedmessagemodel.cpp"
|
||||
62
src/models/pinnedmessagemodel.h
Normal file
62
src/models/pinnedmessagemodel.h
Normal file
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
|
||||
#include <Quotient/csapi/rooms.h>
|
||||
|
||||
#include "messagemodel.h"
|
||||
#include "neochatroommember.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class Connection;
|
||||
}
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class PinnedMessageModel
|
||||
*
|
||||
* This class defines the model for visualising a room's pinned messages.
|
||||
*/
|
||||
class PinnedMessageModel : public MessageModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief Whether the model is currently loading.
|
||||
*/
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
|
||||
public:
|
||||
explicit PinnedMessageModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool loading() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void loadingChanged();
|
||||
|
||||
protected:
|
||||
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
|
||||
|
||||
private:
|
||||
void setLoading(bool loading);
|
||||
void fill();
|
||||
|
||||
bool m_loading = false;
|
||||
|
||||
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_pinnedEvents;
|
||||
};
|
||||
@@ -11,18 +11,18 @@
|
||||
#include "chatbarcache.h"
|
||||
#include "eventhandler.h"
|
||||
#include "messagecomponenttype.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
||||
: QConcatenateTablesProxyModel(room)
|
||||
, m_threadRootId(threadRootId)
|
||||
, m_threadFetchModel(new ThreadFetchModel(this))
|
||||
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
|
||||
{
|
||||
Q_ASSERT(!m_threadRootId.isEmpty());
|
||||
Q_ASSERT(room);
|
||||
|
||||
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
|
||||
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0)
|
||||
connect(room, &Quotient::Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
|
||||
#else
|
||||
@@ -49,7 +49,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
||||
// If the thread was created by the local user fetchMore() won't find the current
|
||||
// pending event.
|
||||
checkPending();
|
||||
fetchMore({});
|
||||
fetchMoreEvents(3);
|
||||
addModels();
|
||||
}
|
||||
|
||||
@@ -73,34 +73,28 @@ QString ThreadModel::threadRootId() const
|
||||
return m_threadRootId;
|
||||
}
|
||||
|
||||
MessageContentModel *ThreadModel::threadRootContentModel() const
|
||||
{
|
||||
return m_threadRootContentModel.get();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ThreadModel::roleNames() const
|
||||
{
|
||||
return m_threadRootContentModel->roleNames();
|
||||
return MessageContentModel::roleNamesStatic();
|
||||
}
|
||||
|
||||
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
|
||||
bool ThreadModel::moreEventsAvailable(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return !m_currentJob && m_nextBatch.has_value();
|
||||
}
|
||||
|
||||
void ThreadModel::fetchMore(const QModelIndex &parent)
|
||||
void ThreadModel::fetchMoreEvents(int max)
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
if (!m_currentJob && m_nextBatch.has_value()) {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
const auto connection = room->connection();
|
||||
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5);
|
||||
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), max);
|
||||
Q_EMIT moreEventsAvailableChanged();
|
||||
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
auto newEvents = m_currentJob->chunk();
|
||||
for (auto &event : newEvents) {
|
||||
m_contentModels.push_back(new MessageContentModel(room, event->id()));
|
||||
m_events.push_back(event->id());
|
||||
}
|
||||
|
||||
addModels();
|
||||
@@ -116,18 +110,18 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
|
||||
}
|
||||
|
||||
m_currentJob.clear();
|
||||
Q_EMIT moreEventsAvailableChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
auto eventId = event->id();
|
||||
if (eventId.isEmpty()) {
|
||||
eventId = event->transactionId();
|
||||
}
|
||||
m_contentModels.push_front(new MessageContentModel(room, eventId));
|
||||
m_events.push_front(eventId);
|
||||
}
|
||||
|
||||
void ThreadModel::addModels()
|
||||
@@ -136,9 +130,16 @@ void ThreadModel::addModels()
|
||||
clearModels();
|
||||
}
|
||||
|
||||
addSourceModel(m_threadRootContentModel.get());
|
||||
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
|
||||
addSourceModel(*it);
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
if (room == nullptr) {
|
||||
return;
|
||||
}
|
||||
addSourceModel(m_threadFetchModel);
|
||||
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
|
||||
const auto contentModel = room->contentModelForEvent(*it);
|
||||
if (contentModel != nullptr) {
|
||||
addSourceModel(room->contentModelForEvent(*it));
|
||||
}
|
||||
}
|
||||
addSourceModel(m_threadChatBarModel);
|
||||
|
||||
@@ -148,15 +149,90 @@ void ThreadModel::addModels()
|
||||
|
||||
void ThreadModel::clearModels()
|
||||
{
|
||||
removeSourceModel(m_threadRootContentModel.get());
|
||||
for (const auto &model : m_contentModels) {
|
||||
if (sourceModels().contains(model)) {
|
||||
removeSourceModel(model);
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
if (room == nullptr) {
|
||||
return;
|
||||
}
|
||||
removeSourceModel(m_threadFetchModel);
|
||||
for (const auto &model : m_events) {
|
||||
const auto contentModel = room->contentModelForEvent(model);
|
||||
if (sourceModels().contains(contentModel)) {
|
||||
removeSourceModel(contentModel);
|
||||
}
|
||||
}
|
||||
removeSourceModel(m_threadChatBarModel);
|
||||
}
|
||||
|
||||
void ThreadModel::closeLinkPreview(int row)
|
||||
{
|
||||
if (row < 0 || row >= rowCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = this->index(row, 0);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sourceIndex = mapToSource(index);
|
||||
const auto sourceModel = sourceIndex.model();
|
||||
if (sourceModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
// This is a bit silly but we can only get a const reference to the model from the
|
||||
// index so we need to search the source models.
|
||||
for (const auto &model : sourceModels()) {
|
||||
if (model == sourceModel) {
|
||||
const auto sourceContentModel = dynamic_cast<MessageContentModel *>(model);
|
||||
if (sourceContentModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
sourceContentModel->closeLinkPreview(sourceIndex.row());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThreadFetchModel::ThreadFetchModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
const auto threadModel = dynamic_cast<ThreadModel *>(parent);
|
||||
Q_ASSERT(threadModel != nullptr);
|
||||
connect(threadModel, &ThreadModel::moreEventsAvailableChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
QVariant ThreadFetchModel::data(const QModelIndex &idx, int role) const
|
||||
{
|
||||
if (idx.row() < 0 || idx.row() > 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == ComponentTypeRole) {
|
||||
return MessageComponentType::FetchButton;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int ThreadFetchModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
|
||||
if (threadModel == nullptr) {
|
||||
qWarning() << "ThreadFetchModel created with incorrect parent, a ThreadModel must be set as the parent on creation.";
|
||||
return {};
|
||||
}
|
||||
return threadModel->moreEventsAvailable({}) ? 1 : 0;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ThreadFetchModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ComponentTypeRole, "componentType"},
|
||||
};
|
||||
}
|
||||
|
||||
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
|
||||
: QAbstractListModel(parent)
|
||||
, m_room(room)
|
||||
|
||||
@@ -21,6 +21,52 @@
|
||||
class NeoChatRoom;
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class ThreadFetchModel
|
||||
*
|
||||
* A model to provide a fetch more historical messages button in a thread.
|
||||
*/
|
||||
class ThreadFetchModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*
|
||||
* The role values need to match MessageContentModel not to blow up.
|
||||
*
|
||||
* @sa MessageContentModel
|
||||
*/
|
||||
enum Roles {
|
||||
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit ThreadFetchModel(QObject *parent);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief 1 or 0, depending on whether there are more messages to download.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a map with ComponentTypeRole it's the only one.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class ThreadChatBarModel
|
||||
*
|
||||
@@ -91,11 +137,6 @@ public:
|
||||
|
||||
QString threadRootId() const;
|
||||
|
||||
/**
|
||||
* @brief The content model for the thread root event.
|
||||
*/
|
||||
MessageContentModel *threadRootContentModel() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
@@ -104,32 +145,33 @@ public:
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Whether there is more data available for the model to fetch.
|
||||
*
|
||||
* @sa QAbstractItemModel::canFetchMore()
|
||||
* @brief Whether there are more events for the model to fetch.
|
||||
*/
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
bool moreEventsAvailable(const QModelIndex &parent) const;
|
||||
|
||||
/**
|
||||
* @brief Fetches the next batch of model data if any is available.
|
||||
*
|
||||
* @sa QAbstractItemModel::fetchMore()
|
||||
* @brief Fetches the next batch of events if any is available.
|
||||
*/
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
Q_INVOKABLE void fetchMoreEvents(int max = 5);
|
||||
|
||||
/**
|
||||
* @brief Close the link preview at the given index.
|
||||
*
|
||||
* If the given index is not a link preview component, nothing happens.
|
||||
*/
|
||||
Q_INVOKABLE void closeLinkPreview(int row);
|
||||
|
||||
Q_SIGNALS:
|
||||
void moreEventsAvailableChanged();
|
||||
|
||||
private:
|
||||
QString m_threadRootId;
|
||||
QPointer<MessageContentModel> m_threadRootContentModel;
|
||||
|
||||
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
|
||||
|
||||
std::deque<MessageContentModel *> m_contentModels;
|
||||
std::deque<QString> m_events;
|
||||
ThreadFetchModel *m_threadFetchModel;
|
||||
ThreadChatBarModel *m_threadChatBarModel;
|
||||
|
||||
QList<QString> m_events;
|
||||
QList<QString> m_pendingEvents;
|
||||
|
||||
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
|
||||
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;
|
||||
|
||||
@@ -27,24 +27,20 @@ void TimelineMessageModel::connectNewRoom()
|
||||
}
|
||||
|
||||
connect(m_room, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &&event : events) {
|
||||
Q_EMIT newEventAdded(event.get());
|
||||
}
|
||||
m_initialized = true;
|
||||
beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1);
|
||||
});
|
||||
connect(m_room, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||
for (auto &event : events) {
|
||||
Q_EMIT newEventAdded(event.get());
|
||||
}
|
||||
if (rowCount() > 0) {
|
||||
rowBelowInserted = rowCount() - 1; // See #312
|
||||
}
|
||||
m_initialized = true;
|
||||
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
|
||||
});
|
||||
connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) {
|
||||
if (m_initialized) {
|
||||
for (int i = lowest; i == biggest; ++i) {
|
||||
const auto event = m_room->findInTimeline(i)->event();
|
||||
Q_EMIT newEventAdded(event);
|
||||
}
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
if (!m_lastReadEventIndex.isValid()) {
|
||||
|
||||
@@ -174,8 +174,8 @@ void UserListModel::refreshAllMembers()
|
||||
m_members = m_currentRoom->joinedMemberIds();
|
||||
MemberSorter sorter;
|
||||
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
|
||||
const auto leftPl = m_currentRoom->getUserPowerLevel(left);
|
||||
const auto rightPl = m_currentRoom->getUserPowerLevel(right);
|
||||
const auto leftPl = m_currentRoom->memberEffectivePowerLevel(left);
|
||||
const auto rightPl = m_currentRoom->memberEffectivePowerLevel(right);
|
||||
if (leftPl > rightPl) {
|
||||
return true;
|
||||
} else if (rightPl > leftPl) {
|
||||
@@ -184,7 +184,6 @@ void UserListModel::refreshAllMembers()
|
||||
|
||||
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
|
||||
});
|
||||
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT usersRefreshed();
|
||||
|
||||
@@ -39,7 +39,7 @@ struct WebShortcutModelPrivate;
|
||||
* }
|
||||
* QQC2.MenuSeparator {}
|
||||
* QQC2.MenuItem {
|
||||
* text: i18n("Configure Web Shortcuts...")
|
||||
* text: i18n("Configure Web Shortcuts…")
|
||||
* icon.name: "configure"
|
||||
* onTriggered: webshortcutmodel.configureWebShortcuts()
|
||||
* }
|
||||
|
||||
@@ -223,6 +223,10 @@
|
||||
<label>Reject unknown invites</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="PreferUsingEncryption" type="bool">
|
||||
<label>Prefer encrypting chats</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <QImageReader>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "jobs/neochatdeactivateaccountjob.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "spacehierarchycache.h"
|
||||
@@ -21,7 +20,6 @@
|
||||
#include <Quotient/csapi/profile.h>
|
||||
#include <Quotient/csapi/registration.h>
|
||||
#include <Quotient/csapi/versions.h>
|
||||
#include <Quotient/database.h>
|
||||
#include <Quotient/jobs/downloadfilejob.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#include <Quotient/room.h>
|
||||
@@ -109,10 +107,6 @@ void NeoChatConnection::connectSignals()
|
||||
Q_EMIT homeHaveHighlightNotificationsChanged();
|
||||
});
|
||||
});
|
||||
connect(this, &NeoChatConnection::invitedRoom, this, [this](Quotient::Room *room) {
|
||||
auto r = dynamic_cast<NeoChatRoom *>(room);
|
||||
connect(r, &NeoChatRoom::showInviteNotification, this, &NeoChatConnection::showInviteNotification);
|
||||
});
|
||||
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
|
||||
Q_UNUSED(room)
|
||||
if (prev && prev->isDirectChat()) {
|
||||
@@ -142,9 +136,15 @@ void NeoChatConnection::connectSignals()
|
||||
connect(job, &GetVersionsJob::success, this, [this, job] {
|
||||
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_L1);
|
||||
Q_EMIT canCheckMutualRoomsChanged();
|
||||
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
|
||||
Q_EMIT canEraseDataChanged();
|
||||
});
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] {
|
||||
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
|
||||
});
|
||||
}
|
||||
|
||||
int NeoChatConnection::badgeNotificationCount() const
|
||||
@@ -255,20 +255,20 @@ QString NeoChatConnection::label() const
|
||||
return accountDataJson("org.kde.neochat.account_label"_L1)["account_label"_L1].toString();
|
||||
}
|
||||
|
||||
void NeoChatConnection::deactivateAccount(const QString &password)
|
||||
void NeoChatConnection::deactivateAccount(const QString &password, const bool erase)
|
||||
{
|
||||
auto job = callApi<NeoChatDeactivateAccountJob>();
|
||||
connect(job, &BaseJob::result, this, [this, job, password] {
|
||||
auto job = callApi<DeactivateAccountJob>();
|
||||
connect(job, &BaseJob::result, this, [this, job, password, erase] {
|
||||
if (job->error() == 103) {
|
||||
QJsonObject replyData = job->jsonData();
|
||||
QJsonObject authData;
|
||||
authData["session"_L1] = replyData["session"_L1];
|
||||
authData["password"_L1] = password;
|
||||
authData["type"_L1] = "m.login.password"_L1;
|
||||
authData["user"_L1] = user()->id();
|
||||
AuthenticationData authData;
|
||||
authData.session = replyData["session"_L1].toString();
|
||||
authData.authInfo["password"_L1] = password;
|
||||
authData.type = "m.login.password"_L1;
|
||||
authData.authInfo["user"_L1] = user()->id();
|
||||
QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
|
||||
authData["identifier"_L1] = identifier;
|
||||
auto innerJob = callApi<NeoChatDeactivateAccountJob>(authData);
|
||||
authData.authInfo["identifier"_L1] = identifier;
|
||||
auto innerJob = callApi<DeactivateAccountJob>(authData, QString{}, erase);
|
||||
connect(innerJob, &BaseJob::success, this, [this]() {
|
||||
logout(false);
|
||||
});
|
||||
@@ -481,23 +481,6 @@ QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
|
||||
#endif
|
||||
}
|
||||
|
||||
QString NeoChatConnection::deviceKey() const
|
||||
{
|
||||
return edKeyForUserDevice(userId(), deviceId());
|
||||
}
|
||||
|
||||
QString NeoChatConnection::encryptionKey() const
|
||||
{
|
||||
auto query = database()->prepareQuery(u"SELECT curveKey FROM tracked_devices WHERE matrixId=:matrixId AND deviceid=:deviceId LIMIT 1;"_s);
|
||||
query.bindValue(u":matrixId"_s, userId());
|
||||
query.bindValue(u":deviceId"_s, deviceId());
|
||||
database()->execute(query);
|
||||
if (!query.next()) {
|
||||
return {};
|
||||
}
|
||||
return query.value(0).toString();
|
||||
}
|
||||
|
||||
bool NeoChatConnection::isOnline() const
|
||||
{
|
||||
return m_isOnline;
|
||||
@@ -548,4 +531,9 @@ KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphra
|
||||
return KeyImport::Success;
|
||||
}
|
||||
|
||||
bool NeoChatConnection::canEraseData() const
|
||||
{
|
||||
return m_canEraseData;
|
||||
}
|
||||
|
||||
#include "moc_neochatconnection.cpp"
|
||||
|
||||
@@ -31,8 +31,6 @@ class NeoChatConnection : public Quotient::Connection
|
||||
* Set to an empty string to remove the label.
|
||||
*/
|
||||
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
|
||||
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
|
||||
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The model with the account's 3PIDs.
|
||||
@@ -87,6 +85,11 @@ class NeoChatConnection : public Quotient::Connection
|
||||
*/
|
||||
Q_PROPERTY(bool canCheckMutualRooms READ canCheckMutualRooms NOTIFY canCheckMutualRoomsChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the server supports erasing user data when deactivating the account. This checks for MSC4025.
|
||||
*/
|
||||
Q_PROPERTY(bool canEraseData READ canEraseData NOTIFY canEraseDataChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the status after an attempt to change the password on an account.
|
||||
@@ -104,6 +107,7 @@ public:
|
||||
Q_INVOKABLE void logout(bool serverSideLogout);
|
||||
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
|
||||
bool canCheckMutualRooms() const;
|
||||
bool canEraseData() const;
|
||||
|
||||
/**
|
||||
* @brief Change the password for an account.
|
||||
@@ -123,7 +127,7 @@ public:
|
||||
[[nodiscard]] QString label() const;
|
||||
void setLabel(const QString &label);
|
||||
|
||||
Q_INVOKABLE void deactivateAccount(const QString &password);
|
||||
Q_INVOKABLE void deactivateAccount(const QString &password, bool erase);
|
||||
|
||||
ThreePIdModel *threePIdModel() const;
|
||||
|
||||
@@ -174,9 +178,6 @@ public:
|
||||
// the reference could be destroyed before the task is finished
|
||||
QCoro::Task<void> setupPushNotifications(QString endpoint);
|
||||
|
||||
QString deviceKey() const;
|
||||
QString encryptionKey() const;
|
||||
|
||||
bool isOnline() const;
|
||||
|
||||
LinkPreviewer *previewerForLink(const QUrl &link);
|
||||
@@ -194,6 +195,7 @@ Q_SIGNALS:
|
||||
void userConsentRequired(QUrl url);
|
||||
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
|
||||
void canCheckMutualRoomsChanged();
|
||||
void canEraseDataChanged();
|
||||
|
||||
/**
|
||||
* @brief Request a message be shown to the user of the given type.
|
||||
@@ -205,11 +207,6 @@ Q_SIGNALS:
|
||||
*/
|
||||
void errorOccured(const QString &error);
|
||||
|
||||
/**
|
||||
* @brief Request a notification be shown for an invite to this room.
|
||||
*/
|
||||
void showInviteNotification(NeoChatRoom *room);
|
||||
|
||||
private:
|
||||
bool m_isOnline = true;
|
||||
void setIsOnline(bool isOnline);
|
||||
@@ -223,4 +220,5 @@ private:
|
||||
QCache<QUrl, LinkPreviewer> m_linkPreviewers;
|
||||
|
||||
bool m_canCheckMutualRooms = false;
|
||||
bool m_canEraseData = false;
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "events/pollevent.h"
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroommember.h"
|
||||
#include "roomlastmessageprovider.h"
|
||||
#include "spacehierarchycache.h"
|
||||
#include "texthandler.h"
|
||||
@@ -126,9 +127,6 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
updatePushNotificationState(u"m.push_rules"_s);
|
||||
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
if (this->joinState() == JoinState::Invite) {
|
||||
Q_EMIT showInviteNotification(this);
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
connect(this, &Room::changed, this, [this] {
|
||||
@@ -161,6 +159,22 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
});
|
||||
}
|
||||
|
||||
bool NeoChatRoom::visible() const
|
||||
{
|
||||
return m_visible;
|
||||
}
|
||||
|
||||
void NeoChatRoom::setVisible(bool visible)
|
||||
{
|
||||
m_visible = visible;
|
||||
|
||||
if (!visible) {
|
||||
m_memberObjects.clear();
|
||||
m_eventContentModels.clear();
|
||||
m_threadModels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
int NeoChatRoom::contextAwareNotificationCount() const
|
||||
{
|
||||
// DOn't include spaces, rooms that the user hasn't joined and rooms where the user has joined the successor.
|
||||
@@ -256,7 +270,15 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
|
||||
auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Upload, url.toLocalFile(), txnId);
|
||||
connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
|
||||
connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
|
||||
connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
|
||||
connect(this, &Room::fileTransferFailed, job, [this, job, txnId] {
|
||||
auto info = fileTransferInfo(txnId);
|
||||
if (info.status == FileTransferInfo::Cancelled) {
|
||||
job->fileTransferCanceled(txnId);
|
||||
} else {
|
||||
job->fileTransferFailed(txnId);
|
||||
}
|
||||
});
|
||||
connect(job, &FileTransferPseudoJob::cancelRequested, this, &Room::cancelFileTransfer);
|
||||
KIO::getJobTracker()->registerJob(job);
|
||||
job->start();
|
||||
#endif
|
||||
@@ -759,22 +781,6 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
|
||||
}
|
||||
}
|
||||
|
||||
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
||||
{
|
||||
if (!successorId().isEmpty()) {
|
||||
return 0; // No one can upgrade a room that's already upgraded
|
||||
}
|
||||
|
||||
const auto &mId = userId.isEmpty() ? connection()->userId() : userId;
|
||||
if (const auto *plEvent = currentState().get<RoomPowerLevelsEvent>()) {
|
||||
return plEvent->powerLevelForUser(mId);
|
||||
}
|
||||
if (const auto *createEvent = creation()) {
|
||||
return createEvent->senderId() == mId ? 100 : 0;
|
||||
}
|
||||
return 0; // That's rather weird but may happen, according to rvdh
|
||||
}
|
||||
|
||||
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
|
||||
{
|
||||
QStringList events;
|
||||
@@ -1511,7 +1517,15 @@ void NeoChatRoom::download(const QString &eventId, const QUrl &localFilename)
|
||||
auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Download, localFilename.toLocalFile(), eventId);
|
||||
connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
|
||||
connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
|
||||
connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
|
||||
connect(this, &Room::fileTransferFailed, job, [this, job, eventId] {
|
||||
auto info = fileTransferInfo(eventId);
|
||||
if (info.status == FileTransferInfo::Cancelled) {
|
||||
job->fileTransferCanceled(eventId);
|
||||
} else {
|
||||
job->fileTransferFailed(eventId);
|
||||
}
|
||||
});
|
||||
connect(job, &FileTransferPseudoJob::cancelRequested, this, &Room::cancelFileTransfer);
|
||||
KIO::getJobTracker()->registerJob(job);
|
||||
job->start();
|
||||
#endif
|
||||
@@ -1710,4 +1724,82 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
|
||||
setState(type, stateKey, QJsonDocument::fromJson(content).object());
|
||||
}
|
||||
|
||||
NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
|
||||
{
|
||||
if (!m_memberObjects.contains(memberId)) {
|
||||
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
|
||||
}
|
||||
|
||||
return m_memberObjects[memberId].get();
|
||||
}
|
||||
|
||||
MessageContentModel *NeoChatRoom::contentModelForEvent(const QString &eventId)
|
||||
{
|
||||
if (eventId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_eventContentModels.contains(eventId)) {
|
||||
return m_eventContentModels.emplace(eventId, std::make_unique<MessageContentModel>(this, eventId)).first->second.get();
|
||||
}
|
||||
|
||||
return m_eventContentModels[eventId].get();
|
||||
}
|
||||
|
||||
MessageContentModel *NeoChatRoom::contentModelForEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||
if (roomMessageEvent == nullptr) {
|
||||
// If for some reason a model is there remove.
|
||||
if (m_eventContentModels.contains(event->id())) {
|
||||
m_eventContentModels.erase(event->id());
|
||||
}
|
||||
if (m_eventContentModels.contains(event->transactionId())) {
|
||||
m_eventContentModels.erase(event->transactionId());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eventId = event->id();
|
||||
const auto txnId = event->transactionId();
|
||||
if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) {
|
||||
return m_eventContentModels.emplace(eventId, std::make_unique<MessageContentModel>(this, eventId.isEmpty() ? txnId : eventId, false, eventId.isEmpty()))
|
||||
.first->second.get();
|
||||
}
|
||||
|
||||
if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) {
|
||||
return m_eventContentModels[eventId].get();
|
||||
}
|
||||
|
||||
if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) {
|
||||
if (eventId.isEmpty()) {
|
||||
return m_eventContentModels[txnId].get();
|
||||
}
|
||||
|
||||
// If we now have an event ID use that as the map key instead of transaction ID.
|
||||
auto txnModel = std::move(m_eventContentModels[txnId]);
|
||||
m_eventContentModels.erase(txnId);
|
||||
return m_eventContentModels.emplace(eventId, std::move(txnModel)).first->second.get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ThreadModel *NeoChatRoom::modelForThread(const QString &threadRootId)
|
||||
{
|
||||
if (threadRootId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_threadModels.contains(threadRootId)) {
|
||||
return m_threadModels.emplace(threadRootId, std::make_unique<ThreadModel>(threadRootId, this)).first->second.get();
|
||||
}
|
||||
|
||||
return m_threadModels[threadRootId].get();
|
||||
}
|
||||
|
||||
#include "moc_neochatroom.cpp"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/room.h>
|
||||
|
||||
#include <QCache>
|
||||
@@ -14,6 +15,8 @@
|
||||
|
||||
#include "enums/messagetype.h"
|
||||
#include "enums/pushrule.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "models/threadmodel.h"
|
||||
#include "neochatroommember.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
@@ -211,6 +214,9 @@ class NeoChatRoom : public Quotient::Room
|
||||
public:
|
||||
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
||||
|
||||
bool visible() const;
|
||||
void setVisible(bool visible);
|
||||
|
||||
[[nodiscard]] QDateTime lastActiveTime();
|
||||
|
||||
/**
|
||||
@@ -498,14 +504,6 @@ public:
|
||||
|
||||
bool canEncryptRoom() const;
|
||||
|
||||
/**
|
||||
* @brief Get the power level for the given user ID in the room.
|
||||
*
|
||||
* Returns the default value for a user in the room if they have no escalated
|
||||
* privileges or if they are not a member so membership should be known before using.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int getUserPowerLevel(const QString &userId) const;
|
||||
|
||||
Q_INVOKABLE void setUserPowerLevel(const QString &userID, const int &powerLevel);
|
||||
|
||||
ChatBarCache *mainCache() const;
|
||||
@@ -590,7 +588,57 @@ public:
|
||||
*/
|
||||
Quotient::FileTransferInfo cachedFileTransferInfo(const Quotient::RoomEvent *event) const;
|
||||
|
||||
/**
|
||||
* @brief Return a NeochatRoomMember object for the given user ID.
|
||||
*
|
||||
* @warning Because we can't guarantee that a member state event is downloaded
|
||||
* before a message they sent arrives this will create the object unconditionally
|
||||
* assuming that the state event will turn up later. It is therefor the
|
||||
* responsibility of the caller to ensure that they only ask for objects
|
||||
* for real senders.
|
||||
*/
|
||||
NeochatRoomMember *qmlSafeMember(const QString &memberId);
|
||||
|
||||
/**
|
||||
* @brief Returns the content model for the given event ID.
|
||||
*
|
||||
* A model is created is one doesn't exist. Will return nullptr if evtOrTxnId
|
||||
* is empty.
|
||||
*
|
||||
* @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent
|
||||
* event ID. The caller must ensure that the ID is a real event. A model will be
|
||||
* returned unconditionally.
|
||||
*
|
||||
* @warning Do NOT use for pending events as this function has no way to differentiate.
|
||||
*/
|
||||
MessageContentModel *contentModelForEvent(const QString &evtOrTxnId);
|
||||
|
||||
/**
|
||||
* @brief Returns the content model for the given event.
|
||||
*
|
||||
* A model is created is one doesn't exist. Will return nullptr if event is:
|
||||
* - nullptr
|
||||
* - not a Quotient::RoomMessageEvent (e.g a state event)
|
||||
*
|
||||
* @note This method is preferred to the version using just an event ID as it
|
||||
* can perform some basic checks. If a copy of the event is not available,
|
||||
* you may have to use the version that takes an event ID.
|
||||
*
|
||||
* @note This version must be used for pending events as it can differentiate.
|
||||
*/
|
||||
MessageContentModel *contentModelForEvent(const Quotient::RoomEvent *event);
|
||||
|
||||
/**
|
||||
* @brief Returns the thread model for the given thread root event ID.
|
||||
*
|
||||
* A model is created is one doesn't exist. Will return nullptr if threadRootId
|
||||
* is empty.
|
||||
*/
|
||||
Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId);
|
||||
|
||||
private:
|
||||
bool m_visible = false;
|
||||
|
||||
QSet<const Quotient::RoomEvent *> highlights;
|
||||
|
||||
bool m_hasFileUploading = false;
|
||||
@@ -619,6 +667,10 @@ private:
|
||||
void cleanupExtraEventRange(Quotient::RoomEventsRange events);
|
||||
void cleanupExtraEvent(const QString &eventId);
|
||||
|
||||
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
||||
std::unordered_map<QString, std::unique_ptr<MessageContentModel>> m_eventContentModels;
|
||||
std::unordered_map<QString, std::unique_ptr<ThreadModel>> m_threadModels;
|
||||
|
||||
private Q_SLOTS:
|
||||
void updatePushNotificationState(QString type);
|
||||
|
||||
@@ -654,14 +706,6 @@ Q_SIGNALS:
|
||||
*/
|
||||
void showMessage(MessageType::Type messageType, const QString &message);
|
||||
|
||||
/**
|
||||
* @brief Request a notification be shown for an invite to this room.
|
||||
*
|
||||
* @note This may later be blocked if there are any rules on where invites can
|
||||
* come from, but this is not NeoChatRoom's responsibility.
|
||||
*/
|
||||
void showInviteNotification(NeoChatRoom *room);
|
||||
|
||||
public Q_SLOTS:
|
||||
/**
|
||||
* @brief Upload a file to the matrix server and post the file to the room.
|
||||
|
||||
@@ -127,9 +127,8 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
|
||||
}
|
||||
auto sender = room->member(notification["event"_L1]["sender"_L1].toString());
|
||||
|
||||
// Don't display notifications for events in invited rooms
|
||||
// This should prevent empty notifications from appearing when they shouldn't
|
||||
if (room->joinState() == JoinState::Invite) {
|
||||
postInviteNotification(qobject_cast<NeoChatRoom *>(room));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -244,7 +243,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
|
||||
if (canReply) {
|
||||
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
|
||||
replyAction->setPlaceholderText(i18n("Reply..."));
|
||||
replyAction->setPlaceholderText(i18n("Reply…"));
|
||||
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(text);
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"Description[uk]": "Оприлюднити за допомогою NeoChat",
|
||||
"Description[x-test]": "xxShare via NeoChatxx",
|
||||
"Description[zh_TW]": "透過 NeoChat 分享",
|
||||
"Icon": "org.kde.neochat",
|
||||
"Icon": "org.kde.neochat.tray",
|
||||
"License": "GPL",
|
||||
"Name": "NeoChat",
|
||||
"Name[ar]": "نيوتشات",
|
||||
|
||||
@@ -6,20 +6,19 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
import org.kde.neochat.devtools
|
||||
|
||||
QQC2.Menu {
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
margins: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.MenuItem {
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:button", "Show QR Code")
|
||||
icon.name: "view-barcode-qr-symbolic"
|
||||
onTriggered: {
|
||||
@@ -36,7 +35,8 @@ QQC2.Menu {
|
||||
qrMax.open();
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Edit This Account")
|
||||
icon.name: "document-edit"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'AccountEditorPage'), {
|
||||
@@ -45,21 +45,24 @@ QQC2.Menu {
|
||||
title: i18n("Account editor")
|
||||
})
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Notification Settings")
|
||||
icon.name: "notifications"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('notifications');
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Devices")
|
||||
icon.name: "computer-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('devices');
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18n("Open Developer Tools")
|
||||
icon.name: "tools"
|
||||
visible: NeoChatConfig.developerTools
|
||||
@@ -71,7 +74,8 @@ QQC2.Menu {
|
||||
height: Kirigami.Units.gridUnit * 42
|
||||
})
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Open Secret Backup")
|
||||
icon.name: "unlock"
|
||||
visible: NeoChatConfig.secretBackup
|
||||
@@ -79,22 +83,21 @@ QQC2.Menu {
|
||||
title: i18nc("@title:window", "Open Key Backup")
|
||||
})
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
QQC2.Action {
|
||||
text: i18nc("@action:inmenu", "Verify This Device")
|
||||
icon.name: "security-low"
|
||||
onTriggered: root.connection.startSelfVerification()
|
||||
enabled: Controller.csSupported
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
|
||||
QQC2.Action {
|
||||
text: i18n("Logout")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.window.overlay).open()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: confirmLogoutDialogComponent
|
||||
ConfirmLogoutDialog {
|
||||
connection: root.connection
|
||||
}
|
||||
readonly property Component confirmLogoutDialogComponent: ConfirmLogoutDialog {
|
||||
connection: root.connection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
@@ -18,12 +19,21 @@ Kirigami.PromptDialog {
|
||||
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
|
||||
mainItem: FormCard.FormTextFieldDelegate {
|
||||
id: passwordField
|
||||
label: i18nc("@label:textbox", "Password")
|
||||
echoMode: TextInput.Password
|
||||
horizontalPadding: 0
|
||||
bottomPadding: 0
|
||||
mainItem: ColumnLayout {
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: passwordField
|
||||
label: i18nc("@label:textbox", "Password")
|
||||
echoMode: TextInput.Password
|
||||
horizontalPadding: 0
|
||||
bottomPadding: 0
|
||||
}
|
||||
|
||||
FormCard.FormCheckDelegate {
|
||||
id: eraseDelegate
|
||||
text: i18nc("@label:checkbox", "Erase Data")
|
||||
description: i18nc("@info", "Request your server to delete as much user data as possible.")
|
||||
visible: connection.canEraseData
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
@@ -34,7 +44,7 @@ Kirigami.PromptDialog {
|
||||
icon.name: "emblem-warning"
|
||||
enabled: passwordField.text.length > 0
|
||||
onClicked: {
|
||||
root.connection.deactivateAccount(passwordField.text);
|
||||
root.connection.deactivateAccount(passwordField.text, eraseDelegate.checked);
|
||||
root.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
|
||||
/**
|
||||
* Context menu when clicking on a room in the room list
|
||||
*/
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
property NeoChatRoom room
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal closed
|
||||
|
||||
Component {
|
||||
id: regularMenu
|
||||
QQC2.Menu {
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Mark as Read")
|
||||
icon.name: "checkmark"
|
||||
enabled: room.notificationCount > 0
|
||||
onTriggered: room.markAllMessagesAsRead()
|
||||
}
|
||||
|
||||
QQC2.MenuSeparator {}
|
||||
|
||||
QQC2.Menu {
|
||||
title: i18nc("@action:inmenu", "Notifications")
|
||||
icon.name: "notifications"
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Follow Global Setting")
|
||||
icon.name: "globe"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.Default
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.Default;
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("As in 'notify for all messages'", "All")
|
||||
icon.name: "notifications"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.All
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.All;
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'", "@Mentions and Keywords")
|
||||
icon.name: "im-user"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.MentionKeyword;
|
||||
}
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("As in 'do not notify for any messages'", "Off")
|
||||
icon.name: "notifications-disabled"
|
||||
checkable: true
|
||||
autoExclusive: true
|
||||
checked: room.pushNotificationState === PushNotificationState.Mute
|
||||
enabled: room.pushNotificationState != PushNotificationState.Unknown
|
||||
onTriggered: {
|
||||
room.pushNotificationState = PushNotificationState.Mute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: room.isFavourite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
|
||||
icon.name: room.isFavourite ? "rating" : "rating-unrated"
|
||||
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
|
||||
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
|
||||
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
||||
}
|
||||
|
||||
QQC2.MenuSeparator {}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: if (room.isDirectChat()) {
|
||||
Clipboard.saveText(room.directChatRemoteMember.id);
|
||||
} else if (room.canonicalAlias.length === 0) {
|
||||
Clipboard.saveText(room.id);
|
||||
} else {
|
||||
Clipboard.saveText(room.canonicalAlias);
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@action:inmenu", "Room Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onTriggered: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuSeparator {}
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Leave Room")
|
||||
icon.name: "go-previous"
|
||||
onTriggered: {
|
||||
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
root.closed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mobileMenu
|
||||
|
||||
Kirigami.OverlayDrawer {
|
||||
id: drawer
|
||||
|
||||
parent: applicationWindow().overlay
|
||||
edge: Qt.BottomEdge
|
||||
|
||||
height: popupContent.implicitHeight
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
|
||||
ColumnLayout {
|
||||
id: popupContent
|
||||
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
source: room.avatarMediaUrl
|
||||
name: room.displayName
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
Kirigami.Heading {
|
||||
level: 5
|
||||
Layout.fillWidth: true
|
||||
text: room.displayName
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
checked: room.isFavourite
|
||||
checkable: true
|
||||
icon.name: 'favorite'
|
||||
Accessible.name: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
|
||||
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
text: i18nc("@action:button", "Room Settings")
|
||||
icon.name: 'settings-configure-symbolic'
|
||||
onClicked: {
|
||||
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
|
||||
drawer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
|
||||
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
|
||||
onClicked: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
text: i18n("Mark as Read")
|
||||
icon.name: "checkmark"
|
||||
onClicked: room.markAllMessagesAsRead()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
text: i18n("Leave Room")
|
||||
icon.name: "go-previous"
|
||||
onClicked: {
|
||||
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
|
||||
room: root.room
|
||||
}).open();
|
||||
drawer.close();
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: root.closed()
|
||||
}
|
||||
}
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
|
||||
|
||||
function open() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
onStatusChanged: if (status == Loader.Ready) {
|
||||
if (Kirigami.Settings.isMobile) {
|
||||
item.open();
|
||||
} else {
|
||||
item.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import Qt.labs.qmlmodels
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
@@ -25,7 +26,7 @@ import org.kde.neochat
|
||||
* For event types that need alternate actions this class can be used as a base and
|
||||
* the actions and nested actions can be overwritten to show the alternate items.
|
||||
*/
|
||||
Loader {
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
/**
|
||||
@@ -62,25 +63,6 @@ Loader {
|
||||
*/
|
||||
property string hoveredLink: ""
|
||||
|
||||
/**
|
||||
* @brief The list of menu item actions that have sub-actions.
|
||||
*
|
||||
* Each action will be instantiated as a single line that open a sub menu.
|
||||
*/
|
||||
property list<Kirigami.Action> nestedActions
|
||||
|
||||
/**
|
||||
* @brief The main list of menu item actions.
|
||||
*
|
||||
* Each action will be instantiated as a single line in the menu.
|
||||
*/
|
||||
property list<Kirigami.Action> actions
|
||||
|
||||
/**
|
||||
* @brief Whether the web search menu should be shown or not.
|
||||
*/
|
||||
property bool enableWebSearch: true
|
||||
|
||||
/**
|
||||
* Some common actions shared between menus
|
||||
*/
|
||||
@@ -112,7 +94,7 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
component ReplyMessageAction: Kirigami.Action {
|
||||
component ReplyMessageAction: QQC2.Action {
|
||||
text: i18n("Reply")
|
||||
icon.name: "mail-replied-symbolic"
|
||||
onTriggered: {
|
||||
@@ -150,90 +132,70 @@ Loader {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: regularMenu
|
||||
headerContentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
QQC2.Menu {
|
||||
id: menu
|
||||
Instantiator {
|
||||
model: root.nestedActions
|
||||
delegate: QQC2.Menu {
|
||||
id: menuItem
|
||||
visible: modelData.visible
|
||||
title: modelData.text
|
||||
icon: modelData.icon
|
||||
KirigamiComponents.Avatar {
|
||||
source: root.author.avatarUrl
|
||||
|
||||
Instantiator {
|
||||
model: modelData.children
|
||||
delegate: QQC2.MenuItem {
|
||||
text: modelData.text
|
||||
icon.name: modelData.icon.name
|
||||
onTriggered: modelData.trigger()
|
||||
}
|
||||
onObjectAdded: (index, object) => {
|
||||
menuItem.insertItem(0, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
onObjectAdded: (index, object) => {
|
||||
object.visible = false;
|
||||
menu.addMenu(object);
|
||||
}
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 4
|
||||
text: root.author.htmlSafeDisplayName
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: root.plainText
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
onLinkActivated: RoomManager.resolveResource(link, "join")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
visible: Kirigami.Settings.isMobile
|
||||
|
||||
displayComponent: RowLayout {
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
|
||||
Repeater {
|
||||
model: root.actions
|
||||
DelegateChooser {
|
||||
role: "separator"
|
||||
DelegateChoice {
|
||||
roleValue: true
|
||||
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
QQC2.MenuSeparator {
|
||||
visible: modelData.visible
|
||||
}
|
||||
contentItem: Kirigami.Heading {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.family: "emoji"
|
||||
text: modelData
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: false
|
||||
|
||||
QQC2.MenuItem {
|
||||
visible: modelData.visible
|
||||
action: modelData
|
||||
onClicked: root.item.close()
|
||||
}
|
||||
onClicked: {
|
||||
currentRoom.toggleReaction(eventId, modelData);
|
||||
root.item.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Menu {
|
||||
id: webshortcutmenu
|
||||
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
|
||||
icon.name: "search-symbolic"
|
||||
property bool isVisible: webshortcutmodel.enabled && root.enableWebSearch
|
||||
Component.onCompleted: {
|
||||
webshortcutmenu.parent.visible = isVisible;
|
||||
}
|
||||
onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible
|
||||
Instantiator {
|
||||
model: WebShortcutModel {
|
||||
id: webshortcutmodel
|
||||
selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText
|
||||
onOpenUrl: url => RoomManager.resolveResource(url.toString())
|
||||
}
|
||||
delegate: QQC2.MenuItem {
|
||||
text: model.display
|
||||
icon.name: model.decoration
|
||||
onTriggered: webshortcutmodel.trigger(model.edit)
|
||||
}
|
||||
onObjectAdded: (index, object) => webshortcutmenu.insertItem(0, object)
|
||||
}
|
||||
QQC2.MenuSeparator {}
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Configure Web Shortcuts...")
|
||||
icon.name: "configure"
|
||||
visible: !Controller.isFlatpak
|
||||
onTriggered: webshortcutmodel.configureWebShortcuts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
}
|
||||
}
|
||||
Component {
|
||||
@@ -330,31 +292,6 @@ Loader {
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
|
||||
Repeater {
|
||||
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
|
||||
delegate: QQC2.ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
font.family: "emoji"
|
||||
text: modelData
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
currentRoom.toggleReaction(eventId, modelData);
|
||||
root.item.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -422,5 +359,5 @@ Loader {
|
||||
} else {
|
||||
item.popup();
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -19,65 +19,6 @@ RowLayout {
|
||||
|
||||
signal search
|
||||
|
||||
property Kirigami.Action exploreAction: Kirigami.Action {
|
||||
text: i18n("Explore rooms")
|
||||
icon.name: "compass"
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
property Kirigami.Action chatAction: Kirigami.Action {
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
property Kirigami.Action roomAction: Kirigami.Action {
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "system-users-symbolic"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Room")
|
||||
});
|
||||
}
|
||||
shortcut: StandardKey.New
|
||||
}
|
||||
property Kirigami.Action spaceAction: Kirigami.Action {
|
||||
text: i18n("Create a Space")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection,
|
||||
isSpace: true,
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
property Kirigami.Action scanAction: Kirigami.Action {
|
||||
text: i18n("Scan a QR Code")
|
||||
icon.name: "view-barcode-qr"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Emitted when the text is changed in the search field.
|
||||
*/
|
||||
@@ -119,18 +60,13 @@ RowLayout {
|
||||
Accessible.onPressAction: menuButton.action.trigger()
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
action: Kirigami.Action {
|
||||
action: QQC2.Action {
|
||||
text: i18nc("@action:button", "Show Menu")
|
||||
icon.name: "application-menu-symbolic"
|
||||
onTriggered: {
|
||||
if (Kirigami.isMobile) {
|
||||
const menu = mobileMenu.createObject();
|
||||
menu.open();
|
||||
} else {
|
||||
const menu = desktopMenu.createObject(menuButton);
|
||||
menu.closed.connect(menuButton.toggle);
|
||||
menu.open();
|
||||
}
|
||||
const item = menu.createObject(menuButton);
|
||||
item.closed.connect(menuButton.toggle);
|
||||
item.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,86 +76,70 @@ RowLayout {
|
||||
}
|
||||
|
||||
Component {
|
||||
id: desktopMenu
|
||||
id: menu
|
||||
QQC2.Menu {
|
||||
x: mirrored ? parent.width - width : 0
|
||||
y: parent ? parent.height : 0
|
||||
|
||||
modal: true
|
||||
dim: false
|
||||
QQC2.MenuItem {
|
||||
text: i18n("Explore rooms")
|
||||
icon.name: "compass"
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
});
|
||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
Accessible.onPressAction: action.triggered()
|
||||
action: exploreAction
|
||||
text: i18n("Find your friends")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
})
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
Accessible.onPressAction: action.triggered()
|
||||
action: chatAction
|
||||
text: i18n("Create a Room")
|
||||
icon.name: "system-users-symbolic"
|
||||
action: QQC2.Action {
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Room")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
Accessible.onPressAction: action.triggered()
|
||||
action: roomAction
|
||||
text: i18n("Create a Space")
|
||||
icon.name: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
connection: root.connection,
|
||||
isSpace: true,
|
||||
title: i18nc("@title", "Create a Space")
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Space")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.MenuItem {
|
||||
Accessible.onPressAction: action.triggered()
|
||||
action: spaceAction
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
Accessible.onPressAction: action.triggered()
|
||||
action: scanAction
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: mobileMenu
|
||||
|
||||
Kirigami.OverlayDrawer {
|
||||
id: menuRoot
|
||||
|
||||
edge: Qt.BottomEdge
|
||||
parent: applicationWindow().overlay
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
bottomPadding: 0
|
||||
topPadding: 0
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
|
||||
Kirigami.ListSectionHeader {
|
||||
label: i18n("Create rooms and chats")
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
action: exploreAction
|
||||
onClicked: menuRoot.close()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
action: chatAction
|
||||
onClicked: menuRoot.close()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
action: roomAction
|
||||
onClicked: menuRoot.close()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
action: roomAction
|
||||
onClicked: menuRoot.close()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Delegates.RoundedItemDelegate {
|
||||
action: scanAction
|
||||
onClicked: menuRoot.close()
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
text: i18n("Scan a QR Code")
|
||||
icon.name: "view-barcode-qr"
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user