Compare commits
197 Commits
work/nvrwh
...
work/carl/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5222f3b221 | ||
|
|
a7ee1fac3c | ||
|
|
068bfc948b | ||
|
|
47470f5a6d | ||
|
|
144dc1f8f4 | ||
|
|
4002bb804c | ||
|
|
760ed24b37 | ||
|
|
489979af43 | ||
|
|
e482e12826 | ||
|
|
fa7b9d54e2 | ||
|
|
f60114c7f6 | ||
|
|
de55253e54 | ||
|
|
8479e51051 | ||
|
|
bb2fd7c9c4 | ||
|
|
a0b0a5d47f | ||
|
|
10bdc1d3d1 | ||
|
|
6ec7d8d6b4 | ||
|
|
0dca9588ff | ||
|
|
ca03c530b2 | ||
|
|
973ec24674 | ||
|
|
1da767ff0a | ||
|
|
89127876f9 | ||
|
|
47a738a703 | ||
|
|
516b1cff88 | ||
|
|
6438977964 | ||
|
|
d750263d39 | ||
|
|
3ed952db9e | ||
|
|
f8040a1bf6 | ||
|
|
d83b31fd86 | ||
|
|
e0dbb657f6 | ||
|
|
a807cc6143 | ||
|
|
fe064c0ef8 | ||
|
|
6cc773426f | ||
|
|
db94408ba6 | ||
|
|
333bd3cdb9 | ||
|
|
9a0d82eb31 | ||
|
|
0990c0507c | ||
|
|
9f76ce22c1 | ||
|
|
6acd6075ff | ||
|
|
57b6f00d8e | ||
|
|
280c9327cb | ||
|
|
05bcbb695f | ||
|
|
552f4e8f13 | ||
|
|
1223c5348d | ||
|
|
3ccff4f337 | ||
|
|
a67f3334ea | ||
|
|
52dafbb6c8 | ||
|
|
4341cc437d | ||
|
|
7bb7dd7bbb | ||
|
|
b4090d9671 | ||
|
|
3d2bcce99a | ||
|
|
f3a04635cf | ||
|
|
419cb07557 | ||
|
|
57fccaa076 | ||
|
|
4bf65339f8 | ||
|
|
e5f2e209a2 | ||
|
|
be3b5cdb8a | ||
|
|
abd56baa51 | ||
|
|
ff27a1d0bb | ||
|
|
8af2d4d273 | ||
|
|
f64c8e28da | ||
|
|
4570d6350b | ||
|
|
6dd51a35c5 | ||
|
|
2470990d75 | ||
|
|
1a87e605d6 | ||
|
|
e995740790 | ||
|
|
0fb8b740a4 | ||
|
|
5087161e4b | ||
|
|
918e9e492a | ||
|
|
f10805dddb | ||
|
|
55e4d03dfe | ||
|
|
9f9086b4b0 | ||
|
|
e00ce79d26 | ||
|
|
a13b2e6bd2 | ||
|
|
cefe5acdaa | ||
|
|
1438aea965 | ||
|
|
1b60e24c64 | ||
|
|
4f1c8f6f35 | ||
|
|
882945260a | ||
|
|
3677088104 | ||
|
|
8f141cd88d | ||
|
|
0d1b35b610 | ||
|
|
87c20bf03c | ||
|
|
77479ca22d | ||
|
|
34ad743e98 | ||
|
|
493e27622f | ||
|
|
b1b6c7ceed | ||
|
|
d0b1610a9f | ||
|
|
dcf520a7a9 | ||
|
|
a0ae8b28b2 | ||
|
|
78a6179a11 | ||
|
|
9c91557d8f | ||
|
|
fb24ffd20d | ||
|
|
0c985a0af1 | ||
|
|
787dc5ab66 | ||
|
|
f11abdeebd | ||
|
|
89141dddf2 | ||
|
|
69f08de0ff | ||
|
|
925e20ebb8 | ||
|
|
ee254a286d | ||
|
|
3cc8d32dd3 | ||
|
|
50cf6d9750 | ||
|
|
cedbb64932 | ||
|
|
b45898a5b6 | ||
|
|
ccb1748ab3 | ||
|
|
11e9af15f7 | ||
|
|
8ad41fec92 | ||
|
|
ac5212ebb2 | ||
|
|
4e16b91f54 | ||
|
|
343f4965ed | ||
|
|
ac775c5aaf | ||
|
|
db17888d42 | ||
|
|
f12d10baa6 | ||
|
|
4289c1345f | ||
|
|
0d6a83b063 | ||
|
|
76b04dcba9 | ||
|
|
64dee7eb12 | ||
|
|
bc84ad8d56 | ||
|
|
afe8a2a5e4 | ||
|
|
87213dc9dd | ||
|
|
300d2428eb | ||
|
|
abe7d70822 | ||
|
|
d8bf26158a | ||
|
|
741cb57105 | ||
|
|
81c73037ca | ||
|
|
f6ba4f2ecd | ||
|
|
23303c0483 | ||
|
|
5a02448326 | ||
|
|
411ae25e80 | ||
|
|
6e1b3fe860 | ||
|
|
4a38d83a68 | ||
|
|
280d90e191 | ||
|
|
b4b24430a1 | ||
|
|
b7b7f1817a | ||
|
|
3d67a682f4 | ||
|
|
da6df18367 | ||
|
|
0b1b1b8c8c | ||
|
|
2493a00ba4 | ||
|
|
d6ebb1308c | ||
|
|
332c311023 | ||
|
|
27d33d121a | ||
|
|
e954fc204f | ||
|
|
d1f7e7091e | ||
|
|
bd4eeb405b | ||
|
|
e6a060c192 | ||
|
|
f53a7a27f6 | ||
|
|
34384dced4 | ||
|
|
b68cfafab2 | ||
|
|
58b836fd1e | ||
|
|
90cad05bae | ||
|
|
072f7cec37 | ||
|
|
39388e204e | ||
|
|
6ccb201110 | ||
|
|
178b516c7c | ||
|
|
ac88e13e58 | ||
|
|
8d3e145e0b | ||
|
|
f40d1b9f4e | ||
|
|
e9cd165457 | ||
|
|
c0c86c67b6 | ||
|
|
12afa43d23 | ||
|
|
386f637b94 | ||
|
|
d7bd9f4609 | ||
|
|
33c9edc9a3 | ||
|
|
3b2dbc731e | ||
|
|
51a29ac528 | ||
|
|
cc8bf79a9b | ||
|
|
8d47e58861 | ||
|
|
47ce8a4846 | ||
|
|
71c9537c61 | ||
|
|
8825e6ec83 | ||
|
|
44ec93f0a0 | ||
|
|
ed7688e66f | ||
|
|
6b49854b12 | ||
|
|
e8484ebc7a | ||
|
|
17d01c68c4 | ||
|
|
dae2cbab90 | ||
|
|
b8e8fa3ee5 | ||
|
|
9c4a925171 | ||
|
|
8996806b05 | ||
|
|
2bd4579c10 | ||
|
|
0891f32c08 | ||
|
|
5287c2d529 | ||
|
|
084b89f3dc | ||
|
|
2dd3197beb | ||
|
|
6d9dca7da8 | ||
|
|
e0f16054fc | ||
|
|
fa70679439 | ||
|
|
949bd20873 | ||
|
|
79a9eb0de0 | ||
|
|
e1b9bc7d0e | ||
|
|
a1abf22174 | ||
|
|
fa27d993e2 | ||
|
|
d4b750433e | ||
|
|
9df534c72c | ||
|
|
f785e4d5b0 | ||
|
|
f186be7314 | ||
|
|
3cbcc2b597 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ kate.project.ctags.*
|
||||
*.user
|
||||
.flatpak-builder/
|
||||
.idea/
|
||||
cmake-build-*
|
||||
|
||||
39
.kde-ci.yml
39
.kde-ci.yml
@@ -1,8 +1,8 @@
|
||||
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
# SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
Dependencies:
|
||||
- 'on': ['@all']
|
||||
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@stable'
|
||||
'frameworks/kcoreaddons': '@stable'
|
||||
@@ -11,6 +11,7 @@ Dependencies:
|
||||
'frameworks/kconfig': '@stable'
|
||||
'frameworks/syntax-highlighting': '@stable'
|
||||
'frameworks/kitemmodels': '@stable'
|
||||
'frameworks/kquickcharts': '@stable'
|
||||
'frameworks/knotifications': '@stable'
|
||||
'libraries/kquickimageeditor': '@stable'
|
||||
'frameworks/sonnet': '@stable'
|
||||
@@ -19,15 +20,43 @@ Dependencies:
|
||||
'third-party/qtkeychain': '@latest'
|
||||
'third-party/cmark': '@latest'
|
||||
'third-party/qcoro': '@latest'
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@stable'
|
||||
'frameworks/kio': '@stable'
|
||||
'frameworks/kwindowsystem': '@stable'
|
||||
'frameworks/kconfigwidgets': '@stable'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@stable'
|
||||
|
||||
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
|
||||
'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'
|
||||
'libraries/kquickimageeditor': '@latest-kf6'
|
||||
'frameworks/sonnet': '@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/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kconfigwidgets': '@latest-kf6'
|
||||
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows/Qt5' ]
|
||||
|
||||
@@ -7,7 +7,7 @@ Copyright: 2020 Carson Black <uhhadd@gmail.com>
|
||||
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
Files: android/res/drawable/splash.xml
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: .gitignore
|
||||
@@ -27,11 +27,11 @@ Copyright: 2021 Carl Schwan <carlschwan@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: src/neochatconfig.kcfg
|
||||
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <fella@posteo.de>
|
||||
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: src/neochat.notifyrc
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
|
||||
@@ -39,5 +39,5 @@ Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: .flatpak-manifest.json
|
||||
Copyright: 2020-2022 Tobias Fella <fella@posteo.de>
|
||||
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
@@ -8,19 +8,19 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# KDE Applications version, managed by release script.
|
||||
set(RELEASE_SERVICE_VERSION_MAJOR "23")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "07")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
set(KF5_MIN_VERSION "5.91.0")
|
||||
set(KF_MIN_VERSION "5.91.0")
|
||||
set(QT_MIN_VERSION "5.15.2")
|
||||
if (ANDROID)
|
||||
set(QT_MIN_VERSION "5.15.8")
|
||||
endif()
|
||||
|
||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
@@ -57,16 +57,16 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
|
||||
set_package_properties(KF5 PROPERTIES
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
set_package_properties(KF5Kirigami2 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
find_package(KF5KirigamiAddons 0.6 REQUIRED)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION}Keychain)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
|
||||
@@ -82,15 +82,15 @@ if(ANDROID)
|
||||
)
|
||||
else()
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
|
||||
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
|
||||
TYPE RUNTIME
|
||||
)
|
||||
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
|
||||
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
find_package(Quotient 0.6)
|
||||
@@ -111,6 +111,7 @@ set_package_properties(cmark PROPERTIES
|
||||
|
||||
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
||||
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
||||
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||
|
||||
find_package(KQuickImageEditor COMPONENTS)
|
||||
set_package_properties(KQuickImageEditor PROPERTIES
|
||||
@@ -124,19 +125,18 @@ find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
find_package(KF5DocTools ${KF5_MIN_VERSION})
|
||||
set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
|
||||
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
|
||||
"Tools to generate documentation"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
|
||||
find_package(Sqlite3)
|
||||
|
||||
if(NOT Quotient_VERSION_MINOR GREATER 6)
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
find_package(Sqlite3)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
@@ -155,7 +155,7 @@ if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
|
||||
add_subdirectory(autotests)
|
||||
endif()
|
||||
|
||||
if(KF5DocTools_FOUND)
|
||||
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
|
||||
kdoctools_install(po)
|
||||
add_subdirectory(doc)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
|
||||
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
-->
|
||||
# NeoChat
|
||||
|
||||
@@ -8,3 +8,9 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test Quotient
|
||||
TEST_NAME neochatroomtest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
texthandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME texthandlertest
|
||||
)
|
||||
|
||||
@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
|
||||
void NeoChatRoomTest::subtitleTextTest()
|
||||
{
|
||||
QCOMPARE(room->timelineSize(), 1);
|
||||
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
|
||||
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::eventTest()
|
||||
|
||||
591
autotests/texthandlertest.cpp
Normal file
591
autotests/texthandlertest.cpp
Normal file
@@ -0,0 +1,591 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "texthandler.h"
|
||||
|
||||
#include <qnamespace.h>
|
||||
#include <quotient_common.h>
|
||||
#include <syncdata.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class TextHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void allowedAttributes();
|
||||
void stripDisallowedTags();
|
||||
void stripDisallowedAttributes();
|
||||
void emptyCodeTags();
|
||||
|
||||
void sendSimpleStringCase();
|
||||
void sendSingleParaMarkup();
|
||||
void sendMultipleSectionMarkup();
|
||||
void sendBadLinks();
|
||||
void sendEscapeCode();
|
||||
void sendCodeClass();
|
||||
|
||||
void receiveStripReply();
|
||||
void receivePlainTextIn();
|
||||
|
||||
void receiveRichInPlainOut_data();
|
||||
void receiveRichInPlainOut();
|
||||
void receivePlainStripHtml();
|
||||
void receivePlainStripMarkup();
|
||||
void receiveStripNewlines();
|
||||
|
||||
void receiveRichUserPill();
|
||||
void receiveRichStrikethrough();
|
||||
void receiveRichtextIn();
|
||||
void receiveRichMxcUrl();
|
||||
void receiveRichPlainUrl();
|
||||
void receiveRichEmote();
|
||||
void receiveRichEdited_data();
|
||||
void receiveRichEdited();
|
||||
};
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
const auto json = QJsonDocument::fromJson(R"EVENT({
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an **example** text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "/me This is an emote.",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an emote.",
|
||||
"msgtype": "m.emote"
|
||||
},
|
||||
"event_id": "$153273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1231
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "tested",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
|
||||
"origin_server_ts": 1680948575928,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1747776,
|
||||
"m.relations": {
|
||||
"m.replace": {
|
||||
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
|
||||
"origin_server_ts": 1680948580992,
|
||||
"sender": "@example:example.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
})EVENT");
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
#endif
|
||||
|
||||
void TextHandlerTest::allowedAttributes()
|
||||
{
|
||||
const QString testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
|
||||
const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
|
||||
// Handle urls where the href has either single (') or double (") quotes.
|
||||
const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
|
||||
const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString1);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
|
||||
|
||||
testTextHandler.setData(testInputString2);
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
|
||||
}
|
||||
|
||||
void TextHandlerTest::stripDisallowedTags()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>");
|
||||
const QString testOutputString = QStringLiteral("<p>Allowed</p> <span>Allowed</span> Disallowed");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::stripDisallowedAttributes()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
|
||||
const QString testOutputString = QStringLiteral("<p>Test</p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that empty code tags are handled.
|
||||
* (this was a bug during development hence the test)
|
||||
*/
|
||||
void TextHandlerTest::emptyCodeTags()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<pre><code></code></pre>");
|
||||
const QString testOutputString = QStringLiteral("<pre><code></code></pre>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendSimpleStringCase()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
|
||||
const QString testOutputString = QStringLiteral("<p>This data should just be put in a paragraph.</p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendSingleParaMarkup()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
"Text para with **bold**, *italic*, [link](https://kde.org), , `inline code`.");
|
||||
const QString testOutputString = QStringLiteral(
|
||||
"<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
|
||||
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendMultipleSectionMarkup()
|
||||
{
|
||||
const QString testInputString =
|
||||
QStringLiteral("Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```");
|
||||
const QString testOutputString = QStringLiteral(
|
||||
"<p>Text para</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>\n<ul>\n<li>List 1</li>\n<li>List "
|
||||
"2</li>\n</ul>\n<ol>\n<li>one</li>\n<li>two</li>\n</ol>\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<p>horizontal "
|
||||
"rule</p>\n<hr>\n<pre><code>codeblock\n</code></pre>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendBadLinks()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("[link](kde.org), ");
|
||||
const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
/**
|
||||
* All text between code tags is treated as plain so it should get escaped.
|
||||
*/
|
||||
void TextHandlerTest::sendEscapeCode()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("```\n<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```");
|
||||
const QString testOutputString =
|
||||
QStringLiteral("<pre><code><p>Test <span style="font-size:50px;">some</span> code</p>\n</code></pre>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendCodeClass()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>");
|
||||
const QString testOutputString = QStringLiteral("<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveStripReply()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
"<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a><a "
|
||||
"href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />Message replied to.</blockquote></mx-reply>Reply message.");
|
||||
const QString testOutputString = QStringLiteral("Reply message.");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichInPlainOut_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
|
||||
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
||||
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichInPlainOut()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receivePlainTextIn()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
|
||||
const QString testOutputStringRich = QStringLiteral("<plain text in tag bracket><br>Test link <a href=\"https://kde.org\">https://kde.org</a>.");
|
||||
QString testOutputStringPlain = QStringLiteral("<plain text in tag bracket>\nTest link https://kde.org.");
|
||||
|
||||
// Make sure quotes are maintained in a plain string.
|
||||
const QString testInputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
|
||||
const QString testOutputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\"");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich);
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain);
|
||||
|
||||
testTextHandler.setData(testInputString2);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2);
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveStripNewlines()
|
||||
{
|
||||
const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines.");
|
||||
const QString testInputStringRich = QStringLiteral("Test<br>many<br />new<br>lines.");
|
||||
const QString testOutputString = QStringLiteral("Test many new lines.");
|
||||
|
||||
const QString testInputStringPlain2 = QStringLiteral("* List\n* Items");
|
||||
const QString testOutputString2 = QStringLiteral("List Items");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputStringPlain);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);
|
||||
|
||||
testTextHandler.setData(testInputStringRich);
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);
|
||||
|
||||
testTextHandler.setData(testInputStringPlain2);
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a plain text output of a received string all html is stripped except for
|
||||
* code which is unescaped if it's html.
|
||||
*/
|
||||
void TextHandlerTest::receivePlainStripHtml()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||
const QString testOutputString = QStringLiteral("Test Some code <strong>with tags</strong>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receivePlainStripMarkup()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("**bold** `<p>inline code</p>` *italic*");
|
||||
const QString testOutputString = QStringLiteral("bold <p>inline code</p> italic");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichUserPill()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
|
||||
const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichStrikethrough()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
|
||||
const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichtextIn()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||
const QString testOutputString = QStringLiteral("<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::receiveRichMxcUrl()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" "
|
||||
"alt=\"image\">");
|
||||
const QString testOutputString = QStringLiteral(
|
||||
"<img "
|
||||
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
|
||||
"alt=\"image\"><img "
|
||||
"src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" "
|
||||
"alt=\"image\">");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* For when your rich input string has a plain text url left in.
|
||||
*
|
||||
* This test is to show that a url that is already rich will be left alone but a
|
||||
* plain one will be linkified.
|
||||
*/
|
||||
void TextHandlerTest::receiveRichPlainUrl()
|
||||
{
|
||||
// This is an actual link that caused trouble which is why it's so long. Keeping
|
||||
// so we can confirm consistent behaviour for complex urls.
|
||||
const QString testInputStringLink1 = QStringLiteral(
|
||||
"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im "
|
||||
"<a "
|
||||
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>");
|
||||
const QString testOutputStringLink1 = QStringLiteral(
|
||||
"<a "
|
||||
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">https://matrix.to/#/"
|
||||
"!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a "
|
||||
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
|
||||
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>");
|
||||
|
||||
// Another real case. The linkification wasn't handling it when a single link
|
||||
// contains what looks like and email. It was been broken into 3 but needs to
|
||||
// be just single link.
|
||||
const QString testInputStringLink2 = QStringLiteral("https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/");
|
||||
const QString testOutputStringLink2 = QStringLiteral(
|
||||
"<a "
|
||||
"href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/"
|
||||
"CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>");
|
||||
|
||||
QString testInputStringEmail = QStringLiteral(R"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)");
|
||||
QString testOutputStringEmail =
|
||||
QStringLiteral(R"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)");
|
||||
|
||||
QString testInputStringMxId = QStringLiteral("@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>");
|
||||
QString testOutputStringMxId = QStringLiteral(
|
||||
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputStringLink1);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1);
|
||||
|
||||
testTextHandler.setData(testInputStringLink2);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2);
|
||||
|
||||
testTextHandler.setData(testInputStringEmail);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail);
|
||||
|
||||
testTextHandler.setData(testInputStringMxId);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
|
||||
}
|
||||
|
||||
// Test that user pill is add to an emote message.
|
||||
// N.B. The second message in the test timeline is marked as an emote.
|
||||
void TextHandlerTest::receiveRichEmote()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
|
||||
const QString testInputString = QStringLiteral("This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
|
||||
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichEdited_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
|
||||
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
|
||||
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
|
||||
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
|
||||
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
|
||||
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichEdited()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
@@ -2,7 +2,7 @@
|
||||
<!--
|
||||
- SPDX-License-Identifier: CC0-1.0
|
||||
- SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
|
||||
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||
-->
|
||||
<component type="desktop">
|
||||
<id>org.kde.neochat</id>
|
||||
@@ -179,7 +179,7 @@
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
<developer_name>The KDE Community</developer_name>
|
||||
<developer_name xml:lang="ar">مجتمع كدي</developer_name>
|
||||
<developer_name xml:lang="ar">مجتمع كِيدِي</developer_name>
|
||||
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name>
|
||||
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
|
||||
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
|
||||
@@ -231,6 +231,7 @@
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.04.0" date="2023-04-20"/>
|
||||
<release version="23.01" date="2023-01-30">
|
||||
<url>https://plasma-mobile.org/2023/01/30/january-blog-post/</url>
|
||||
<description>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
# SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Name=NeoChat
|
||||
|
||||
1814
po/ar/neochat.po
1814
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1777
po/az/neochat.po
1777
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1659
po/ca/neochat.po
1659
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1636
po/cs/neochat.po
1636
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1675
po/da/neochat.po
1675
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1931
po/de/neochat.po
1931
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1678
po/el/neochat.po
1678
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1970
po/en_GB/neochat.po
1970
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1660
po/es/neochat.po
1660
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1534
po/eu/neochat.po
1534
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
2842
po/fi/neochat.po
2842
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1790
po/fr/neochat.po
1790
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1783
po/hu/neochat.po
1783
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
2046
po/ia/neochat.po
2046
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1842
po/id/neochat.po
1842
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1688
po/ie/neochat.po
1688
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1800
po/it/neochat.po
1800
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1591
po/ja/neochat.po
1591
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1647
po/ka/neochat.po
1647
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1765
po/ko/neochat.po
1765
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
3817
po/lt/neochat.po
Normal file
3817
po/lt/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
1652
po/nl/neochat.po
1652
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1746
po/nn/neochat.po
1746
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1765
po/pa/neochat.po
1765
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1858
po/pl/neochat.po
1858
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1799
po/pt/neochat.po
1799
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1777
po/pt_BR/neochat.po
1777
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1683
po/ru/neochat.po
1683
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1786
po/sk/neochat.po
1786
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1794
po/sl/neochat.po
1794
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1765
po/sv/neochat.po
1765
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1794
po/ta/neochat.po
1794
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1650
po/tok/neochat.po
1650
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
5
po/tr/docs/neochat/CMakeLists.txt
Normal file
5
po/tr/docs/neochat/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})
|
||||
122
po/tr/docs/neochat/man-neochat.1.docbook
Normal file
122
po/tr/docs/neochat/man-neochat.1.docbook
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
|
||||
<!ENTITY % Turkish "INCLUDE">
|
||||
]>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
-->
|
||||
|
||||
<refentry lang="&language;">
|
||||
<refentryinfo>
|
||||
<title
|
||||
>NeoChat Kullanıcı Kılavuzu</title>
|
||||
<author
|
||||
><firstname
|
||||
>Carl</firstname
|
||||
><surname
|
||||
>Schwan</surname
|
||||
> <contrib
|
||||
>NeoChat man page.</contrib
|
||||
> <email
|
||||
>carl@carlschwan.eu</email
|
||||
></author>
|
||||
<date
|
||||
>2022-11-01</date>
|
||||
<releaseinfo
|
||||
>22.09</releaseinfo>
|
||||
<productname
|
||||
>NeoChat</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>
|
||||
<command
|
||||
>neochat</command>
|
||||
</refentrytitle>
|
||||
<manvolnum
|
||||
>1</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname
|
||||
>neochat</refname>
|
||||
<refpurpose
|
||||
>Matrix iletileşme protokolü ile etkileşim için istemci</refpurpose>
|
||||
</refnamediv>
|
||||
<!-- body begins here -->
|
||||
<refsynopsisdiv id='synopsis'>
|
||||
<cmdsynopsis
|
||||
><command
|
||||
>neochat</command
|
||||
> <arg choice="opt"
|
||||
><replaceable
|
||||
>URI</replaceable
|
||||
></arg
|
||||
> </cmdsynopsis>
|
||||
</refsynopsisdiv>
|
||||
|
||||
|
||||
<refsect1 id="description">
|
||||
<title
|
||||
>Açıklama</title>
|
||||
<para
|
||||
><command
|
||||
>neochat</command
|
||||
>, Matrix protokolü için hem masaüstünde hem de taşınabilir aygıtlarda çalışan bir sohbet uygulamasıdır. </para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="options"
|
||||
><title
|
||||
>Seçenekler</title>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term
|
||||
><option
|
||||
>URI</option
|
||||
></term>
|
||||
<listitem>
|
||||
<para
|
||||
>Bir kullanıcı veya oda için matrix URI'si; örneğin, matrix:u/kullanıcı:örnek.org ve matrix:r/kök:örnek.org. Bu, NeoChat'in verilen odayı veya konuşmayı açmayı denemesini sağlar. </para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="bug">
|
||||
<title
|
||||
>Hata Bildirme</title>
|
||||
<para
|
||||
>Hataları veya özellik isteklerini <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General"
|
||||
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General</ulink
|
||||
> bağlantısından bildirebilirsiniz</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title
|
||||
>Ayrıca</title>
|
||||
<simplelist>
|
||||
<member
|
||||
>Matrix üzerine sıkça sorulan soruların bir listesi: <ulink url="https://matrix.org/faq/"
|
||||
>https://matrix.org/faq/</ulink
|
||||
> </member>
|
||||
<member
|
||||
>kf5options(7)</member>
|
||||
<member
|
||||
>qt5options(7)</member>
|
||||
</simplelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="copyright"
|
||||
><title
|
||||
>Telif Hakkı</title>
|
||||
<para
|
||||
>Telif hakkı © 2020-2022 Tobias Fella </para>
|
||||
<para
|
||||
>Telif hakkı © 2020-2022 Carl Schwan </para>
|
||||
<para
|
||||
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası <<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
|
||||
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
|
||||
>></para>
|
||||
</refsect1>
|
||||
</refentry>
|
||||
1689
po/tr/neochat.po
1689
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1654
po/uk/neochat.po
1654
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1728
po/zh_CN/neochat.po
1728
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1590
po/zh_TW/neochat.po
1590
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
|
||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
add_library(neochat STATIC
|
||||
@@ -24,7 +24,6 @@ add_library(neochat STATIC
|
||||
models/publicroomlistmodel.cpp
|
||||
models/userdirectorylistmodel.cpp
|
||||
models/keywordnotificationrulemodel.cpp
|
||||
utils.cpp
|
||||
notificationsmanager.cpp
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
chatdocumenthandler.cpp
|
||||
@@ -47,6 +46,7 @@ add_library(neochat STATIC
|
||||
models/statemodel.cpp
|
||||
filetransferpseudojob.cpp
|
||||
models/searchmodel.cpp
|
||||
texthandler.cpp
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
@@ -79,7 +79,7 @@ if(NOT ANDROID)
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp)
|
||||
endif()
|
||||
target_link_libraries(neochat PUBLIC KF5::ConfigWidgets KF5::WindowSystem)
|
||||
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
||||
endif()
|
||||
@@ -87,13 +87,14 @@ endif()
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat-app PRIVATE res_desktop.qrc)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
|
||||
target_sources(neochat PRIVATE runner.cpp)
|
||||
else()
|
||||
target_sources(neochat-app PRIVATE res_android.qrc)
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::SonnetCore KF5::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
@@ -174,9 +175,13 @@ if(ANDROID)
|
||||
"window-new"
|
||||
"globe"
|
||||
"visibility"
|
||||
"home"
|
||||
"preferences-desktop-notification"
|
||||
"computer-symbolic"
|
||||
"gps"
|
||||
)
|
||||
else()
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||
endif()
|
||||
|
||||
@@ -184,12 +189,12 @@ if(NOT ANDROID)
|
||||
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
|
||||
endif()
|
||||
|
||||
if(TARGET KF5::DBusAddons)
|
||||
target_link_libraries(neochat PUBLIC KF5::DBusAddons)
|
||||
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
|
||||
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
|
||||
endif()
|
||||
|
||||
if (TARGET KF5::KIOWidgets)
|
||||
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -20,25 +20,10 @@
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "roommanager.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
QString markdownToHTML(const QString &markdown)
|
||||
{
|
||||
const auto str = markdown.toUtf8();
|
||||
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE);
|
||||
|
||||
const std::string html(tmp_buf);
|
||||
|
||||
free(tmp_buf);
|
||||
|
||||
auto result = QString::fromStdString(html).trimmed();
|
||||
|
||||
result.replace("<!-- raw HTML omitted -->", "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ActionsHandler::ActionsHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -169,7 +154,10 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
}
|
||||
|
||||
handledText = CustomEmojiModel::instance().preprocessText(handledText);
|
||||
handledText = markdownToHTML(handledText);
|
||||
TextHandler textHandler;
|
||||
textHandler.setData(handledText);
|
||||
handledText = textHandler.handleSendText();
|
||||
|
||||
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
||||
handledText.remove("<p>");
|
||||
handledText.remove("</p>");
|
||||
|
||||
@@ -50,5 +50,3 @@ private:
|
||||
QString handleMentions(QString handledText, const bool &isEdit = false);
|
||||
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
||||
};
|
||||
|
||||
QString markdownToHTML(const QString &markdown);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "blurhashimageprovider.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -138,12 +138,13 @@ int ChatDocumentHandler::completionStartIndex() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
|
||||
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
|
||||
const long long cursor = cursorPosition();
|
||||
#else
|
||||
const auto cursor = cursorPosition();
|
||||
#endif
|
||||
const auto &text = getText();
|
||||
|
||||
auto start = std::min(cursor, text.size()) - 1;
|
||||
while (start > -1) {
|
||||
if (text.at(start) == QLatin1Char(' ')) {
|
||||
@@ -324,3 +325,35 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
|
||||
m_room->mentions()->push_back(mention);
|
||||
}
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::mentionColor() const
|
||||
{
|
||||
return m_mentionColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setMentionColor(const QColor &color)
|
||||
{
|
||||
if (m_mentionColor == color) {
|
||||
return;
|
||||
}
|
||||
m_mentionColor = color;
|
||||
m_highlighter->mentionFormat.setForeground(m_mentionColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT mentionColorChanged();
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::errorColor() const
|
||||
{
|
||||
return m_errorColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setErrorColor(const QColor &color)
|
||||
{
|
||||
if (m_errorColor == color) {
|
||||
return;
|
||||
}
|
||||
m_errorColor = color;
|
||||
m_highlighter->errorFormat.setForeground(m_errorColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT errorColorChanged();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ class ChatDocumentHandler : public QObject
|
||||
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
|
||||
|
||||
public:
|
||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||
|
||||
@@ -60,6 +63,13 @@ public:
|
||||
|
||||
void updateCompletions();
|
||||
CompletionModel *completionModel() const;
|
||||
|
||||
[[nodiscard]] QColor mentionColor() const;
|
||||
void setMentionColor(const QColor &color);
|
||||
|
||||
[[nodiscard]] QColor errorColor() const;
|
||||
void setErrorColor(const QColor &color);
|
||||
|
||||
Q_SIGNALS:
|
||||
void isEditChanged();
|
||||
void documentChanged();
|
||||
@@ -68,6 +78,8 @@ Q_SIGNALS:
|
||||
void completionModelChanged();
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void errorColorChanged();
|
||||
void mentionColorChanged();
|
||||
|
||||
private:
|
||||
int completionStartIndex() const;
|
||||
@@ -79,6 +91,9 @@ private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
bool completionVisible = false;
|
||||
|
||||
QColor m_mentionColor;
|
||||
QColor m_errorColor;
|
||||
|
||||
int m_cursorPosition;
|
||||
int m_selectionStart;
|
||||
int m_selectionEnd;
|
||||
|
||||
@@ -61,9 +61,21 @@ QString Clipboard::saveImage(QString localPath) const
|
||||
|
||||
void Clipboard::saveText(QString message)
|
||||
{
|
||||
QRegularExpression re("<[^>]*>");
|
||||
static QRegularExpression re(QStringLiteral("<[^>]*>"));
|
||||
auto *mineData = new QMimeData; // ownership is transferred to clipboard
|
||||
mineData->setHtml(message);
|
||||
mineData->setText(message.replace(re, ""));
|
||||
mineData->setText(message.replace(re, QString()));
|
||||
m_clipboard->setMimeData(mineData);
|
||||
}
|
||||
|
||||
void Clipboard::setImage(const QUrl &url)
|
||||
{
|
||||
if (url.isLocalFile()) {
|
||||
QImage img(url.path());
|
||||
auto *mimeData = new QMimeData;
|
||||
mimeData->setImageData(img);
|
||||
if (!img.isNull()) {
|
||||
m_clipboard->setMimeData(mimeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
Q_INVOKABLE QString saveImage(QString localPath = {}) const;
|
||||
|
||||
Q_INVOKABLE void saveText(QString message);
|
||||
Q_INVOKABLE void setImage(const QUrl &image);
|
||||
|
||||
private:
|
||||
QClipboard *m_clipboard;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "controller.h"
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <csapi/content-repo.h>
|
||||
#include <csapi/logout.h>
|
||||
#include <csapi/profile.h>
|
||||
#include <jobs/downloadfilejob.h>
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
@@ -169,7 +170,9 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
|
||||
auto room = connection->room(notification["room_id"].toString());
|
||||
|
||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
if (room
|
||||
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
|
||||
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||
|
||||
@@ -590,6 +593,7 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
}
|
||||
if (m_connection != nullptr) {
|
||||
disconnect(m_connection, &Connection::syncError, this, nullptr);
|
||||
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
|
||||
}
|
||||
m_connection = connection;
|
||||
if (connection != nullptr) {
|
||||
@@ -608,12 +612,23 @@ void Controller::setActiveConnection(Connection *connection)
|
||||
m_isOnline = true;
|
||||
Q_EMIT isOnlineChanged(true);
|
||||
});
|
||||
connect(connection, &Connection::requestFailed, this, [=](BaseJob *job) {
|
||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"].toString() == "M_TOO_LARGE"_ls) {
|
||||
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
|
||||
}
|
||||
});
|
||||
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
|
||||
if (type == QLatin1String("org.kde.neochat.account_label")) {
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NeoChatConfig::self()->setActiveConnection(QString());
|
||||
}
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT activeConnectionChanged();
|
||||
Q_EMIT activeConnectionIndexChanged();
|
||||
Q_EMIT activeAccountLabelChanged();
|
||||
}
|
||||
|
||||
void Controller::saveWindowGeometry()
|
||||
@@ -636,9 +651,17 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
|
||||
void Controller::createRoom(const QString &name, const QString &topic)
|
||||
{
|
||||
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
|
||||
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [this, createRoomJob] {
|
||||
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
|
||||
Q_EMIT errorOccured(i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
|
||||
});
|
||||
connectSingleShot(
|
||||
this,
|
||||
&Controller::roomAdded,
|
||||
this,
|
||||
[this](NeoChatRoom *room) {
|
||||
RoomManager::instance().enterRoom(room);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
bool Controller::isOnline() const
|
||||
@@ -793,3 +816,22 @@ bool Controller::isFlatpak() const
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Controller::setActiveAccountLabel(const QString &label)
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
QJsonObject json{
|
||||
{"account_label", label},
|
||||
};
|
||||
m_connection->setAccountData("org.kde.neochat.account_label", json);
|
||||
}
|
||||
|
||||
QString Controller::activeAccountLabel() const
|
||||
{
|
||||
if (!m_connection) {
|
||||
return {};
|
||||
}
|
||||
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
|
||||
}
|
||||
@@ -42,6 +42,7 @@ class Controller : public QObject
|
||||
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
|
||||
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
|
||||
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
|
||||
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
|
||||
|
||||
public:
|
||||
static Controller &instance();
|
||||
@@ -103,6 +104,23 @@ public:
|
||||
int quotientMinorVersion() const;
|
||||
bool isFlatpak() const;
|
||||
|
||||
/**
|
||||
* @brief The label for this account.
|
||||
*
|
||||
* Account labels are a concept Specific to NeoChat, allowing accounts to be labelled, e.g. for "Work", "Private", etc.
|
||||
* @return The label, if it exists, otherwise an empty string
|
||||
*/
|
||||
[[nodiscard]] QString activeAccountLabel() const;
|
||||
|
||||
/**
|
||||
* @brief Set the label for this account.
|
||||
*
|
||||
* Set to an empty string to remove the label
|
||||
* @sa Controller::activeAccountLabel
|
||||
* @param label The label to use, or an empty string
|
||||
*/
|
||||
void setActiveAccountLabel(const QString &label);
|
||||
|
||||
private:
|
||||
explicit Controller(QObject *parent = nullptr);
|
||||
|
||||
@@ -151,6 +169,8 @@ Q_SIGNALS:
|
||||
void keyVerificationAccept(const QString &commitment);
|
||||
void keyVerificationKey(const QString &sas);
|
||||
void activeConnectionIndexChanged();
|
||||
void roomAdded(NeoChatRoom *room);
|
||||
void activeAccountLabelChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void logout(Quotient::Connection *conn, bool serverSideLogout);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "filetransferpseudojob.h"
|
||||
@@ -25,6 +25,7 @@ void FileTransferPseudoJob::fileTransferProgress(QString id, qint64 progress, qi
|
||||
|
||||
void FileTransferPseudoJob::fileTransferCompleted(QString id, QUrl localFile)
|
||||
{
|
||||
Q_UNUSED(localFile);
|
||||
if (id != m_eventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "login.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#include "clipboard.h"
|
||||
#include "controller.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "joinrulesevent.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
@@ -157,7 +156,7 @@ int main(int argc, char *argv[])
|
||||
KAboutLicense::GPL_V3,
|
||||
i18n("© 2018-2020 Black Hat, 2020-2022 KDE Community"));
|
||||
about.addAuthor(i18n("Carl Schwan"), i18n("Maintainer"), QStringLiteral("carl@carlschwan.eu"), QStringLiteral("https://carlschwan.eu"));
|
||||
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("fella@posteo.de"), QStringLiteral("https://tobiasfella.de"));
|
||||
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
|
||||
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
|
||||
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
|
||||
about.addCredit(i18n("Alexey Rusakov"), i18n("Maintainer of Quotient"), QStringLiteral("Kitsune-Ral@users.sf.net"));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "actionsmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include "roommanager.h"
|
||||
#include <events/roommemberevent.h>
|
||||
#include <events/roompowerlevelsevent.h>
|
||||
@@ -19,6 +18,41 @@ QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500",
|
||||
"#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff",
|
||||
"#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
|
||||
|
||||
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||
room->connection()->leaveRoom(room);
|
||||
} else {
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto leaving = room->connection()->room(text);
|
||||
if (!leaving) {
|
||||
leaving = room->connection()->roomByAlias(text);
|
||||
}
|
||||
if (leaving) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||
room->connection()->leaveRoom(leaving);
|
||||
} else {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
} else {
|
||||
room->connection()->user()->rename(text, room);
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
QVector<ActionsModel::Action> actions{
|
||||
Action{
|
||||
QStringLiteral("shrug"),
|
||||
@@ -115,7 +149,7 @@ QVector<ActionsModel::Action> actions{
|
||||
QStringLiteral("spoiler"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
|
||||
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
|
||||
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
|
||||
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
|
||||
RoomMessageEvent::MsgType::Text,
|
||||
room->chatBoxReplyId(),
|
||||
@@ -209,6 +243,40 @@ QVector<ActionsModel::Action> actions{
|
||||
kli18n("<room alias or id>"),
|
||||
kli18n("Joins the given room"),
|
||||
},
|
||||
#ifdef QUOTIENT_07
|
||||
Action{
|
||||
QStringLiteral("knock"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
auto parts = text.split(QLatin1String(" "));
|
||||
QString roomName = parts[0];
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(roomName);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
|
||||
if (targetRoom) {
|
||||
RoomManager::instance().enterRoom(dynamic_cast<NeoChatRoom *>(targetRoom));
|
||||
return QString();
|
||||
}
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
|
||||
auto connection = Controller::instance().activeConnection();
|
||||
const auto knownServer = roomName.mid(roomName.indexOf(":") + 1);
|
||||
if (parts.length() >= 2) {
|
||||
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
|
||||
} else {
|
||||
RoomManager::instance().knockRoom(connection, roomName, QString(), QStringList{knownServer});
|
||||
}
|
||||
return QString();
|
||||
},
|
||||
false,
|
||||
std::nullopt,
|
||||
kli18n("<room alias or id> [<reason>]"),
|
||||
kli18n("Requests to join the given room"),
|
||||
},
|
||||
#endif
|
||||
Action{
|
||||
QStringLiteral("j"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
@@ -234,31 +302,7 @@ QVector<ActionsModel::Action> actions{
|
||||
},
|
||||
Action{
|
||||
QStringLiteral("part"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||
room->connection()->leaveRoom(room);
|
||||
} else {
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto leaving = room->connection()->room(text);
|
||||
if (!leaving) {
|
||||
leaving = room->connection()->roomByAlias(text);
|
||||
}
|
||||
if (leaving) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||
room->connection()->leaveRoom(leaving);
|
||||
} else {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
},
|
||||
leaveRoomLambda,
|
||||
false,
|
||||
std::nullopt,
|
||||
kli18n("[<room alias or id>]"),
|
||||
@@ -266,31 +310,7 @@ QVector<ActionsModel::Action> actions{
|
||||
},
|
||||
Action{
|
||||
QStringLiteral("leave"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
|
||||
room->connection()->leaveRoom(room);
|
||||
} else {
|
||||
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
|
||||
auto regexMatch = roomRegex.match(text);
|
||||
if (!regexMatch.hasMatch()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error,
|
||||
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
|
||||
return QString();
|
||||
}
|
||||
auto leaving = room->connection()->room(text);
|
||||
if (!leaving) {
|
||||
leaving = room->connection()->roomByAlias(text);
|
||||
}
|
||||
if (leaving) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
|
||||
room->connection()->leaveRoom(leaving);
|
||||
} else {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
},
|
||||
leaveRoomLambda,
|
||||
false,
|
||||
std::nullopt,
|
||||
kli18n("[<room alias or id>]"),
|
||||
@@ -313,14 +333,15 @@ QVector<ActionsModel::Action> actions{
|
||||
},
|
||||
Action{
|
||||
QStringLiteral("roomnick"),
|
||||
[](const QString &text, NeoChatRoom *room) {
|
||||
if (text.isEmpty()) {
|
||||
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
|
||||
} else {
|
||||
room->connection()->user()->rename(text, room);
|
||||
}
|
||||
return QString();
|
||||
},
|
||||
roomNickLambda,
|
||||
false,
|
||||
std::nullopt,
|
||||
kli18n("<display name>"),
|
||||
kli18n("Changes your display name in this room"),
|
||||
},
|
||||
Action{
|
||||
QStringLiteral("myroomnick"),
|
||||
roomNickLambda,
|
||||
false,
|
||||
std::nullopt,
|
||||
kli18n("<display name>"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -9,20 +9,42 @@
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class ActionsModel
|
||||
*
|
||||
* This class defines a model for chat actions.
|
||||
*
|
||||
* @note A chat action is a message starting with /, resulting in something other
|
||||
* than a normal message being sent (e.g. /me, /join).
|
||||
*/
|
||||
class ActionsModel : public QAbstractListModel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Definition of an action.
|
||||
*/
|
||||
struct Action {
|
||||
// The prefix, without '/' and space after the word
|
||||
QString prefix;
|
||||
QString prefix; /**< The prefix, without '/' and space after the word. */
|
||||
/**
|
||||
* @brief The function to execute when the action is triggered.
|
||||
*/
|
||||
std::function<QString(const QString &, NeoChatRoom *)> handle;
|
||||
// If this is true, this action transforms a message to a different message and it will be sent.
|
||||
// If this is false, this message does some action on the client and should not be sent as a message.
|
||||
/**
|
||||
* @brief Whether the action is a message type action.
|
||||
*
|
||||
* If this is true, a message action will be sent. If this is false, this
|
||||
* message does some action on the client and should not be sent as a message.
|
||||
*/
|
||||
bool messageAction;
|
||||
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
|
||||
/**
|
||||
* @brief The new message type of a message being sent.
|
||||
*
|
||||
* For a non-message action or a message action that outputs the same type
|
||||
* as its input, it's nullopt.
|
||||
*/
|
||||
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
|
||||
KLazyLocalizedString parameters;
|
||||
KLazyLocalizedString description;
|
||||
KLazyLocalizedString parameters; /**< The input parameters expected by the action. */
|
||||
KLazyLocalizedString description; /**< The description of the action. */
|
||||
};
|
||||
static ActionsModel &instance()
|
||||
{
|
||||
@@ -30,18 +52,41 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Prefix = Qt::DisplayRole,
|
||||
Description,
|
||||
CompletionType,
|
||||
Parameters,
|
||||
Prefix = Qt::DisplayRole, /**< The prefix, without '/' and space after the word. */
|
||||
Description, /**< The description of the action. */
|
||||
CompletionType, /**< The completion type (always "action" for this model). */
|
||||
Parameters, /**< The input parameters expected by the action. */
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Return a vector with all supported actions.
|
||||
*/
|
||||
QVector<Action> &allActions() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "collapsestateproxymodel.h"
|
||||
@@ -9,9 +9,9 @@
|
||||
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent);
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
|
||||
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|
||||
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
|
||||
}
|
||||
@@ -47,7 +47,7 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||
if (!uniqueAuthors.contains(nextAuthor)) {
|
||||
uniqueAuthors.append(nextAuthor);
|
||||
}
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
@@ -105,7 +105,7 @@ QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
|
||||
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
|
||||
};
|
||||
stateEvents.append(nextState);
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
@@ -123,7 +123,7 @@ QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
|
||||
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||
uniqueAuthors.append(nextAvatar);
|
||||
}
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -6,26 +6,58 @@
|
||||
#include "messageeventmodel.h"
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class CollapseStateProxyModel
|
||||
*
|
||||
* This model aggregates multiple sequential state events into a single entry.
|
||||
*
|
||||
* Events are only aggregated if they happened on the same day.
|
||||
*
|
||||
* @sa MessageEventModel
|
||||
*/
|
||||
class CollapseStateProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
||||
StateEventsRole,
|
||||
AuthorListRole,
|
||||
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
|
||||
StateEventsRole, /**< List of state events in the aggregated state. */
|
||||
AuthorListRole, /**< List of unique authors of the aggregated state event. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @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;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief QString aggregating the text of consecutive state events starting at row.
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractProxyModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Aggregation of the text of consecutive state events starting at row.
|
||||
*
|
||||
* If state events happen on different days they will be split into two aggregate
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||
|
||||
/**
|
||||
* @brief Return a list of consecutive state events starting at row.
|
||||
*
|
||||
@@ -33,6 +65,7 @@ public:
|
||||
* events.
|
||||
*/
|
||||
[[nodiscard]] QVariantList stateEventsList(int row) const;
|
||||
|
||||
/**
|
||||
* @brief List of unique authors for the aggregate state events starting at row.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "completionmodel.h"
|
||||
#include <QDebug>
|
||||
|
||||
#include "actionsmodel.h"
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "completionproxymodel.h"
|
||||
#include "customemojimodel.h"
|
||||
#include "emojimodel.h"
|
||||
@@ -145,7 +144,7 @@ void CompletionModel::updateCompletion()
|
||||
m_filterModel->setFullText(m_fullText);
|
||||
m_filterModel->setFilterText(m_text);
|
||||
m_filterModel->invalidate();
|
||||
} else if (text().startsWith(QLatin1Char(':'))
|
||||
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
|
||||
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|
||||
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
|
||||
m_filterModel->setSourceModel(m_emojiModel);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -13,41 +13,89 @@ class UserListModel;
|
||||
class NeoChatRoom;
|
||||
class RoomListModel;
|
||||
|
||||
/**
|
||||
* @class CompletionModel
|
||||
*
|
||||
* This class defines the model for suggesting completions in chat text.
|
||||
*
|
||||
* This model is able to select the appropriate completion type for the input text
|
||||
* and present a list of options that can be presented to the user.
|
||||
*/
|
||||
class CompletionModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current text to search for completions.
|
||||
*/
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged)
|
||||
|
||||
/**
|
||||
* @brief The current room that the model is getting completions for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The current type of completion being done on the entered text.
|
||||
*
|
||||
* @sa AutoCompletionType
|
||||
*/
|
||||
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
|
||||
|
||||
/**
|
||||
* @brief The RoomListModel to be used for room completions.
|
||||
*/
|
||||
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the different types of completion available.
|
||||
*/
|
||||
enum AutoCompletionType {
|
||||
User,
|
||||
Room,
|
||||
Emoji,
|
||||
Command,
|
||||
None,
|
||||
User, /**< A user in the current room. */
|
||||
Room, /**< A matrix room. */
|
||||
Emoji, /**< An emoji. */
|
||||
Command, /**< A / command. */
|
||||
None, /**< No available completion for the current text. */
|
||||
};
|
||||
Q_ENUM(AutoCompletionType)
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Text = Qt::DisplayRole,
|
||||
Subtitle,
|
||||
Icon,
|
||||
ReplacedText,
|
||||
Text = Qt::DisplayRole, /**< The main text to show. */
|
||||
Subtitle, /**< The subtitle text to show. */
|
||||
Icon, /**< The icon to show. */
|
||||
ReplacedText, /**< The text to replace the input text with for the completion. */
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
CompletionModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
QString text() const;
|
||||
void setText(const QString &text, const QString &fullText);
|
||||
void updateCompletion();
|
||||
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
@@ -56,6 +104,7 @@ public:
|
||||
void setRoomListModel(RoomListModel *roomListModel);
|
||||
|
||||
AutoCompletionType autoCompletionType() const;
|
||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||
|
||||
Q_SIGNALS:
|
||||
void textChanged();
|
||||
@@ -70,7 +119,7 @@ private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
AutoCompletionType m_autoCompletionType = None;
|
||||
|
||||
void setAutoCompletionType(AutoCompletionType autoCompletionType);
|
||||
void updateCompletion();
|
||||
|
||||
UserListModel *m_userListModel;
|
||||
RoomListModel *m_roomListModel;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "completionproxymodel.h"
|
||||
#include <QDebug>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include <QDebug>
|
||||
|
||||
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
@@ -44,7 +43,6 @@ int CompletionProxyModel::secondaryFilterRole() const
|
||||
void CompletionProxyModel::setSecondaryFilterRole(int role)
|
||||
{
|
||||
m_secondaryFilterRole = role;
|
||||
Q_EMIT secondaryFilterRoleChanged();
|
||||
}
|
||||
|
||||
QString CompletionProxyModel::filterText() const
|
||||
@@ -55,7 +53,6 @@ QString CompletionProxyModel::filterText() const
|
||||
void CompletionProxyModel::setFilterText(const QString &filterText)
|
||||
{
|
||||
m_filterText = filterText;
|
||||
Q_EMIT filterTextChanged();
|
||||
}
|
||||
|
||||
void CompletionProxyModel::setFullText(const QString &fullText)
|
||||
|
||||
@@ -1,32 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class CompletionProxyModel
|
||||
*
|
||||
* A filter model to sort and filter completion results.
|
||||
*
|
||||
* This model is designed to work with multiple source models depending upon the
|
||||
* completion type.
|
||||
*
|
||||
* A model value will be shown if its primary or secondary role values start with
|
||||
* the filter text. The exception is if the full text perfectly matches
|
||||
* the primary filter role value in which case the completion ends (i.e. the filter
|
||||
* will return no results).
|
||||
*
|
||||
* @note The filter is primarily design to work with strings, therefore make sure
|
||||
* that the source model roles that are to be filtered are strings.
|
||||
*/
|
||||
class CompletionProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int secondaryFilterRole READ secondaryFilterRole WRITE setSecondaryFilterRole NOTIFY secondaryFilterRoleChanged)
|
||||
Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Wether a row should be shown or not.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::filterAcceptsRow
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns true if the value of source_left is less than source_right.
|
||||
*
|
||||
* @sa QSortFilterProxyModel::lessThan
|
||||
*/
|
||||
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||
|
||||
/**
|
||||
* @brief Get the current secondary filter role.
|
||||
*/
|
||||
int secondaryFilterRole() const;
|
||||
|
||||
/**
|
||||
* @brief Set the secondary filter role.
|
||||
*
|
||||
* Refer to the source model for what value corresponds to what role.
|
||||
*/
|
||||
void setSecondaryFilterRole(int role);
|
||||
|
||||
/**
|
||||
* @brief Get the current text being used to filter the source model.
|
||||
*/
|
||||
QString filterText() const;
|
||||
|
||||
/**
|
||||
* @brief Set the text to be used to filter the source model.
|
||||
*/
|
||||
void setFilterText(const QString &filterText);
|
||||
|
||||
/**
|
||||
* @brief Set the full text in the chatbar after the completion start.
|
||||
*
|
||||
* This is used to automatically end the completion if the user replicated the
|
||||
* primary filter role value perfectly.
|
||||
*/
|
||||
void setFullText(const QString &fullText);
|
||||
|
||||
Q_SIGNALS:
|
||||
void secondaryFilterRoleChanged();
|
||||
void filterTextChanged();
|
||||
|
||||
private:
|
||||
int m_secondaryFilterRole = -1;
|
||||
QString m_filterText;
|
||||
|
||||
@@ -58,7 +58,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
||||
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
|
||||
|
||||
if (running(job)) {
|
||||
connect(job, &BaseJob::success, this, [this, name, job] {
|
||||
connect(job, &BaseJob::success, this, [name, job] {
|
||||
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
|
||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
||||
auto emojiData = json["images"].toObject();
|
||||
@@ -141,6 +141,8 @@ QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
return QUrl(QStringLiteral("image://mxc/") + data.url.mid(6));
|
||||
case Roles::MxcUrl:
|
||||
return data.url.mid(6);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
|
||||
@@ -17,19 +17,29 @@ struct CustomEmoji {
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class CustomEmojiModel
|
||||
*
|
||||
* This class defines the model for custom user emojis.
|
||||
*
|
||||
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
||||
*/
|
||||
class CustomEmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Name = Qt::DisplayRole,
|
||||
ImageURL,
|
||||
ModelData, // for emulating the regular emoji model's usage, otherwise the UI code would get too complicated
|
||||
MxcUrl = 50,
|
||||
DisplayRole = 51,
|
||||
ReplacedTextRole = 52,
|
||||
DescriptionRole = 53, // also invalid, reserved
|
||||
Name = Qt::DisplayRole, /**< The name of the emoji. */
|
||||
ImageURL, /**< The URL for the custom emoji. */
|
||||
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
|
||||
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
|
||||
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
|
||||
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
|
||||
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
@@ -39,14 +49,45 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Substitute any custom emojis for an image in the input text.
|
||||
*/
|
||||
Q_INVOKABLE QString preprocessText(const QString &it);
|
||||
|
||||
/**
|
||||
* @brief Return a list of custom emojis where the name contains the filter text.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
||||
|
||||
/**
|
||||
* @brief Add a new emoji to the model.
|
||||
*/
|
||||
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
||||
|
||||
/**
|
||||
* @brief Remove an emoji from the model.
|
||||
*/
|
||||
Q_INVOKABLE void removeEmoji(const QString &name);
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "devicesmodel.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -12,28 +12,67 @@ namespace Quotient
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class DevicesModel
|
||||
*
|
||||
* This class defines the model for managing the devices of the local user.
|
||||
*
|
||||
* A device is any session where the local user is logged into a client. This means
|
||||
* the same physical device can have multiple sessions for example if the user uses
|
||||
* multiple clients on the same machine.
|
||||
*/
|
||||
class DevicesModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its devices from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
Id,
|
||||
DisplayName,
|
||||
LastIp,
|
||||
LastTimestamp,
|
||||
Id, /**< The device ID. */
|
||||
DisplayName, /**< Display name set by the user for this device. */
|
||||
LastIp, /**< The IP address where this device was last seen. */
|
||||
LastTimestamp, /**< The timestamp when this devices was last seen. */
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
DevicesModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Logout the device at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void logout(int index, const QString &password);
|
||||
|
||||
/**
|
||||
* @brief Set the display name of the device at the given index.
|
||||
*/
|
||||
Q_INVOKABLE void setName(int index, const QString &name);
|
||||
|
||||
Quotient::Connection *connection() const;
|
||||
|
||||
@@ -51,12 +51,30 @@ struct Emoji {
|
||||
|
||||
Q_DECLARE_METATYPE(Emoji)
|
||||
|
||||
/**
|
||||
* @class EmojiModel
|
||||
*
|
||||
* This class defines the model for visualising a list of emojis.
|
||||
*/
|
||||
class EmojiModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief Return a list of recently used emojis.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji categories.
|
||||
*
|
||||
* @note No custom emoji categories will be included.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji categories with custom emojis.
|
||||
*/
|
||||
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
|
||||
|
||||
public:
|
||||
@@ -66,47 +84,92 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum RoleNames {
|
||||
ShortNameRole = Qt::DisplayRole,
|
||||
UnicodeRole,
|
||||
InvalidRole = 50,
|
||||
DisplayRole = 51,
|
||||
ReplacedTextRole = 52,
|
||||
DescriptionRole = 53,
|
||||
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
|
||||
UnicodeRole, /**< The unicode character of the emoji. */
|
||||
InvalidRole = 50, /**< Invalid, reserved. */
|
||||
DisplayRole = 51, /**< The display text for an emoji. */
|
||||
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
|
||||
DescriptionRole = 53, /**< The long description of an emoji. */
|
||||
};
|
||||
Q_ENUM(RoleNames);
|
||||
|
||||
/**
|
||||
* @brief Defines the potential categories an emoji can be placed in.
|
||||
*/
|
||||
enum Category {
|
||||
Custom,
|
||||
Search,
|
||||
SearchNoCustom,
|
||||
History,
|
||||
HistoryNoCustom,
|
||||
Smileys,
|
||||
People,
|
||||
Nature,
|
||||
Food,
|
||||
Activities,
|
||||
Travel,
|
||||
Objects,
|
||||
Symbols,
|
||||
Flags,
|
||||
Component,
|
||||
Custom, /**< A custom user emoji. */
|
||||
Search, /**< The results of a filter. */
|
||||
SearchNoCustom, /**< The results of a filter with no custom emojis. */
|
||||
History, /**< Recently used emojis. */
|
||||
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
|
||||
Smileys, /**< Smileys & emotion emojis. */
|
||||
People, /**< People & Body emojis. */
|
||||
Nature, /**< Animals & Nature emojis. */
|
||||
Food, /**< Food & Drink emojis. */
|
||||
Activities, /**< Activities emojis. */
|
||||
Travel, /**< Travel & Places emojis. */
|
||||
Objects, /**< Objects emojis. */
|
||||
Symbols, /**< Symbols emojis. */
|
||||
Flags, /**< Flags emojis. */
|
||||
Component, /**< ??? */
|
||||
};
|
||||
Q_ENUM(Category)
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
/**
|
||||
* @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 Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa RoleNames, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE QVariantList history() const;
|
||||
/**
|
||||
* @brief Return a filtered list of emojis.
|
||||
*
|
||||
* @note This includes custom emojis, use filterModelNoCustom to return a result
|
||||
* without custom emojis.
|
||||
*
|
||||
* @sa filterModelNoCustom
|
||||
*/
|
||||
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
|
||||
|
||||
/**
|
||||
* @brief Return a filtered list of emojis without custom emojis.
|
||||
*
|
||||
* @note Use filterModel to return a result with custom emojis.
|
||||
*
|
||||
* @sa filterModel
|
||||
*/
|
||||
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
|
||||
|
||||
/**
|
||||
* @brief Return a list of emojis for the given category.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList emojis(Category category) const;
|
||||
|
||||
/**
|
||||
* @brief Return a list of emoji tones for the given base emoji.
|
||||
*/
|
||||
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
|
||||
|
||||
Q_INVOKABLE QVariantList history() const;
|
||||
QVariantList categories() const;
|
||||
QVariantList categoriesWithCustom() const;
|
||||
|
||||
|
||||
@@ -7,23 +7,54 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
/**
|
||||
* @class KeywordNotificationRuleModel
|
||||
*
|
||||
* This class defines the model for managing notification push rule keywords.
|
||||
*/
|
||||
class KeywordNotificationRuleModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::DisplayRole,
|
||||
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
|
||||
};
|
||||
|
||||
KeywordNotificationRuleModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Add a new keyword to the model.
|
||||
*/
|
||||
Q_INVOKABLE void addKeyword(const QString &keyword);
|
||||
|
||||
/**
|
||||
* @brief Remove a keyword from the model.
|
||||
*/
|
||||
Q_INVOKABLE void removeKeywordAtIndex(int index);
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
@@ -27,14 +27,13 @@
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "neochatuser.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
|
||||
roles[EventTypeRole] = "eventType";
|
||||
roles[DelegateTypeRole] = "delegateType";
|
||||
roles[MessageRole] = "message";
|
||||
roles[EventIdRole] = "eventId";
|
||||
roles[TimeRole] = "time";
|
||||
@@ -46,16 +45,16 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[SpecialMarksRole] = "marks";
|
||||
roles[LongOperationRole] = "progressInfo";
|
||||
roles[FileMimetypeIcon] = "fileMimetypeIcon";
|
||||
roles[AnnotationRole] = "annotation";
|
||||
roles[EventResolvedTypeRole] = "eventResolvedType";
|
||||
roles[IsReplyRole] = "isReply";
|
||||
roles[ReplyRole] = "reply";
|
||||
roles[ReplyIdRole] = "replyId";
|
||||
roles[UserMarkerRole] = "userMarker";
|
||||
roles[ShowAuthorRole] = "showAuthor";
|
||||
roles[ShowSectionRole] = "showSection";
|
||||
roles[ReadMarkersRole] = "readMarkers";
|
||||
roles[ReadMarkersStringRole] = "readMarkersString";
|
||||
roles[ShowReadMarkersRole] = "showReadMarkers";
|
||||
roles[ReactionRole] = "reaction";
|
||||
roles[IsEditedRole] = "isEdited";
|
||||
roles[SourceRole] = "source";
|
||||
roles[MimeTypeRole] = "mimeType";
|
||||
roles[FormattedBodyRole] = "formattedBody";
|
||||
@@ -64,16 +63,17 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
||||
roles[VerifiedRole] = "verified";
|
||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||
roles[IsNameChangeRole] = "isNameChange";
|
||||
roles[IsAvatarChangeRole] = "isAvatarChange";
|
||||
roles[IsRedactedRole] = "isRedacted";
|
||||
roles[GenericDisplayRole] = "genericDisplay";
|
||||
roles[IsPendingRole] = "isPending";
|
||||
roles[LatitudeRole] = "latitude";
|
||||
roles[LongitudeRole] = "longitude";
|
||||
roles[AssetRole] = "asset";
|
||||
return roles;
|
||||
}
|
||||
|
||||
MessageEventModel::MessageEventModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_currentRoom(nullptr)
|
||||
{
|
||||
using namespace Quotient;
|
||||
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
|
||||
@@ -86,6 +86,11 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
|
||||
MessageEventModel::~MessageEventModel() = default;
|
||||
|
||||
NeoChatRoom *MessageEventModel::room() const
|
||||
{
|
||||
return m_currentRoom;
|
||||
}
|
||||
|
||||
void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
if (room == m_currentRoom) {
|
||||
@@ -176,6 +181,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
});
|
||||
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
|
||||
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) {
|
||||
Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole});
|
||||
if (i == 0) {
|
||||
return; // No need to move anything, just refresh
|
||||
}
|
||||
@@ -214,6 +220,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
||||
}
|
||||
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
|
||||
});
|
||||
connect(m_currentRoom, &Room::changed, this, [this]() {
|
||||
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
|
||||
auto event = it->event();
|
||||
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole});
|
||||
}
|
||||
});
|
||||
connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
|
||||
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
|
||||
@@ -306,7 +318,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
|
||||
return -1;
|
||||
}
|
||||
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
||||
if (data(index(row, 0), EventTypeRole).toInt() == ReadMarker || data(index(row, 0), EventTypeRole).toInt() == Other) {
|
||||
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
|
||||
row++;
|
||||
}
|
||||
}
|
||||
@@ -437,7 +449,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (m_lastReadEventIndex.row() == row) {
|
||||
switch (role) {
|
||||
case EventTypeRole:
|
||||
case DelegateTypeRole:
|
||||
return DelegateType::ReadMarker;
|
||||
case TimeRole: {
|
||||
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
|
||||
@@ -486,10 +498,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == SourceRole) {
|
||||
return evt.originalJson();
|
||||
return QJsonDocument(evt.fullJson()).toJson();
|
||||
}
|
||||
|
||||
if (role == EventTypeRole) {
|
||||
if (role == DelegateTypeRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
switch (e->msgtype()) {
|
||||
case MessageEventType::Emote:
|
||||
@@ -502,6 +514,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return DelegateType::Audio;
|
||||
case MessageEventType::Video:
|
||||
return DelegateType::Video;
|
||||
case MessageEventType::Location:
|
||||
return DelegateType::Location;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -556,6 +570,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
if (e->msgtype() == Quotient::MessageEventType::Location) {
|
||||
return e->contentJson();
|
||||
}
|
||||
// Cannot use e.contentJson() here because some
|
||||
// EventContent classes inject values into the copy of the
|
||||
// content JSON stored in EventContent::Base
|
||||
@@ -569,7 +586,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == HighlightRole) {
|
||||
return m_currentRoom->isEventHighlighted(&evt);
|
||||
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
|
||||
}
|
||||
|
||||
if (role == FileMimetypeIcon) {
|
||||
@@ -597,12 +614,25 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
if (role == SpecialMarksRole) {
|
||||
if (isPending) {
|
||||
// A pending event with an m.new_content key will be merged into the
|
||||
// original event so don't show.
|
||||
if (evt.contentJson().contains("m.new_content")) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
return pendingIt->deliveryStatus();
|
||||
}
|
||||
|
||||
auto *memberEvent = timelineIt->viewAs<RoomMemberEvent>();
|
||||
if (memberEvent) {
|
||||
if ((memberEvent->isJoin() || memberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
|
||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||
return EventStatus::Hidden;
|
||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||
return EventStatus::Hidden;
|
||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
}
|
||||
@@ -633,14 +663,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return EventStatus::Normal;
|
||||
}
|
||||
|
||||
if (role == IsEditedRole) {
|
||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||
return !e->unsignedJson().isEmpty() && e->unsignedJson().contains("m.relations")
|
||||
&& e->unsignedJson()["m.relations"].toObject().contains("m.replace");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == EventIdRole) {
|
||||
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
|
||||
}
|
||||
@@ -656,29 +678,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
}
|
||||
|
||||
if (role == AnnotationRole) {
|
||||
if (isPending) {
|
||||
return pendingIt->annotation();
|
||||
}
|
||||
}
|
||||
|
||||
if (role == TimeRole || role == SectionRole) {
|
||||
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
|
||||
return role == TimeRole ? QVariant(ts) : renderDate(ts);
|
||||
}
|
||||
|
||||
if (role == UserMarkerRole) {
|
||||
QVariantList variantList;
|
||||
const auto users = m_currentRoom->usersAtEventId(evt.id());
|
||||
for (User *user : users) {
|
||||
if (user == m_currentRoom->localUser()) {
|
||||
continue;
|
||||
}
|
||||
variantList.append(QVariant::fromValue(user));
|
||||
}
|
||||
return variantList;
|
||||
}
|
||||
|
||||
if (role == IsReplyRole) {
|
||||
return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
|
||||
}
|
||||
@@ -764,7 +768,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) == MessageEventModel::State
|
||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
|
||||
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|
||||
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
|
||||
}
|
||||
@@ -789,6 +793,91 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == LatitudeRole) {
|
||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
if (role == LongitudeRole) {
|
||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
||||
if (geoUri.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
||||
return latitude.toFloat();
|
||||
}
|
||||
|
||||
if (role == AssetRole) {
|
||||
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"].toObject()["type"].toString();
|
||||
if (assetType.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return assetType;
|
||||
}
|
||||
|
||||
if (role == ReadMarkersRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
|
||||
QVariantList users;
|
||||
users.reserve(userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||
#else
|
||||
auto user = static_cast<NeoChatUser *>(userId);
|
||||
#endif
|
||||
users += userAtEvent(user, m_currentRoom, evt);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
if (role == ReadMarkersStringRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
/**
|
||||
* The string ends up in the form
|
||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||
*/
|
||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||
for (const auto &userId : userIds) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
|
||||
#else
|
||||
auto user = static_cast<NeoChatUser *>(userId);
|
||||
#endif
|
||||
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
|
||||
}
|
||||
readMarkersString.chop(2);
|
||||
return readMarkersString;
|
||||
}
|
||||
|
||||
if (role == ShowReadMarkersRole) {
|
||||
#ifdef QUOTIENT_07
|
||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
||||
userIds.remove(m_currentRoom->localUser()->id());
|
||||
#else
|
||||
auto userIds = room()->usersAtEventId(evt.id());
|
||||
userIds.removeAll(m_currentRoom->localUser());
|
||||
#endif
|
||||
return userIds.size() > 0;
|
||||
}
|
||||
|
||||
if (role == ReactionRole) {
|
||||
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
|
||||
if (annotations.isEmpty()) {
|
||||
@@ -889,29 +978,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
}
|
||||
|
||||
if (role == IsNameChangeRole) {
|
||||
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
|
||||
if (roomMemberEvent) {
|
||||
return roomMemberEvent->isRename();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role == IsAvatarChangeRole) {
|
||||
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
|
||||
if (roomMemberEvent) {
|
||||
return roomMemberEvent->isAvatarUpdate();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (role == IsRedactedRole) {
|
||||
return evt.isRedacted();
|
||||
}
|
||||
|
||||
if (role == IsPendingRole) {
|
||||
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int MessageEventModel::eventIDToIndex(const QString &eventID) const
|
||||
int MessageEventModel::eventIdToRow(const QString &eventID) const
|
||||
{
|
||||
const auto it = m_currentRoom->findInTimeline(eventID);
|
||||
if (it == m_currentRoom->historyEdge()) {
|
||||
@@ -953,7 +1031,7 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
|
||||
targetMessage.insert("event_id", eventId);
|
||||
targetMessage.insert("formattedBody", content["formatted_body"].toString());
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
||||
QModelIndex idx = index(eventIdToRow(eventId), 0);
|
||||
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
|
||||
|
||||
return targetMessage;
|
||||
@@ -963,18 +1041,21 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
|
||||
return targetMessage;
|
||||
}
|
||||
|
||||
QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
|
||||
QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
|
||||
{
|
||||
QVariantMap replyResponse;
|
||||
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + baseline;
|
||||
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow;
|
||||
|
||||
// set a cap limit of baseline + 35 messages, to prevent loading a lot of messages
|
||||
// set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
|
||||
// in rooms where the user has not sent many messages
|
||||
const auto limit = timelineBottom + std::min(baseline + 35, m_currentRoom->timelineSize());
|
||||
const auto limit = timelineBottom + std::min(startRow + 35, m_currentRoom->timelineSize());
|
||||
|
||||
for (auto it = timelineBottom; it != limit; ++it) {
|
||||
auto evt = it->event();
|
||||
auto e = eventCast<const RoomMessageEvent>(evt);
|
||||
if (!e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto content = (*it)->contentJson();
|
||||
|
||||
@@ -989,7 +1070,7 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
|
||||
}
|
||||
replyResponse.insert("event_id", eventId);
|
||||
// Need to get the message from the original eventId or body will have * on the front
|
||||
QModelIndex idx = index(eventIDToIndex(eventId), 0);
|
||||
QModelIndex idx = index(eventIdToRow(eventId), 0);
|
||||
replyResponse.insert("message", idx.data(Qt::UserRole + 2));
|
||||
replyResponse.insert("sender_id", QVariant::fromValue(m_currentRoom->getUser((*it)->senderId())));
|
||||
replyResponse.insert("at", -it->index());
|
||||
|
||||
@@ -7,72 +7,99 @@
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
/**
|
||||
* @class MessageEventModel
|
||||
*
|
||||
* This class defines the model for visualising the room timeline.
|
||||
*
|
||||
* This model covers all event types in the timeline 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.
|
||||
*
|
||||
* @sa NeoChatRoom
|
||||
*/
|
||||
class MessageEventModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current room that the model is getting its messages from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The type of delegate that is needed for the event.
|
||||
*
|
||||
* @note While similar this is not the matrix event or message type. This is
|
||||
* to tell a QML ListView what delegate to show for each event. So while
|
||||
* similar to the spec it is not the same.
|
||||
*/
|
||||
enum DelegateType {
|
||||
Emote,
|
||||
Notice,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
File,
|
||||
Message,
|
||||
Sticker,
|
||||
State,
|
||||
Encrypted,
|
||||
ReadMarker,
|
||||
Poll,
|
||||
Other,
|
||||
Emote, /**< A message that begins with /me. */
|
||||
Notice, /**< A notice event. */
|
||||
Image, /**< A message that is an image. */
|
||||
Audio, /**< A message that is an audio recording. */
|
||||
Video, /**< A message that is a video. */
|
||||
File, /**< A message that is a file. */
|
||||
Message, /**< A text message. */
|
||||
Sticker, /**< A message that is a sticker. */
|
||||
State, /**< A state event in the room. */
|
||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||
ReadMarker, /**< The local user read marker. */
|
||||
Poll, /**< The initial event for a poll. */
|
||||
Location, /**< A location event. */
|
||||
Other, /**< Anything that cannot be classified as another type. */
|
||||
};
|
||||
Q_ENUM(DelegateType);
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
EventTypeRole = Qt::UserRole + 1,
|
||||
MessageRole,
|
||||
EventIdRole,
|
||||
TimeRole,
|
||||
SectionRole,
|
||||
AuthorRole,
|
||||
ContentRole,
|
||||
ContentTypeRole,
|
||||
HighlightRole,
|
||||
SpecialMarksRole,
|
||||
LongOperationRole,
|
||||
AnnotationRole,
|
||||
UserMarkerRole,
|
||||
FormattedBodyRole,
|
||||
GenericDisplayRole,
|
||||
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||
MessageRole, /**< Plain text representation of the message. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
TimeRole, /**< The timestamp for when the event was sent. */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ContentRole, /**< The full message content. */
|
||||
ContentTypeRole, /**< The content mime type. */
|
||||
HighlightRole, /**< Whether the event should be highlighted. */
|
||||
SpecialMarksRole, /**< Whether the event is hidden or not. */
|
||||
LongOperationRole, /**< Progress info when downloading files. */
|
||||
FormattedBodyRole, /**< The formatted body of a rich message. */
|
||||
GenericDisplayRole, /**< A generic string based upon the message type. */
|
||||
|
||||
MimeTypeRole,
|
||||
FileMimetypeIcon,
|
||||
MimeTypeRole, /**< The mime type of the message's file or media. */
|
||||
FileMimetypeIcon, /**< The icon name for the mime type of a file. */
|
||||
|
||||
IsReplyRole,
|
||||
ReplyRole,
|
||||
ReplyIdRole,
|
||||
IsReplyRole, /**< Is the message a reply to another event. */
|
||||
ReplyRole, /**< The content data of the message that was replied to. */
|
||||
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
|
||||
|
||||
ShowAuthorRole,
|
||||
ShowSectionRole,
|
||||
ShowAuthorRole, /**< Whether the author's name should be shown. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
|
||||
ReactionRole,
|
||||
ReadMarkersRole, /**< Other users at the event for read marker tracking. */
|
||||
ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */
|
||||
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
|
||||
ReactionRole, /**< List of reactions to this event. */
|
||||
SourceRole, /**< The full message source JSON. */
|
||||
MediaUrlRole, /**< The source URL for any media in the message. */
|
||||
|
||||
IsEditedRole,
|
||||
SourceRole,
|
||||
MediaUrlRole,
|
||||
// For debugging
|
||||
EventResolvedTypeRole,
|
||||
AuthorIdRole,
|
||||
VerifiedRole,
|
||||
// Sender's displayname, always without the matrix id
|
||||
DisplayNameForInitialsRole,
|
||||
// The displayname for the event's sender; for name change events, the old displayname
|
||||
AuthorDisplayNameRole,
|
||||
IsNameChangeRole,
|
||||
IsAvatarChangeRole,
|
||||
IsRedactedRole,
|
||||
EventResolvedTypeRole, /**< The event type the message. */
|
||||
AuthorIdRole, /**< Matrix ID of the message author. */
|
||||
|
||||
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
||||
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
|
||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||
LatitudeRole, /**< Latitude for a location event. */
|
||||
LongitudeRole, /**< Longitude for a location event. */
|
||||
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
|
||||
LastRole, // Keep this last
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
@@ -80,20 +107,67 @@ public:
|
||||
explicit MessageEventModel(QObject *parent = nullptr);
|
||||
~MessageEventModel() override;
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const
|
||||
{
|
||||
return m_currentRoom;
|
||||
}
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
/**
|
||||
* @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 Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const;
|
||||
/**
|
||||
* @brief Get the row number of the given event ID in the model.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
|
||||
/**
|
||||
* @brief Get the last message sent by the local user.
|
||||
*
|
||||
* @note This checks a maximum of the previous 35 message for performance reasons.
|
||||
*
|
||||
* @return a QVariantMap for the event with the following parameters:
|
||||
* - eventId - The event ID.
|
||||
* - formattedBody - The message text formatted as Qt::RichText.
|
||||
* - message - The message text formatted as Qt::PlainText.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
|
||||
Q_INVOKABLE void loadReply(const QModelIndex &row);
|
||||
|
||||
/**
|
||||
* @brief Get the last message sent earlier than the given row.
|
||||
*
|
||||
* @note This checks a maximum of the previous 35 message for performance reasons.
|
||||
*
|
||||
* @return a QVariantMap for the event with the following parameters:
|
||||
* - eventId - The event ID.
|
||||
* - message - The message text formatted as Qt::PlainText.
|
||||
* - sender_id - The matrix ID of the sender.
|
||||
* - at - The QModelIndex of the message.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromRow(const int startRow);
|
||||
|
||||
/**
|
||||
* @brief Load the event that the item at the given index replied to.
|
||||
*
|
||||
* This is used to ensure that the reply data is available when the message that
|
||||
* was replied to is outside the currently loaded timeline.
|
||||
*/
|
||||
Q_INVOKABLE void loadReply(const QModelIndex &index);
|
||||
|
||||
private Q_SLOTS:
|
||||
int refreshEvent(const QString &eventId);
|
||||
|
||||
@@ -11,23 +11,20 @@ using namespace Quotient;
|
||||
MessageFilterModel::MessageFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||
invalidateFilter();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
invalidateFilter();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,31 +32,20 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
||||
{
|
||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
|
||||
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
|
||||
if (index.data(MessageEventModel::IsNameChangeRole).toBool() && !NeoChatConfig::self()->showRename()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index.data(MessageEventModel::IsAvatarChangeRole).toBool() && !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||
return false;
|
||||
}
|
||||
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
|
||||
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt();
|
||||
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
|
||||
|
||||
if (eventType == MessageEventModel::Other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,21 @@
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @class MessageFilterModel
|
||||
*
|
||||
* This model filters out any messages that should be hidden.
|
||||
*
|
||||
* Deleted messages are only hidden if the user hasn't set them to be shown.
|
||||
*/
|
||||
class MessageFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
MessageFilterModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Custom filter function to remove hidden messages.
|
||||
*/
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,11 @@ PublicRoomListModel::PublicRoomListModel(QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
Quotient::Connection *PublicRoomListModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setConnection(Connection *conn)
|
||||
{
|
||||
if (m_connection == conn) {
|
||||
@@ -47,6 +52,11 @@ void PublicRoomListModel::setConnection(Connection *conn)
|
||||
Q_EMIT hasMoreChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::server() const
|
||||
{
|
||||
return m_server;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setServer(const QString &value)
|
||||
{
|
||||
if (m_server == value) {
|
||||
@@ -76,6 +86,11 @@ void PublicRoomListModel::setServer(const QString &value)
|
||||
Q_EMIT hasMoreChanged();
|
||||
}
|
||||
|
||||
QString PublicRoomListModel::keyword() const
|
||||
{
|
||||
return m_keyword;
|
||||
}
|
||||
|
||||
void PublicRoomListModel::setKeyword(const QString &value)
|
||||
{
|
||||
if (m_keyword == value) {
|
||||
|
||||
@@ -13,54 +13,96 @@ namespace Quotient
|
||||
class Connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PublicRoomListModel
|
||||
*
|
||||
* This class defines the model for visualising a list of public rooms.
|
||||
*
|
||||
* The model finds the public rooms visible to the given server (which doesn't have
|
||||
* to be the user's home server) and can also apply a filter if desired.
|
||||
*
|
||||
* Due to the fact that the public room list could be huge the model is lazily loaded
|
||||
* and requires that the next batch of rooms be manually called.
|
||||
*/
|
||||
class PublicRoomListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its rooms from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The server to get the public room list from.
|
||||
*/
|
||||
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
|
||||
|
||||
/**
|
||||
* @brief The filter keyword for the list of public rooms.
|
||||
*/
|
||||
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model has more items to load.
|
||||
*/
|
||||
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::DisplayRole + 1,
|
||||
AvatarRole,
|
||||
TopicRole,
|
||||
RoomIDRole,
|
||||
AliasRole,
|
||||
MemberCountRole,
|
||||
AllowGuestsRole,
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
NameRole = Qt::DisplayRole + 1, /**< The name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
TopicRole, /**< The room topic. */
|
||||
RoomIDRole, /**< The room matrix ID. */
|
||||
AliasRole, /**< The room canonical alias. */
|
||||
MemberCountRole, /**< The number of members in the room. */
|
||||
AllowGuestsRole, /**< Whether the room allows guest users. */
|
||||
WorldReadableRole, /**< Whether the room events can be seen by non-members. */
|
||||
IsJoinedRole, /**< Whether the local user has joined the room. */
|
||||
};
|
||||
|
||||
PublicRoomListModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = NameRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] Quotient::Connection *connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
void setConnection(Quotient::Connection *conn);
|
||||
|
||||
[[nodiscard]] QString server() const
|
||||
{
|
||||
return m_server;
|
||||
}
|
||||
[[nodiscard]] QString server() const;
|
||||
void setServer(const QString &value);
|
||||
|
||||
[[nodiscard]] QString keyword() const
|
||||
{
|
||||
return m_keyword;
|
||||
}
|
||||
[[nodiscard]] QString keyword() const;
|
||||
void setKeyword(const QString &value);
|
||||
|
||||
[[nodiscard]] bool hasMore() const;
|
||||
|
||||
/**
|
||||
* @brief Load the next set of rooms.
|
||||
*
|
||||
* @param count the maximum number of rooms to load.
|
||||
*/
|
||||
Q_INVOKABLE void next(int count = 50);
|
||||
|
||||
private:
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
|
||||
#include "roomlistmodel.h"
|
||||
|
||||
#include "controller.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "user.h"
|
||||
|
||||
#include <QDebug>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusMessage>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QGuiApplication>
|
||||
@@ -28,15 +31,6 @@ using namespace Quotient;
|
||||
|
||||
Q_DECLARE_METATYPE(Quotient::JoinState)
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
bool useUnityCounter()
|
||||
{
|
||||
static const auto Result = QDBusInterface("com.canonical.Unity", "/").isValid();
|
||||
|
||||
return Result;
|
||||
}
|
||||
#endif
|
||||
|
||||
RoomListModel::RoomListModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
@@ -45,34 +39,41 @@ RoomListModel::RoomListModel(QObject *parent)
|
||||
m_categoryVisibility[collapsedSection] = false;
|
||||
}
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
|
||||
if (useUnityCounter()) {
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop";
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(m_notificationCount, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
#ifndef Q_OS_ANDROID
|
||||
// copied from Telegram desktop
|
||||
const auto launcherUrl = "application://org.kde.neochat.desktop";
|
||||
// Gnome requires that count is a 64bit integer
|
||||
const qint64 counterSlice = std::min(m_notificationCount, 9999);
|
||||
QVariantMap dbusUnityProperties;
|
||||
|
||||
if (counterSlice > 0) {
|
||||
dbusUnityProperties["count"] = counterSlice;
|
||||
dbusUnityProperties["count-visible"] = true;
|
||||
} else {
|
||||
dbusUnityProperties["count-visible"] = false;
|
||||
}
|
||||
|
||||
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat", "com.canonical.Unity.LauncherEntry", "Update");
|
||||
|
||||
signal.setArguments({launcherUrl, dbusUnityProperties});
|
||||
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
if (counterSlice > 0) {
|
||||
dbusUnityProperties["count"] = counterSlice;
|
||||
dbusUnityProperties["count-visible"] = true;
|
||||
} else {
|
||||
dbusUnityProperties["count-visible"] = false;
|
||||
}
|
||||
|
||||
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat", "com.canonical.Unity.LauncherEntry", "Update");
|
||||
|
||||
signal.setArguments({launcherUrl, dbusUnityProperties});
|
||||
|
||||
QDBusConnection::sessionBus().send(signal);
|
||||
#endif // Q_OS_ANDROID
|
||||
#else
|
||||
qGuiApp->setBadgeNumber(m_notificationCount);
|
||||
#endif // QT_VERSION_CHECK(6, 6, 0)
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
RoomListModel::~RoomListModel() = default;
|
||||
|
||||
Quotient::Connection *RoomListModel::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
void RoomListModel::setConnection(Connection *connection)
|
||||
{
|
||||
if (connection == m_connection) {
|
||||
@@ -143,6 +144,7 @@ void RoomListModel::doAddRoom(Room *r)
|
||||
m_rooms.append(room);
|
||||
connectRoomSignals(room);
|
||||
Q_EMIT roomAdded(room);
|
||||
Q_EMIT Controller::instance().roomAdded(room);
|
||||
} else {
|
||||
qCritical() << "Attempt to add nullptr to the room list";
|
||||
Q_ASSERT(false);
|
||||
@@ -152,10 +154,10 @@ void RoomListModel::doAddRoom(Room *r)
|
||||
void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
{
|
||||
connect(room, &Room::displaynameChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
refresh(room, {DisplayNameRole, NameRole});
|
||||
});
|
||||
connect(room, &Room::unreadMessagesChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
refresh(room, {UnreadCountRole, NotificationCountRole, HighlightCountRole});
|
||||
});
|
||||
connect(room, &Room::notificationCountChanged, this, [this, room] {
|
||||
refresh(room);
|
||||
@@ -170,7 +172,7 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
refresh(room);
|
||||
});
|
||||
connect(room, &Room::addedMessages, this, [this, room] {
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
refresh(room, {LastEventRole, SubtitleTextRole, LastActiveTimeRole});
|
||||
});
|
||||
connect(room, &Room::pendingEventMerged, this, [this, room] {
|
||||
refresh(room, {LastEventRole, SubtitleTextRole});
|
||||
@@ -178,29 +180,12 @@ void RoomListModel::connectRoomSignals(NeoChatRoom *room)
|
||||
#ifndef QUOTIENT_07
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::handleNotifications);
|
||||
#endif
|
||||
connect(room, &Room::highlightCountChanged, this, [this, room] {
|
||||
if (room->highlightCount() == 0) {
|
||||
return;
|
||||
}
|
||||
if (room->timelineSize() == 0) {
|
||||
return;
|
||||
}
|
||||
auto *lastEvent = room->lastEvent();
|
||||
|
||||
if (!lastEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastEvent->isStateEvent()) {
|
||||
return;
|
||||
}
|
||||
User *sender = room->user(lastEvent->senderId());
|
||||
if (sender == room->localUser()) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT newHighlight(room->id(), lastEvent->id(), room->displayName(), sender->displayname(), room->eventToString(*lastEvent), room->avatar(128));
|
||||
});
|
||||
#ifndef QUOTIENT_07
|
||||
connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
#else
|
||||
connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
@@ -229,10 +214,13 @@ void RoomListModel::handleNotifications()
|
||||
continue;
|
||||
}
|
||||
oldNotifications += notification["event"].toObject()["event_id"].toString();
|
||||
|
||||
auto room = m_connection->room(notification["room_id"].toString());
|
||||
auto currentRoom = RoomManager::instance().currentRoom();
|
||||
bool roomIsActive = currentRoom && room->id() == currentRoom->id();
|
||||
|
||||
// If room exists, room is NOT active OR the application is NOT active, show notification
|
||||
if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
if (room && !(roomIsActive && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
|
||||
// The room might have been deleted (for example rejected invitation).
|
||||
auto sender = room->user(notification["event"].toObject()["sender"].toString());
|
||||
|
||||
@@ -254,6 +242,11 @@ void RoomListModel::handleNotifications()
|
||||
}
|
||||
#endif
|
||||
|
||||
int RoomListModel::notificationCount() const
|
||||
{
|
||||
return m_notificationCount;
|
||||
}
|
||||
|
||||
void RoomListModel::refreshNotificationCount()
|
||||
{
|
||||
int count = 0;
|
||||
@@ -408,7 +401,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
||||
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
|
||||
}
|
||||
if (role == SubtitleTextRole) {
|
||||
return room->subtitleText();
|
||||
return room->lastEventToString(Qt::PlainText, true);
|
||||
}
|
||||
if (role == AvatarImageRole) {
|
||||
return room->avatar(128);
|
||||
@@ -457,40 +450,40 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryName(int section)
|
||||
QString RoomListModel::categoryName(int category)
|
||||
{
|
||||
switch (section) {
|
||||
case 1:
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return i18n("Invited");
|
||||
case 2:
|
||||
case NeoChatRoomType::Favorite:
|
||||
return i18n("Favorite");
|
||||
case 3:
|
||||
case NeoChatRoomType::Direct:
|
||||
return i18n("Direct Messages");
|
||||
case 4:
|
||||
case NeoChatRoomType::Normal:
|
||||
return i18n("Normal");
|
||||
case 5:
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return i18n("Low priority");
|
||||
case 6:
|
||||
case NeoChatRoomType::Space:
|
||||
return i18n("Spaces");
|
||||
default:
|
||||
return "Deadbeef";
|
||||
}
|
||||
}
|
||||
|
||||
QString RoomListModel::categoryIconName(int section)
|
||||
QString RoomListModel::categoryIconName(int category)
|
||||
{
|
||||
switch (section) {
|
||||
case 1:
|
||||
switch (category) {
|
||||
case NeoChatRoomType::Invited:
|
||||
return QStringLiteral("user-invisible");
|
||||
case 2:
|
||||
case NeoChatRoomType::Favorite:
|
||||
return QStringLiteral("favorite");
|
||||
case 3:
|
||||
case NeoChatRoomType::Direct:
|
||||
return QStringLiteral("dialog-messages");
|
||||
case 4:
|
||||
case NeoChatRoomType::Normal:
|
||||
return QStringLiteral("group");
|
||||
case 5:
|
||||
case NeoChatRoomType::Deprioritized:
|
||||
return QStringLiteral("object-order-lower");
|
||||
case 6:
|
||||
case NeoChatRoomType::Space:
|
||||
return QStringLiteral("group");
|
||||
default:
|
||||
return QStringLiteral("tools-report-bug");
|
||||
@@ -528,7 +521,7 @@ NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int RoomListModel::indexForRoom(NeoChatRoom *room) const
|
||||
int RoomListModel::rowForRoom(NeoChatRoom *room) const
|
||||
{
|
||||
return m_rooms.indexOf(room);
|
||||
}
|
||||
|
||||
@@ -20,77 +20,137 @@ class NeoChatRoomType : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the room list categories a room can be assigned.
|
||||
*/
|
||||
enum Types {
|
||||
Invited = 1,
|
||||
Favorite,
|
||||
Direct,
|
||||
Normal,
|
||||
Deprioritized,
|
||||
Space,
|
||||
Invited = 1, /**< The user has been invited to the room. */
|
||||
Favorite, /**< The room is set as a favourite. */
|
||||
Direct, /**< The room is a direct chat. */
|
||||
Normal, /**< The default category for a joined room. */
|
||||
Deprioritized, /**< The room is set as low priority. */
|
||||
Space, /**< The room is a space. */
|
||||
};
|
||||
Q_ENUM(Types)
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RoomListModel
|
||||
*
|
||||
* This class defines the model for visualising the user's list of joined rooms.
|
||||
*/
|
||||
class RoomListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is getting its rooms from.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The total number of notifications for all the rooms.
|
||||
*/
|
||||
Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
DisplayNameRole,
|
||||
AvatarRole,
|
||||
CanonicalAliasRole,
|
||||
TopicRole,
|
||||
CategoryRole,
|
||||
UnreadCountRole,
|
||||
NotificationCountRole,
|
||||
HighlightCountRole,
|
||||
LastEventRole,
|
||||
LastActiveTimeRole,
|
||||
JoinStateRole,
|
||||
CurrentRoomRole,
|
||||
CategoryVisibleRole,
|
||||
SubtitleTextRole,
|
||||
AvatarImageRole,
|
||||
IdRole,
|
||||
IsSpaceRole,
|
||||
NameRole = Qt::UserRole + 1, /**< The name of the room. */
|
||||
DisplayNameRole, /**< The display name of the room. */
|
||||
AvatarRole, /**< The source URL for the room's avatar. */
|
||||
CanonicalAliasRole, /**< The room canonical alias. */
|
||||
TopicRole, /**< The room topic. */
|
||||
CategoryRole, /**< The room category, e.g favourite. */
|
||||
UnreadCountRole, /**< The number of unread messages in the room. */
|
||||
NotificationCountRole, /**< The number of notifications in the room. */
|
||||
HighlightCountRole, /**< The number of highlighted messages in the room. */
|
||||
LastEventRole, /**< Text for the last event in the room. */
|
||||
LastActiveTimeRole, /**< The timestamp of the last event sent in the room. */
|
||||
JoinStateRole, /**< The local user's join state in the room. */
|
||||
CurrentRoomRole, /**< The room object for the room. */
|
||||
CategoryVisibleRole, /**< If the room's category is visible. */
|
||||
SubtitleTextRole, /**< The text to show as the room subtitle. */
|
||||
AvatarImageRole, /**< The room avatar as an image. */
|
||||
IdRole, /**< The room matrix ID. */
|
||||
IsSpaceRole, /**< Whether the room is a space. */
|
||||
};
|
||||
Q_ENUM(EventRoles)
|
||||
|
||||
RoomListModel(QObject *parent = nullptr);
|
||||
~RoomListModel() override;
|
||||
|
||||
[[nodiscard]] Quotient::Connection *connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
[[nodiscard]] Quotient::Connection *connection() const;
|
||||
void setConnection(Quotient::Connection *connection);
|
||||
void doResetModel();
|
||||
|
||||
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
|
||||
[[nodiscard]] int notificationCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryName(int section);
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int section);
|
||||
/**
|
||||
* @brief Return the room at the given row.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] NeoChatRoom *roomAt(int row) const;
|
||||
|
||||
/**
|
||||
* @brief Return a string to represent the given room category.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryName(int category);
|
||||
|
||||
/**
|
||||
* @brief Return a string with the name of the given room category icon.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] static QString categoryIconName(int category);
|
||||
|
||||
/**
|
||||
* @brief Set whether a given category should be visible or not.
|
||||
*
|
||||
* @param category the NeoChatRoomType::Types value for the category (it's an
|
||||
* int due to the pain of Q_INVOKABLES and cpp enums).
|
||||
* @param visible true if the category should be visible, false if not.
|
||||
*/
|
||||
Q_INVOKABLE void setCategoryVisible(int category, bool visible);
|
||||
|
||||
/**
|
||||
* @brief Return whether a room category is set to be visible.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] bool categoryVisible(int category) const;
|
||||
Q_INVOKABLE [[nodiscard]] int indexForRoom(NeoChatRoom *room) const;
|
||||
|
||||
[[nodiscard]] int notificationCount() const
|
||||
{
|
||||
return m_notificationCount;
|
||||
}
|
||||
/**
|
||||
* @brief Return the model row for the given room.
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int rowForRoom(NeoChatRoom *room) const;
|
||||
|
||||
/**
|
||||
* @brief Return a room for the given room alias or room matrix ID.
|
||||
*
|
||||
* The room must be in the model.
|
||||
*/
|
||||
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
|
||||
|
||||
private Q_SLOTS:
|
||||
void doResetModel();
|
||||
void doAddRoom(Quotient::Room *room);
|
||||
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
|
||||
void deleteRoom(Quotient::Room *room);
|
||||
@@ -116,5 +176,4 @@ Q_SIGNALS:
|
||||
void notificationCountChanged();
|
||||
|
||||
void roomAdded(NeoChatRoom *_t1);
|
||||
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "searchmodel.h"
|
||||
@@ -43,20 +43,27 @@ void SearchModel::search()
|
||||
}
|
||||
|
||||
SearchJob::RoomEventsCriteria criteria{
|
||||
m_searchText,
|
||||
{},
|
||||
RoomEventFilter{
|
||||
.rooms = {m_room->id()},
|
||||
},
|
||||
"recent",
|
||||
SearchJob::IncludeEventContext{3, 3, true},
|
||||
false,
|
||||
none,
|
||||
.searchTerm = m_searchText,
|
||||
.keys = {},
|
||||
.filter =
|
||||
RoomEventFilter{
|
||||
.unreadThreadNotifications = none,
|
||||
.lazyLoadMembers = true,
|
||||
.includeRedundantMembers = false,
|
||||
.notRooms = {},
|
||||
.rooms = {m_room->id()},
|
||||
.containsUrl = false,
|
||||
},
|
||||
.orderBy = "recent",
|
||||
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
|
||||
.includeState = false,
|
||||
.groupings = none,
|
||||
|
||||
};
|
||||
|
||||
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
|
||||
m_job = job;
|
||||
connect(job, &BaseJob::finished, this, [=] {
|
||||
connect(job, &BaseJob::finished, this, [this, job] {
|
||||
beginResetModel();
|
||||
m_result = job->searchCategories().roomEvents;
|
||||
endResetModel();
|
||||
@@ -116,6 +123,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
int SearchModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
#ifdef QUOTIENT_07
|
||||
if (m_result.has_value()) {
|
||||
return m_result->results.size();
|
||||
@@ -127,7 +135,7 @@ int SearchModel::rowCount(const QModelIndex &parent) const
|
||||
QHash<int, QByteArray> SearchModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{EventTypeRole, "eventType"},
|
||||
{DelegateTypeRole, "delegateType"},
|
||||
{DisplayRole, "display"},
|
||||
{AuthorRole, "author"},
|
||||
{ShowSectionRole, "showSection"},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -17,24 +17,50 @@ class Connection;
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
* @class SearchModel
|
||||
*
|
||||
* This class defines the model for visualising the results of a room message search.
|
||||
*/
|
||||
class SearchModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief The text to search messages for.
|
||||
*/
|
||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
||||
|
||||
/**
|
||||
* @brief The current connection that the model is using to search for messages.
|
||||
*/
|
||||
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
/**
|
||||
* @brief The current room that the search is being done from.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the model is currently searching for messages.
|
||||
*/
|
||||
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*
|
||||
* Some of the roles exist only for compatibility with the MessageEventModel,
|
||||
* since the same delegates are used.
|
||||
*/
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole,
|
||||
EventTypeRole,
|
||||
ShowAuthorRole,
|
||||
AuthorRole,
|
||||
ShowSectionRole,
|
||||
SectionRole,
|
||||
TimeRole,
|
||||
DisplayRole = Qt::DisplayRole, /**< The message string. */
|
||||
DelegateTypeRole, /**< The type of the event. */
|
||||
ShowAuthorRole, /**< Whether the author should be shown (always true). */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
SectionRole, /**< The date of the event as a string. */
|
||||
TimeRole, /**< The timestamp for when the event was sent. */
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
SearchModel(QObject *parent = nullptr);
|
||||
@@ -48,13 +74,33 @@ public:
|
||||
NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
Q_INVOKABLE void search();
|
||||
bool searching() const;
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
bool searching() const;
|
||||
/**
|
||||
* @brief Start searching for messages.
|
||||
*/
|
||||
Q_INVOKABLE void search();
|
||||
|
||||
Q_SIGNALS:
|
||||
void searchTextChanged();
|
||||
|
||||
@@ -9,33 +9,89 @@
|
||||
#include <QPointer>
|
||||
#include <QUrl>
|
||||
|
||||
/**
|
||||
* @class ServerListModel
|
||||
*
|
||||
* This class defines the model for visualising a list of matrix servers.
|
||||
*
|
||||
* The list of servers is retrieved from the local cache. Any additions are also
|
||||
* stored locally so that they are retrieved on subsequent instantiations.
|
||||
*
|
||||
* The model also automatically adds the local user's home server and matrix.org to
|
||||
* the model. Finally the model also adds an entry to create a space in the model
|
||||
* for an "add new server" delegate.
|
||||
*/
|
||||
class ServerListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Define the data required to represent a server.
|
||||
*/
|
||||
struct Server {
|
||||
QString url;
|
||||
bool isHomeServer;
|
||||
bool isAddServerDelegate;
|
||||
bool isDeletable;
|
||||
QString url; /**< Server URL. */
|
||||
bool isHomeServer; /**< Whether the server is the local user's home server. */
|
||||
bool isAddServerDelegate; /**< Wether the item is the "add new server" delegate. */
|
||||
bool isDeletable; /**< Whether the item can be deleted from the model. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum EventRoles {
|
||||
UrlRole = Qt::UserRole + 1,
|
||||
IsHomeServerRole,
|
||||
IsAddServerDelegateRole,
|
||||
IsDeletableRole,
|
||||
UrlRole = Qt::UserRole + 1, /**< Server URL. */
|
||||
IsHomeServerRole, /**< Whether the server is the local user's home server. */
|
||||
IsAddServerDelegateRole, /**< Whether the item is the add new server delegate. */
|
||||
IsDeletableRole, /**< Whether the item can be deleted from the model. */
|
||||
};
|
||||
|
||||
ServerListModel(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa EventRoles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Start a check to see if the given URL is a valid matrix server.
|
||||
*
|
||||
* This function starts the check but due to the requests being asynchronous
|
||||
* the caller will need to watch the serverCheckComplete signal for confirmation.
|
||||
* The server URL should be treated as invalid until the signal is emitted true.
|
||||
*
|
||||
* @sa serverCheckComplete()
|
||||
*/
|
||||
Q_INVOKABLE void checkServer(const QString &url);
|
||||
|
||||
/**
|
||||
* @brief Add a new server to the model.
|
||||
*
|
||||
* The server will also be stored in local cache.
|
||||
*/
|
||||
Q_INVOKABLE void addServer(const QString &url);
|
||||
|
||||
/**
|
||||
* @brief Remove the server at the given index.
|
||||
*
|
||||
* The server will also be removed from local cache.
|
||||
*/
|
||||
Q_INVOKABLE void removeServerAtIndex(int index);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "sortfilterroomlistmodel.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "statemodel.h"
|
||||
@@ -49,7 +49,7 @@ void StateModel::setRoom(NeoChatRoom *room)
|
||||
Q_EMIT roomChanged();
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
connect(room, &NeoChatRoom::changed, this, [=] {
|
||||
connect(room, &NeoChatRoom::changed, this, [this] {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -217,6 +217,9 @@ int UserListModel::findUserPos(Quotient::User *user) const
|
||||
|
||||
int UserListModel::findUserPos(const QString &username) const
|
||||
{
|
||||
if (!m_currentRoom) {
|
||||
return 0;
|
||||
}
|
||||
return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "neochataccountregistry.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
<label>Merge Room Lists</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowLeaveJoinEvent" type="bool">
|
||||
<label>Show leave and join events in the timeline</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="AllowQuickEdit" type="bool">
|
||||
<label>Use s/text/replacement syntax to edit your last message.</label>
|
||||
<default>false</default>
|
||||
@@ -41,9 +37,6 @@
|
||||
<label>"Show your messages on the right</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="RoomListPageWidth" type="int">
|
||||
<default>-1</default>
|
||||
</entry>
|
||||
<entry name="RoomDrawerWidth" type="int">
|
||||
<default>-1</default>
|
||||
</entry>
|
||||
@@ -75,6 +68,14 @@
|
||||
<label>Use a compact room list layout</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
<entry name="ShowStateEvent" type="bool">
|
||||
<label>Show state events in the timeline</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="ShowLeaveJoinEvent" type="bool">
|
||||
<label>Show leave and join events in the timeline</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="ShowRename" type="bool">
|
||||
<label>Show rename events in the timeline</label>
|
||||
<default>true</default>
|
||||
@@ -108,6 +109,10 @@
|
||||
<label>Show avatar in the room drawer</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="Collapsed" type="bool">
|
||||
<label>Save the collapsed state of the room list</label>
|
||||
<default>false</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="NetworkProxy">
|
||||
<entry name="ProxyType" type="Enum">
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
#include <QMimeDatabase>
|
||||
#include <QTemporaryFile>
|
||||
#include <QTextDocument>
|
||||
#include <functional>
|
||||
|
||||
#include <QMediaMetaData>
|
||||
#include <QMediaPlayer>
|
||||
|
||||
#include <jobs/basejob.h>
|
||||
#include <qcoro/qcorosignal.h>
|
||||
|
||||
#include <connection.h>
|
||||
#include <csapi/account-data.h>
|
||||
#include <csapi/directory.h>
|
||||
#include <csapi/pushrules.h>
|
||||
#include <csapi/redaction.h>
|
||||
@@ -47,7 +48,7 @@
|
||||
#endif
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "stickerevent.h"
|
||||
#include "utils.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KIO/Job>
|
||||
@@ -98,6 +99,42 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
Q_EMIT canEncryptRoomChanged();
|
||||
});
|
||||
connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
|
||||
connect(this, &Room::changed, this, [this]() {
|
||||
Q_EMIT defaultUrlPreviewStateChanged();
|
||||
});
|
||||
connect(this, &Room::accountDataChanged, this, [this](QString type) {
|
||||
if (type == "org.matrix.room.preview_urls") {
|
||||
Q_EMIT urlPreviewEnabledChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool NeoChatRoom::hasFileUploading() const
|
||||
{
|
||||
return m_hasFileUploading;
|
||||
}
|
||||
|
||||
void NeoChatRoom::setHasFileUploading(bool value)
|
||||
{
|
||||
if (value == m_hasFileUploading) {
|
||||
return;
|
||||
}
|
||||
m_hasFileUploading = value;
|
||||
Q_EMIT hasFileUploadingChanged();
|
||||
}
|
||||
|
||||
int NeoChatRoom::fileUploadingProgress() const
|
||||
{
|
||||
return m_fileUploadingProgress;
|
||||
}
|
||||
|
||||
void NeoChatRoom::setFileUploadingProgress(int value)
|
||||
{
|
||||
if (m_fileUploadingProgress == value) {
|
||||
return;
|
||||
}
|
||||
m_fileUploadingProgress = value;
|
||||
Q_EMIT fileUploadingProgressChanged();
|
||||
}
|
||||
|
||||
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
|
||||
@@ -205,7 +242,7 @@ void NeoChatRoom::sendTypingNotification(bool isTyping)
|
||||
connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
|
||||
}
|
||||
|
||||
const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
const RoomEvent *NeoChatRoom::lastEvent() const
|
||||
{
|
||||
for (auto timelineItem = messageEvents().rbegin(); timelineItem < messageEvents().rend(); timelineItem++) {
|
||||
const RoomEvent *event = timelineItem->get();
|
||||
@@ -217,8 +254,20 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event->isStateEvent()
|
||||
&& (ignoreStateEvent || !NeoChatConfig::self()->showLeaveJoinEvent() || static_cast<const StateEventBase &>(*event).repeatsState())) {
|
||||
if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
|
||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
|
||||
continue;
|
||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
|
||||
continue;
|
||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (event->isStateEvent() && static_cast<const StateEventBase &>(*event).repeatsState()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -232,6 +281,14 @@ const RoomEvent *NeoChatRoom::lastEvent(bool ignoreStateEvent) const
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
if (auto lastEvent = eventCast<const StateEvent>(event)) {
|
||||
#else
|
||||
if (auto lastEvent = eventCast<const StateEventBase>(event)) {
|
||||
#endif
|
||||
return lastEvent;
|
||||
}
|
||||
|
||||
if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
|
||||
return lastEvent;
|
||||
}
|
||||
@@ -257,12 +314,13 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
||||
return false;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::lastEventToString() const
|
||||
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
if (auto event = lastEvent()) {
|
||||
return roomMembername(event->senderId()) + (event->isStateEvent() ? " " : ": ") + eventToString(*event);
|
||||
return roomMembername(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
|
||||
+ eventToString(*event, format, stripNewlines);
|
||||
}
|
||||
return QLatin1String("");
|
||||
return {};
|
||||
}
|
||||
|
||||
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
|
||||
@@ -321,7 +379,7 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
if (auto event = lastEvent(true)) {
|
||||
if (auto event = lastEvent()) {
|
||||
return event->originTimestamp();
|
||||
}
|
||||
|
||||
@@ -329,69 +387,6 @@ QDateTime NeoChatRoom::lastActiveTime()
|
||||
return messageEvents().rbegin()->get()->originTimestamp();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::subtitleText()
|
||||
{
|
||||
static const QRegularExpression blockquote("(\r\n\t|\n|\r\t|)> ");
|
||||
static const QRegularExpression heading("(\r\n\t|\n|\r\t|)\\#{1,6} ");
|
||||
static const QRegularExpression newlines("(\r\n\t|\n|\r\t|\r\n)");
|
||||
static const QRegularExpression bold1("(\\*\\*|__)(?=\\S)([^\\r]*\\S)\\1");
|
||||
static const QRegularExpression bold2("(\\*|_)(?=\\S)([^\\r]*\\S)\\1");
|
||||
static const QRegularExpression strike1("~~(.*)~~");
|
||||
static const QRegularExpression strike2("~(.*)~");
|
||||
static const QRegularExpression del("<del>(.*)</del>");
|
||||
static const QRegularExpression multileLineCode("```([^```]+)```");
|
||||
static const QRegularExpression singleLinecode("`([^`]+)`");
|
||||
QString subtitle = lastEventToString().size() == 0 ? topic() : lastEventToString();
|
||||
|
||||
subtitle
|
||||
// replace blockquote, i.e. '> text'
|
||||
.replace(blockquote, " ")
|
||||
// replace headings, i.e. "# text"
|
||||
.replace(heading, " ")
|
||||
// replace newlines
|
||||
.replace(newlines, " ")
|
||||
// replace '**text**' and '__text__'
|
||||
.replace(bold1, "\\2")
|
||||
// replace '*text*' and '_text_'
|
||||
.replace(bold2, "\\2")
|
||||
// replace '~~text~~'
|
||||
.replace(strike1, "\\1")
|
||||
// replace '~text~'
|
||||
.replace(strike2, "\\1")
|
||||
// replace '<del>text</del>'
|
||||
.replace(del, "\\1")
|
||||
// replace '```code```'
|
||||
.replace(multileLineCode, "\\1")
|
||||
// replace '`code`'
|
||||
.replace(singleLinecode, "\\1");
|
||||
|
||||
return subtitle.size() > 0 ? subtitle : QStringLiteral(" ");
|
||||
}
|
||||
|
||||
int NeoChatRoom::savedTopVisibleIndex() const
|
||||
{
|
||||
return firstDisplayedMarker() == historyEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin());
|
||||
}
|
||||
|
||||
int NeoChatRoom::savedBottomVisibleIndex() const
|
||||
{
|
||||
return lastDisplayedMarker() == historyEdge() ? 0 : int(lastDisplayedMarker() - messageEvents().rbegin());
|
||||
}
|
||||
|
||||
void NeoChatRoom::saveViewport(int topIndex, int bottomIndex)
|
||||
{
|
||||
if (topIndex == -1 || bottomIndex == -1 || (bottomIndex == savedBottomVisibleIndex() && (bottomIndex == 0 || topIndex == savedTopVisibleIndex()))) {
|
||||
return;
|
||||
}
|
||||
if (bottomIndex == 0) {
|
||||
setFirstDisplayedEventId({});
|
||||
setLastDisplayedEventId({});
|
||||
return;
|
||||
}
|
||||
setFirstDisplayedEvent(maxTimelineIndex() - topIndex);
|
||||
setLastDisplayedEvent(maxTimelineIndex() - bottomIndex);
|
||||
}
|
||||
|
||||
QVariantList NeoChatRoom::getUsers(const QString &keyword, int limit) const
|
||||
{
|
||||
const auto userList = users();
|
||||
@@ -425,15 +420,6 @@ QVariantMap NeoChatRoom::getUser(const QString &userID) const
|
||||
{QStringLiteral("color"), user.color()}};
|
||||
}
|
||||
|
||||
QUrl NeoChatRoom::urlToMxcUrl(const QUrl &mxcUrl)
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
return connection()->makeMediaUrl(mxcUrl);
|
||||
#else
|
||||
return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl);
|
||||
#endif
|
||||
}
|
||||
|
||||
QString NeoChatRoom::avatarMediaId() const
|
||||
{
|
||||
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
|
||||
@@ -451,7 +437,7 @@ QString NeoChatRoom::avatarMediaId() const
|
||||
return {};
|
||||
}
|
||||
|
||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool removeReply) const
|
||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const
|
||||
{
|
||||
const bool prettyPrint = (format == Qt::RichText);
|
||||
|
||||
@@ -462,58 +448,48 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
return visit(
|
||||
#endif
|
||||
evt,
|
||||
[this, prettyPrint, removeReply](const RoomMessageEvent &e) {
|
||||
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
||||
using namespace MessageEventContent;
|
||||
|
||||
// 1. prettyPrint/HTML
|
||||
if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") {
|
||||
auto htmlBody = static_cast<const TextContent *>(e.content())->body;
|
||||
if (removeReply) {
|
||||
htmlBody.remove(utils::removeRichReplyRegex);
|
||||
}
|
||||
htmlBody.replace(utils::userPillRegExp, R"(<b class="user-pill">\1</b>)");
|
||||
htmlBody.replace(utils::strikethroughRegExp, "<s>\\1</s>");
|
||||
|
||||
auto url = connection()->homeserver();
|
||||
auto base = url.scheme() + QStringLiteral("://") + url.host() + (url.port() != -1 ? ':' + QString::number(url.port()) : QString());
|
||||
htmlBody.replace(utils::mxcImageRegExp, QStringLiteral(R"(<img \1 src="%1/_matrix/media/r0/download/\2/\3" \4 > )").arg(base));
|
||||
|
||||
return htmlBody;
|
||||
}
|
||||
TextHandler textHandler;
|
||||
|
||||
if (e.hasFileContent()) {
|
||||
auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped();
|
||||
auto fileCaption = e.content()->fileInfo()->originalName;
|
||||
if (fileCaption.isEmpty()) {
|
||||
fileCaption = prettyPrint ? Quotient::prettyPrint(e.plainBody()) : e.plainBody();
|
||||
fileCaption = e.plainBody();
|
||||
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
||||
fileCaption = e.plainBody() + " | " + fileCaption;
|
||||
}
|
||||
return !fileCaption.isEmpty() ? fileCaption : i18n("a file");
|
||||
textHandler.setData(fileCaption);
|
||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
||||
}
|
||||
|
||||
// 2. prettyPrint/text 3. plainText/HTML 4. plainText/text
|
||||
QString plainBody;
|
||||
if (e.hasTextContent() && e.content() && e.mimeType().name() == "text/plain") { // 2/4
|
||||
plainBody = static_cast<const TextContent *>(e.content())->body;
|
||||
} else { // 3
|
||||
plainBody = e.plainBody();
|
||||
QString body;
|
||||
if (e.hasTextContent() && e.content()) {
|
||||
body = static_cast<const TextContent *>(e.content())->body;
|
||||
} else {
|
||||
body = e.plainBody();
|
||||
}
|
||||
|
||||
if (prettyPrint) {
|
||||
if (removeReply) {
|
||||
plainBody.remove(utils::removeReplyRegex);
|
||||
}
|
||||
return Quotient::prettyPrint(plainBody);
|
||||
textHandler.setData(body);
|
||||
|
||||
Qt::TextFormat inputFormat;
|
||||
if (e.mimeType().name() == "text/plain") {
|
||||
inputFormat = Qt::PlainText;
|
||||
} else {
|
||||
inputFormat = Qt::RichText;
|
||||
}
|
||||
if (removeReply) {
|
||||
return plainBody.remove(utils::removeReplyRegex);
|
||||
|
||||
if (format == Qt::RichText) {
|
||||
return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines);
|
||||
} else {
|
||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||
}
|
||||
return plainBody;
|
||||
},
|
||||
[](const StickerEvent &e) {
|
||||
return e.body();
|
||||
},
|
||||
[this](const RoomMemberEvent &e) {
|
||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||
// FIXME: Rewind to the name that was at the time of this event
|
||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
||||
if (e.membership() == MembershipType::Leave) {
|
||||
@@ -526,8 +502,11 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
#endif
|
||||
}
|
||||
}
|
||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
|
||||
|
||||
if (prettyPrint) {
|
||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||
.arg(e.userId(), static_cast<NeoChatUser *>(user(e.userId()))->color().name(), subjectName);
|
||||
}
|
||||
|
||||
// The below code assumes senderName output in AuthorRole
|
||||
switch (e.membership()) {
|
||||
@@ -618,8 +597,12 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
||||
[](const RoomNameEvent &e) {
|
||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
||||
},
|
||||
[prettyPrint](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic to: %1", prettyPrint ? Quotient::prettyPrint(e.topic()) : e.topic());
|
||||
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||
: i18n("set the topic to: %1",
|
||||
prettyPrint ? Quotient::prettyPrint(e.topic())
|
||||
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
||||
: e.topic());
|
||||
},
|
||||
[](const RoomAvatarEvent &) {
|
||||
return i18n("changed the room avatar");
|
||||
@@ -781,6 +764,7 @@ QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||
},
|
||||
#ifdef QUOTIENT_07
|
||||
[](const PollStartEvent &e) {
|
||||
Q_UNUSED(e);
|
||||
return i18n("started a poll");
|
||||
},
|
||||
#endif
|
||||
@@ -805,30 +789,6 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::addLocalAlias(const QString &alias)
|
||||
{
|
||||
auto a = aliases();
|
||||
if (a.contains(alias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
a += alias;
|
||||
|
||||
setLocalAliases(a);
|
||||
}
|
||||
|
||||
void NeoChatRoom::removeLocalAlias(const QString &alias)
|
||||
{
|
||||
auto a = aliases();
|
||||
if (!a.contains(alias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
a.removeAll(alias);
|
||||
|
||||
setLocalAliases(a);
|
||||
}
|
||||
|
||||
QString msgTypeToString(MessageEventType msgType)
|
||||
{
|
||||
switch (msgType) {
|
||||
@@ -985,7 +945,7 @@ bool NeoChatRoom::canSendState(const QString &eventType) const
|
||||
auto currentPl = plEvent->powerLevelForUser(localUser()->id());
|
||||
|
||||
#ifndef QUOTIENT_07
|
||||
if (eventType == "m.room.history_visibility") {
|
||||
if (eventType == "m.room.history_visibility" || eventType == "org.matrix.room.preview_urls") {
|
||||
return false;
|
||||
} else {
|
||||
return currentPl >= pl;
|
||||
@@ -1015,11 +975,6 @@ bool NeoChatRoom::isUserBanned(const QString &user) const
|
||||
return getCurrentState<RoomMemberEvent>(user)->membership() == MembershipType::Ban;
|
||||
}
|
||||
|
||||
QString NeoChatRoom::htmlSafeName() const
|
||||
{
|
||||
return name().toHtmlEscaped();
|
||||
}
|
||||
|
||||
QString NeoChatRoom::htmlSafeDisplayName() const
|
||||
{
|
||||
return displayName().toHtmlEscaped();
|
||||
@@ -1075,6 +1030,91 @@ void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
|
||||
// Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
|
||||
}
|
||||
|
||||
bool NeoChatRoom::defaultUrlPreviewState() const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls");
|
||||
#else
|
||||
auto urlPreviewsDisabled = getCurrentState("org.matrix.room.preview_urls");
|
||||
#endif
|
||||
|
||||
// Some rooms will not have this state event set so check for a nullptr return.
|
||||
if (urlPreviewsDisabled != nullptr) {
|
||||
return !urlPreviewsDisabled->contentJson()["disable"].toBool();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
|
||||
{
|
||||
if (!canSendState("org.matrix.room.preview_urls")) {
|
||||
qWarning() << "Power level too low to set the default URL preview state for the room";
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note the org.matrix.room.preview_urls room state event is completely undocumented
|
||||
* so here it is because I'm nice.
|
||||
*
|
||||
* Also note this is a different event to org.matrix.room.preview_urls for room
|
||||
* account data, because even though it has the same name and content it's totally different.
|
||||
*
|
||||
* {
|
||||
* "content": {
|
||||
* "disable": false
|
||||
* },
|
||||
* "origin_server_ts": 1673115224071,
|
||||
* "sender": "@bob:kde.org",
|
||||
* "state_key": "",
|
||||
* "type": "org.matrix.room.preview_urls",
|
||||
* "unsigned": {
|
||||
* "replaces_state": "replaced_event_id",
|
||||
* "prev_content": {
|
||||
* "disable": true
|
||||
* },
|
||||
* "prev_sender": "@jeff:kde.org",
|
||||
* "age": 99
|
||||
* },
|
||||
* "event_id": "$event_id",
|
||||
* "room_id": "!room_id:kde.org"
|
||||
* }
|
||||
*
|
||||
* You just have to set disable to true to disable URL previews by default.
|
||||
*/
|
||||
#ifdef QUOTIENT_07
|
||||
setState("org.matrix.room.preview_urls", "", QJsonObject{{"disable", !defaultUrlPreviewState}});
|
||||
#else
|
||||
qWarning() << "Quotient 0.7 required to set room default url preview setting";
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NeoChatRoom::urlPreviewEnabled() const
|
||||
{
|
||||
if (hasAccountData("org.matrix.room.preview_urls")) {
|
||||
return !accountData("org.matrix.room.preview_urls")->contentJson()["disable"].toBool();
|
||||
} else {
|
||||
return defaultUrlPreviewState();
|
||||
}
|
||||
}
|
||||
|
||||
void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
|
||||
{
|
||||
/**
|
||||
* Once again this is undocumented and even though the name and content are the
|
||||
* same this is a different event to the org.matrix.room.preview_urls room state event.
|
||||
*
|
||||
* {
|
||||
* "content": {
|
||||
* "disable": true
|
||||
* }
|
||||
* "type": "org.matrix.room.preview_urls",
|
||||
* }
|
||||
*/
|
||||
connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(), id(), "org.matrix.room.preview_urls", QJsonObject{{"disable", !urlPreviewEnabled}});
|
||||
}
|
||||
|
||||
void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
|
||||
{
|
||||
if (joinedCount() <= 1) {
|
||||
@@ -1114,6 +1154,12 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
|
||||
}
|
||||
}
|
||||
|
||||
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
|
||||
{
|
||||
auto powerLevelEvent = getCurrentState<RoomPowerLevelsEvent>();
|
||||
return powerLevelEvent->powerLevelForUser(userId);
|
||||
}
|
||||
|
||||
int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
|
||||
{
|
||||
#ifdef QUOTIENT_07
|
||||
@@ -1411,6 +1457,11 @@ bool NeoChatRoom::isSpace()
|
||||
#endif
|
||||
}
|
||||
|
||||
PushNotificationState::State NeoChatRoom::pushNotificationState() const
|
||||
{
|
||||
return m_currentPushNotificationState;
|
||||
}
|
||||
|
||||
void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
|
||||
{
|
||||
// The caller should never try to set the state to unknown.
|
||||
@@ -1829,3 +1880,31 @@ int NeoChatRoom::maxRoomVersion() const
|
||||
}
|
||||
return maxVersion;
|
||||
}
|
||||
NeoChatUser *NeoChatRoom::directChatRemoteUser() const
|
||||
{
|
||||
return dynamic_cast<NeoChatUser *>(connection()->directChatUsers(this)[0]);
|
||||
}
|
||||
|
||||
void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
|
||||
{
|
||||
QJsonObject locationContent{
|
||||
{"uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
|
||||
};
|
||||
|
||||
if (!description.isEmpty()) {
|
||||
locationContent["description"] = description;
|
||||
}
|
||||
|
||||
QJsonObject content{
|
||||
{"body", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
|
||||
{"msgtype", "m.location"},
|
||||
{"geo_uri", "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
|
||||
{"org.matrix.msc3488.location", locationContent},
|
||||
{"org.matrix.msc3488.asset",
|
||||
QJsonObject{
|
||||
{"type", "m.pin"},
|
||||
}},
|
||||
{"org.matrix.msc1767.text", i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
|
||||
};
|
||||
postJson("m.room.message", content);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user