Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
2b1b21dd07 Add spell checking hint to ChatBar again 2023-01-05 21:52:52 -05:00
241 changed files with 43940 additions and 70472 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-22.08",
"runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [

2
.gitignore vendored
View File

@@ -9,5 +9,3 @@ compile_commands.json
kate.project.ctags.*
*.user
.flatpak-builder/
.idea/
cmake-build-*

View File

@@ -4,11 +4,9 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
# TODO enable once we can have qt6 libQuotient on the CI
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -1,8 +1,8 @@
# SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
@@ -11,7 +11,6 @@ Dependencies:
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
@@ -20,43 +19,15 @@ Dependencies:
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
- 'on': ['Linux', 'FreeBSD']
'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/Qt5' ]
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]

View File

@@ -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 <tobias.fella@kde.org>
Copyright: 2020 Tobias Fella <fella@posteo.de>
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 <tobias.fella@kde.org>
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
Copyright: 2020 Tobias Fella <fella@posteo.de>
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 <tobias.fella@kde.org>
Copyright: 2020-2022 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause

View File

@@ -1,26 +1,18 @@
# 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 <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
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)
set(PROJECT_VERSION "22.11")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.91.0")
set(KF5_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 ${KF_MIN_VERSION} REQUIRED NO_MODULE)
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
@@ -57,16 +49,16 @@ set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
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)
set_package_properties(KF5Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF5KirigamiAddons 0.6 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
@@ -82,15 +74,15 @@ if(ANDROID)
)
else()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
endif()
find_package(Quotient 0.6)
@@ -111,7 +103,6 @@ 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
@@ -125,8 +116,8 @@ find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
find_package(KF5DocTools ${KF5_MIN_VERSION})
set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
@@ -136,7 +127,6 @@ if(NOT Quotient_VERSION_MINOR GREATER 6)
endif()
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()
@@ -155,7 +145,7 @@ if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
add_subdirectory(autotests)
endif()
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
if(KF5DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()

0
LICENSES/MIT.txt Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
SPDX-License-Identifier: CC0-1.0
-->
# NeoChat

View File

@@ -14,8 +14,7 @@
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"
android:exported="true">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -23,6 +22,7 @@
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
@@ -38,6 +38,8 @@
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Splash screen -->

View File

@@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:3.6.4'
}
}
@@ -73,10 +73,6 @@ android {
defaultConfig {
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
applicationId "org.kde.neochat"
namespace "org.kde.neochat"
versionCode timestamp
versionName projectVersionFull
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
}

View File

@@ -8,9 +8,3 @@ 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
)

View File

@@ -136,7 +136,7 @@ void NeoChatRoomTest::initTestCase()
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
}
void NeoChatRoomTest::eventTest()

View File

@@ -1,591 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <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), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `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), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)");
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>&lt;p&gt;Test &lt;span style=&quot;font-size:50px;&quot;&gt;some&lt;/span&gt; code&lt;/p&gt;\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 &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << 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("&lt;plain text in tag bracket&gt;<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 &lt;strong&gt;with tags&lt;/strong&gt;</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&amp;via=matrix.org&amp;via=fedora.im "
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>");
const QString testOutputStringLink1 = QStringLiteral(
"<a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/"
"!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a "
"href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/"
"$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;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"

View File

@@ -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 <tobias.fella@kde.org>
- SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
-->
<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,21 +231,6 @@
<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>
<p>New features and bugfixes:</p>
<ul>
<li>Notifications will now be shown for all accounts, not just the active one</li>
<li>There is a new "compact" mode for the room list</li>
<li>You can now search in the room history</li>
<li>Emojis and Reactions have been significantly improved</li>
<li>Fixed several crashes around user invitations</li>
<li>Room permission settings can now be configured</li>
</ul>
</description>
</release>
<release version="22.11" date="2022-11-30">
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
</release>

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
[Desktop Entry]
Version=1.5
Name=NeoChat

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
# 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})

View File

@@ -1,122 +0,0 @@
<?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&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;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ı &copy; 2020-2022 Tobias Fella </para>
<para
>Telif hakkı &copy; 2020-2022 Carl Schwan </para>
<para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,52 @@
# 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 <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
add_library(neochat STATIC
controller.cpp
actionshandler.cpp
models/emojimodel.cpp
emojimodel.cpp
emojitones.cpp
models/customemojimodel.cpp
customemojimodel.cpp
clipboard.cpp
matriximageprovider.cpp
models/messageeventmodel.cpp
models/messagefiltermodel.cpp
models/roomlistmodel.cpp
models/sortfilterspacelistmodel.cpp
messageeventmodel.cpp
messagefiltermodel.cpp
roomlistmodel.cpp
sortfilterspacelistmodel.cpp
spacehierarchycache.cpp
roommanager.cpp
neochatroom.cpp
neochatuser.cpp
models/userlistmodel.cpp
models/userfiltermodel.cpp
models/publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp
userlistmodel.cpp
userfiltermodel.cpp
publicroomlistmodel.cpp
userdirectorylistmodel.cpp
keywordnotificationrulemodel.cpp
utils.cpp
notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp
sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
models/devicesmodel.cpp
devicesmodel.cpp
filetypesingleton.cpp
login.cpp
stickerevent.cpp
models/webshortcutmodel.cpp
webshortcutmodel.cpp
blurhash.cpp
blurhashimageprovider.cpp
joinrulesevent.cpp
models/collapsestateproxymodel.cpp
collapsestateproxymodel.cpp
urlhelper.cpp
windowcontroller.cpp
linkpreviewer.cpp
models/completionmodel.cpp
models/completionproxymodel.cpp
models/actionsmodel.cpp
models/serverlistmodel.cpp
models/statemodel.cpp
completionmodel.cpp
completionproxymodel.cpp
actionsmodel.cpp
serverlistmodel.cpp
statemodel.cpp
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
searchmodel.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 KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
target_link_libraries(neochat PUBLIC KF5::ConfigWidgets KF5::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif()
@@ -87,14 +87,13 @@ 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 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)
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)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -104,9 +103,6 @@ endif()
if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
if(SQLite3_FOUND)
target_link_libraries(neochat-app PRIVATE SQLite::SQLite3)
endif()
target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
@@ -175,13 +171,9 @@ if(ANDROID)
"window-new"
"globe"
"visibility"
"home"
"preferences-desktop-notification"
"computer-symbolic"
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -189,12 +181,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PUBLIC KF5::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
if (TARGET KF5::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()

View File

@@ -13,17 +13,31 @@
#include <KLocalizedString>
#include <QStringBuilder>
#include "actionsmodel.h"
#include "controller.h"
#include "models/actionsmodel.h"
#include "models/customemojimodel.h"
#include "customemojimodel.h"
#include "neochatconfig.h"
#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)
{
@@ -44,9 +58,9 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
void ActionsHandler::handleNewMessage()
void ActionsHandler::handleMessage()
{
checkEffects(m_room->chatBoxText());
checkEffects();
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
@@ -55,39 +69,13 @@ void ActionsHandler::handleNewMessage()
m_room->setChatBoxText({});
return;
}
QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
}
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
for (const auto &mention : *mentions) {
for (const auto &mention : *m_room->mentions()) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
}
@@ -95,16 +83,11 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
}
mentions->clear();
m_room->mentions()->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
auto match = sed.match(text);
auto match = sed.match(m_room->chatBoxText());
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
@@ -154,10 +137,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
}
handledText = CustomEmojiModel::instance().preprocessText(handledText);
TextHandler textHandler;
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
handledText = markdownToHTML(handledText);
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
handledText.remove("<p>");
handledText.remove("</p>");
@@ -166,13 +146,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
if (handledText.length() == 0) {
return;
}
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId());
}
void ActionsHandler::checkEffects(const QString &text)
void ActionsHandler::checkEffects()
{
std::optional<QString> effect = std::nullopt;
const auto &text = m_room->chatBoxText();
if (text.contains("\u2744")) {
effect = QLatin1String("snowflake");
} else if (text.contains("\u1F386")) {

View File

@@ -33,20 +33,14 @@ Q_SIGNALS:
public Q_SLOTS:
/**
* @brief Pre-process text and send message.
*/
void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
/// \brief Post a message.
///
/// This also interprets commands if any.
void handleMessage();
private:
NeoChatRoom *m_room = nullptr;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
void checkEffects();
};
QString markdownToHTML(const QString &markdown);

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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>
@@ -18,41 +19,6 @@ 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"),
@@ -149,7 +115,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("/spoiler %1").arg(text),
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
@@ -192,12 +158,11 @@ QVector<ActionsModel::Action> actions{
return QString();
}
#ifdef QUOTIENT_07
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString();
}
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Ban) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString();
}
@@ -243,40 +208,6 @@ 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) {
@@ -302,7 +233,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("part"),
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();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -310,7 +265,31 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("leave"),
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();
},
false,
std::nullopt,
kli18n("[<room alias or id>]"),
@@ -333,15 +312,14 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("roomnick"),
roomNickLambda,
false,
std::nullopt,
kli18n("<display name>"),
kli18n("Changes your display name in this room"),
},
Action{
QStringLiteral("myroomnick"),
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();
},
false,
std::nullopt,
kli18n("<display name>"),

49
src/actionsmodel.h Normal file
View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <KLazyLocalizedString>
#include <QAbstractListModel>
#include <events/roommessageevent.h>
class NeoChatRoom;
class ActionsModel : public QAbstractListModel
{
public:
struct Action {
// The prefix, without '/' and space after the word
QString prefix;
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.
bool messageAction;
// If this action changes the message type, this is the new message type. Otherwise it's nullopt
std::optional<Quotient::RoomMessageEvent::MsgType> messageType = std::nullopt;
KLazyLocalizedString parameters;
KLazyLocalizedString description;
};
static ActionsModel &instance()
{
static ActionsModel _instance;
return _instance;
}
enum Roles {
Prefix = Qt::DisplayRole,
Description,
CompletionType,
Parameters,
};
Q_ENUM(Roles);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
QVector<Action> &allActions() const;
private:
ActionsModel() = default;
};

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "blurhashimageprovider.h"

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once

View File

@@ -14,9 +14,9 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "models/actionsmodel.h"
#include "models/roomlistmodel.h"
#include "actionsmodel.h"
#include "neochatroom.h"
#include "roomlistmodel.h"
class SyntaxHighlighter : public QSyntaxHighlighter
{
@@ -105,19 +105,14 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
{
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr;
static NeoChatRoom *previousRoom = nullptr;
if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
}
previousRoom = m_room;
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
});
});
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
@@ -128,7 +123,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
return;
}
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start));
});
}
@@ -138,13 +133,12 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
#endif
const auto &text = getText();
const auto &text = m_room->chatBoxText();
auto start = std::min(cursor, text.size()) - 1;
while (start > -1) {
if (text.at(start) == QLatin1Char(' ')) {
@@ -156,20 +150,6 @@ int ChatDocumentHandler::completionStartIndex() const
return start;
}
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_document;
@@ -224,7 +204,7 @@ void ChatDocumentHandler::complete(int index)
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -233,11 +213,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, name, 0, 0, id});
m_room->mentions()->push_back({cursor, name, 0, 0, id});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('/'));
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -245,7 +225,7 @@ void ChatDocumentHandler::complete(int index)
cursor.insertText(QStringLiteral("/%1 ").arg(command));
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -254,11 +234,11 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, alias, 0, 0, alias});
m_room->mentions()->push_back({cursor, alias, 0, 0, alias});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto text = getText();
auto text = m_room->chatBoxText();
auto at = text.lastIndexOf(QLatin1Char(':'));
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
@@ -301,59 +281,3 @@ void ChatDocumentHandler::setSelectionEnd(int position)
m_selectionEnd = position;
Q_EMIT selectionEndChanged();
}
QString ChatDocumentHandler::getText() const
{
if (!m_room) {
return QString();
}
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
}
}
void ChatDocumentHandler::pushMention(const Mention mention) const
{
if (!m_room) {
return;
}
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
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();
}

View File

@@ -7,9 +7,8 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "models/completionmodel.h"
#include "models/userlistmodel.h"
#include "neochatroom.h"
#include "completionmodel.h"
#include "userlistmodel.h"
class QTextDocument;
class NeoChatRoom;
@@ -18,14 +17,6 @@ class SyntaxHighlighter;
class ChatDocumentHandler : public QObject
{
Q_OBJECT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
@@ -33,17 +24,11 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
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);
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
@@ -63,44 +48,26 @@ 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();
void cursorPositionChanged();
void roomChanged();
void completionModelChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
void mentionColorChanged();
private:
int completionStartIndex() const;
bool m_isEdit;
QQuickTextDocument *m_document;
NeoChatRoom *m_room = nullptr;
bool completionVisible = false;
QColor m_mentionColor;
QColor m_errorColor;
int m_cursorPosition;
int m_selectionStart;
int m_selectionEnd;
QString getText() const;
void pushMention(const Mention mention) const;
SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;

View File

@@ -33,14 +33,13 @@ QImage Clipboard::image() const
QString Clipboard::saveImage(QString localPath) const
{
QString imageDir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
if (!QDir().exists(imageDir)) {
QDir().mkdir(imageDir);
if (!QDir().exists(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)))) {
QDir().mkdir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
}
if (localPath.isEmpty()) {
localPath = QStringLiteral("file://%1/%2.png").arg(imageDir, QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
localPath = QStringLiteral("file://%1/screenshots/%2.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-hh-mm-ss")));
}
QUrl url(localPath);
if (!url.isLocalFile()) {
@@ -52,30 +51,21 @@ QString Clipboard::saveImage(QString localPath) const
return {};
}
if (image.save(url.toLocalFile())) {
return localPath;
} else {
return {};
QDir dir;
if (!dir.exists(QFileInfo(url.fileName()).absoluteFilePath())) {
dir.mkpath(QFileInfo(url.fileName()).absoluteFilePath());
}
image.save(url.toLocalFile());
return localPath;
}
void Clipboard::saveText(QString message)
{
static QRegularExpression re(QStringLiteral("<[^>]*>"));
QRegularExpression re("<[^>]*>");
auto *mineData = new QMimeData; // ownership is transferred to clipboard
mineData->setHtml(message);
mineData->setText(message.replace(re, QString()));
mineData->setText(message.replace(re, ""));
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);
}
}
}

View File

@@ -26,7 +26,6 @@ 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;

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include <KLocalizedString>
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)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
!= 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
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0),
MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole)
!= MessageEventModel::DelegateType::State // If it's not a state event
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {
break;
}
}
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1) {
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
}
}
QString text = chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and action 3]", ", ");
text += chunks.takeFirst();
}
text += i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
};
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QString aggregateEventToString(int row) const;
};

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// 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"
@@ -15,7 +16,7 @@ CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_userListModel(new UserListModel(this))
, m_emojiModel(new QConcatenateTablesProxyModel(this))
, m_emojiModel(new KConcatenateRowsProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
connect(this, &CompletionModel::roomChanged, this, [this]() {
@@ -144,7 +145,7 @@ void CompletionModel::updateCompletion()
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':'))
&& (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);

View File

@@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QConcatenateTablesProxyModel>
#include <QSortFilterProxyModel>
#include <KConcatenateRowsProxyModel>
#include "roomlistmodel.h"
class CompletionProxyModel;
@@ -13,89 +14,41 @@ 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, /**< A user in the current room. */
Room, /**< A matrix room. */
Emoji, /**< An emoji. */
Command, /**< A / command. */
None, /**< No available completion for the current text. */
User,
Room,
Emoji,
Command,
None,
};
Q_ENUM(AutoCompletionType)
/**
* @brief Defines the model roles.
*/
enum Roles {
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. */
Text = Qt::DisplayRole,
Subtitle,
Icon,
ReplacedText,
};
Q_ENUM(Roles);
CompletionModel(QObject *parent = nullptr);
/**
* @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()
*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
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);
@@ -104,7 +57,6 @@ public:
void setRoomListModel(RoomListModel *roomListModel);
AutoCompletionType autoCompletionType() const;
void setAutoCompletionType(AutoCompletionType autoCompletionType);
Q_SIGNALS:
void textChanged();
@@ -119,10 +71,10 @@ private:
NeoChatRoom *m_room = nullptr;
AutoCompletionType m_autoCompletionType = None;
void updateCompletion();
void setAutoCompletionType(AutoCompletionType autoCompletionType);
UserListModel *m_userListModel;
RoomListModel *m_roomListModel;
QConcatenateTablesProxyModel *m_emojiModel;
KConcatenateRowsProxyModel *m_emojiModel;
};
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);

View File

@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h"
#include <QDebug>
#include "neochatroom.h"
bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
Q_UNUSED(sourceParent);
@@ -43,6 +44,7 @@ int CompletionProxyModel::secondaryFilterRole() const
void CompletionProxyModel::setSecondaryFilterRole(int role)
{
m_secondaryFilterRole = role;
Q_EMIT secondaryFilterRoleChanged();
}
QString CompletionProxyModel::filterText() const
@@ -53,6 +55,7 @@ QString CompletionProxyModel::filterText() const
void CompletionProxyModel::setFilterText(const QString &filterText)
{
m_filterText = filterText;
Q_EMIT filterTextChanged();
}
void CompletionProxyModel::setFullText(const QString &fullText)

View File

@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QSortFilterProxyModel>
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:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
int secondaryFilterRole() const;
void setSecondaryFilterRole(int role);
QString filterText() const;
void setFilterText(const QString &filterText);
void setFullText(const QString &fullText);
Q_SIGNALS:
void secondaryFilterRoleChanged();
void filterTextChanged();
private:
int m_secondaryFilterRole = -1;
QString m_filterText;
QString m_fullText;
};

View File

@@ -1,14 +1,10 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
#include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <KConfig>
#include <KConfigGroup>
@@ -41,7 +37,6 @@
#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
@@ -133,7 +128,16 @@ Controller::Controller(QObject *parent)
if (AccountRegistry::instance().size() > oldAccountCount) {
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
connect(connection, &Connection::syncDone, this, [=]() {
handleNotifications(connection);
bool changes = false;
for (const auto &room : connection->allRooms()) {
if (m_notificationCounts[room] != room->unreadStats().notableCount) {
m_notificationCounts[room] = room->unreadStats().notableCount;
changes = true;
}
}
if (changes) {
handleNotifications();
}
});
}
oldAccountCount = AccountRegistry::instance().size();
@@ -142,16 +146,19 @@ Controller::Controller(QObject *parent)
}
#ifdef QUOTIENT_07
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
void Controller::handleNotifications()
{
static QStringList initial;
static bool initial = true;
static QStringList oldNotifications;
auto job = connection->callApi<GetNotificationsJob>();
if (!m_connection) {
return;
}
auto job = m_connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [job, connection]() {
connect(job, &BaseJob::success, this, [this, job]() {
const auto notifications = job->jsonData()["notifications"].toArray();
if (!initial.contains(connection->user()->id())) {
initial.append(connection->user()->id());
if (initial) {
initial = false;
for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
}
@@ -167,12 +174,10 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
continue;
}
oldNotifications += notification["event"].toObject()["event_id"].toString();
auto room = connection->room(notification["room_id"].toString());
auto room = m_connection->room(notification["room_id"].toString());
// If room exists, room is NOT active OR the application is NOT active, show notification
if (room
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
if (room && !(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());
@@ -191,7 +196,7 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED
auto decrypted = connection->decryptNotification(notification);
auto decrypted = m_connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString();
#endif
if (body.isEmpty()) {
@@ -376,11 +381,6 @@ void Controller::invokeLogin()
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
} else if (error == "Connection closed") {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
@@ -593,7 +593,6 @@ 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) {
@@ -612,23 +611,12 @@ 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()
@@ -651,17 +639,9 @@ 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());
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [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
@@ -816,22 +796,3 @@ 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();
}

View File

@@ -42,7 +42,6 @@ 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();
@@ -104,23 +103,6 @@ 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);
@@ -137,7 +119,7 @@ private:
bool hasWindowSystem() const;
#ifdef QUOTIENT_07
void handleNotifications(QPointer<Quotient::Connection> connection);
void handleNotifications();
#endif
private Q_SLOTS:
@@ -169,8 +151,6 @@ 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);

View File

@@ -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, [name, job] {
connect(job, &BaseJob::success, this, [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,8 +141,6 @@ 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();

57
src/customemojimodel.h Normal file
View File

@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <memory>
#include <QRegularExpression>
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
Q_PROPERTY(QString name MEMBER name)
};
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
public:
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
};
Q_ENUM(Roles);
static CustomEmojiModel &instance()
{
static CustomEmojiModel _instance;
return _instance;
}
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE QString preprocessText(const QString &it);
Q_INVOKABLE QVariantList filterModel(const QString &filter);
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
Q_INVOKABLE void removeEmoji(const QString &name);
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
void fetchEmojis();
};

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "devicesmodel.h"
@@ -70,12 +70,7 @@ void DevicesModel::logout(int index, const QString &password)
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
};
if (job->error() != BaseJob::Success) {
if (job->error() != 0) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
@@ -84,9 +79,11 @@ void DevicesModel::logout(int index, const QString &password)
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess);
} else {
onSuccess();
connect(innerJob, &BaseJob::success, this, [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
});
}
});
}

47
src/devicesmodel.h Normal file
View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <csapi/definitions/client_device.h>
namespace Quotient
{
class Connection;
}
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
public:
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE void logout(int index, const QString &password);
Q_INVOKABLE void setName(int index, const QString &name);
Quotient::Connection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
};

View File

@@ -51,30 +51,12 @@ 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:
@@ -84,92 +66,47 @@ public:
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum RoleNames {
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. */
ShortNameRole = Qt::DisplayRole,
UnicodeRole,
InvalidRole = 50,
DisplayRole = 51,
ReplacedTextRole = 52,
DescriptionRole = 53,
};
Q_ENUM(RoleNames);
/**
* @brief Defines the potential categories an emoji can be placed in.
*/
enum Category {
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, /**< ??? */
Custom,
Search,
SearchNoCustom,
History,
HistoryNoCustom,
Smileys,
People,
Nature,
Food,
Activities,
Travel,
Objects,
Symbols,
Flags,
Component,
};
Q_ENUM(Category)
/**
* @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]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @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 QVariantList history() const;
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;

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "emojitones.h"
#include "models/emojimodel.h"
#include "emojimodel.h"
QMultiHash<QString, QVariant> EmojiTones::_tones = {
#include "emojitones_data.h"

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "filetransferpseudojob.h"
@@ -25,7 +25,6 @@ void FileTransferPseudoJob::fileTransferProgress(QString id, qint64 progress, qi
void FileTransferPseudoJob::fileTransferCompleted(QString id, QUrl localFile)
{
Q_UNUSED(localFile);
if (id != m_eventId) {
return;
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

View File

@@ -7,54 +7,23 @@
#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, /**< The push rule keyword. */
NameRole = Qt::DisplayRole,
};
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:

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "login.h"

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

View File

@@ -42,38 +42,39 @@
#include "blurhashimageprovider.h"
#include "chatdocumenthandler.h"
#include "clipboard.h"
#include "collapsestateproxymodel.h"
#include "controller.h"
#include "customemojimodel.h"
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h"
#include "joinrulesevent.h"
#include "linkpreviewer.h"
#include "keywordnotificationrulemodel.h"
#include "login.h"
#include "matriximageprovider.h"
#include "models/collapsestateproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/emojimodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#include "searchmodel.h"
#ifdef QUOTIENT_07
#include "pollhandler.h"
#endif
#include "publicroomlistmodel.h"
#include "roomlistmodel.h"
#include "roommanager.h"
#include "serverlistmodel.h"
#include "sortfilterroomlistmodel.h"
#include "sortfilterspacelistmodel.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "userdirectorylistmodel.h"
#include "userfiltermodel.h"
#include "userlistmodel.h"
#include "webshortcutmodel.h"
#include "windowcontroller.h"
#ifdef QUOTIENT_07
#include <keyverificationsession.h>
@@ -81,19 +82,15 @@
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
#include "models/completionmodel.h"
#include "models/statemodel.h"
#include "completionmodel.h"
#include "neochatuser.h"
#include "statemodel.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#ifdef Q_OS_WINDOWS
#include <Windows.h>
#endif
using namespace Quotient;
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
@@ -156,7 +153,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("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("fella@posteo.de"), 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"));

View File

@@ -27,13 +27,14 @@
#include <KLocalizedString>
#include "neochatuser.h"
#include "utils.h"
using namespace Quotient;
QHash<int, QByteArray> MessageEventModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DelegateTypeRole] = "delegateType";
roles[EventTypeRole] = "eventType";
roles[MessageRole] = "message";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
@@ -45,16 +46,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";
@@ -63,17 +64,15 @@ 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,11 +85,6 @@ MessageEventModel::MessageEventModel(QObject *parent)
MessageEventModel::~MessageEventModel() = default;
NeoChatRoom *MessageEventModel::room() const
{
return m_currentRoom;
}
void MessageEventModel::setRoom(NeoChatRoom *room)
{
if (room == m_currentRoom) {
@@ -181,7 +175,6 @@ 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
}
@@ -220,12 +213,6 @@ 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);
@@ -318,7 +305,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
return -1;
}
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
if (data(index(row, 0), EventTypeRole).toInt() == ReadMarker || data(index(row, 0), EventTypeRole).toInt() == Other) {
row++;
}
}
@@ -449,10 +436,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (m_lastReadEventIndex.row() == row) {
switch (role) {
case DelegateTypeRole:
case EventTypeRole:
return DelegateType::ReadMarker;
case TimeRole: {
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime();
const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
}
@@ -475,14 +462,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_currentRoom->eventToString(evt, Qt::RichText);
}
if (role == GenericDisplayRole) {
if (evt.isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return m_currentRoom->eventToGenericString(evt);
}
if (role == FormattedBodyRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
@@ -498,10 +477,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == SourceRole) {
return QJsonDocument(evt.fullJson()).toJson();
return evt.originalJson();
}
if (role == DelegateTypeRole) {
if (role == EventTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
@@ -514,8 +493,6 @@ 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;
}
@@ -570,9 +547,6 @@ 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
@@ -586,7 +560,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == HighlightRole) {
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
return m_currentRoom->isEventHighlighted(&evt);
}
if (role == FileMimetypeIcon) {
@@ -614,25 +588,12 @@ 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();
}
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()) {
auto *memberEvent = timelineIt->viewAs<RoomMemberEvent>();
if (memberEvent) {
if ((memberEvent->isJoin() || memberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return EventStatus::Hidden;
}
}
@@ -663,6 +624,14 @@ 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();
}
@@ -678,11 +647,29 @@ 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();
}
@@ -768,7 +755,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) == 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();
}
@@ -793,91 +780,6 @@ 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()) {
@@ -978,18 +880,29 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
}
if (role == IsRedactedRole) {
return evt.isRedacted();
if (role == IsNameChangeRole) {
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
if (roomMemberEvent) {
return roomMemberEvent->isRename();
}
return false;
}
if (role == IsPendingRole) {
return row < static_cast<int>(m_currentRoom->pendingEvents().size());
if (role == IsAvatarChangeRole) {
auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
if (roomMemberEvent) {
return roomMemberEvent->isAvatarUpdate();
}
return false;
}
if (role == IsRedactedRole) {
return evt.isRedacted();
}
return {};
}
int MessageEventModel::eventIdToRow(const QString &eventID) const
int MessageEventModel::eventIDToIndex(const QString &eventID) const
{
const auto it = m_currentRoom->findInTimeline(eventID);
if (it == m_currentRoom->historyEdge()) {
@@ -1031,7 +944,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(eventIdToRow(eventId), 0);
QModelIndex idx = index(eventIDToIndex(eventId), 0);
targetMessage.insert("message", idx.data(Qt::UserRole + 2));
return targetMessage;
@@ -1041,21 +954,18 @@ QVariant MessageEventModel::getLastLocalUserMessageEventId()
return targetMessage;
}
QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
{
QVariantMap replyResponse;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + startRow;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin() + baseline;
// set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
// set a cap limit of baseline + 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(startRow + 35, m_currentRoom->timelineSize());
const auto limit = timelineBottom + std::min(baseline + 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();
@@ -1070,7 +980,7 @@ QVariant MessageEventModel::getLatestMessageFromRow(const int startRow)
}
replyResponse.insert("event_id", eventId);
// Need to get the message from the original eventId or body will have * on the front
QModelIndex idx = index(eventIdToRow(eventId), 0);
QModelIndex idx = index(eventIDToIndex(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());

125
src/messageeventmodel.h Normal file
View File

@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#include "neochatroom.h"
class MessageEventModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
enum DelegateType {
Emote,
Notice,
Image,
Audio,
Video,
File,
Message,
Sticker,
State,
Encrypted,
ReadMarker,
Poll,
Other,
};
Q_ENUM(DelegateType);
enum EventRoles {
EventTypeRole = Qt::UserRole + 1,
MessageRole,
EventIdRole,
TimeRole,
SectionRole,
AuthorRole,
ContentRole,
ContentTypeRole,
HighlightRole,
SpecialMarksRole,
LongOperationRole,
AnnotationRole,
UserMarkerRole,
FormattedBodyRole,
MimeTypeRole,
FileMimetypeIcon,
IsReplyRole,
ReplyRole,
ReplyIdRole,
ShowAuthorRole,
ShowSectionRole,
ReactionRole,
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,
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
explicit MessageEventModel(QObject *parent = nullptr);
~MessageEventModel() override;
[[nodiscard]] NeoChatRoom *room() const
{
return m_currentRoom;
}
void setRoom(NeoChatRoom *room);
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const;
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
Q_INVOKABLE void loadReply(const QModelIndex &row);
private Q_SLOTS:
int refreshEvent(const QString &eventId);
void refreshRow(int row);
private:
NeoChatRoom *m_currentRoom = nullptr;
QString lastReadEventId;
QPersistentModelIndex m_lastReadEventIndex;
int rowBelowInserted = -1;
bool movingEvent = false;
[[nodiscard]] int timelineBaseIndex() const;
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
[[nodiscard]] static QString renderDate(const QDateTime &timestamp);
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QVector<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
Q_SIGNALS:
void roomChanged();
void fancyEffectsReasonFound(const QString &fancyEffect);
};

View File

@@ -11,20 +11,23 @@ using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
invalidateFilter();
beginResetModel();
endResetModel();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
invalidateFilter();
beginResetModel();
endResetModel();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
invalidateFilter();
beginResetModel();
endResetModel();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
invalidateFilter();
beginResetModel();
endResetModel();
});
}
@@ -32,20 +35,31 @@ 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::DelegateTypeRole).toInt();
const auto eventType = index.data(MessageEventModel::EventTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;
}
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
return false;
}
return true;
}

View File

@@ -5,21 +5,10 @@
#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;
};

View File

@@ -1,94 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <KLazyLocalizedString>
#include <QAbstractListModel>
#include <events/roommessageevent.h>
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 {
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;
/**
* @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;
/**
* @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; /**< The input parameters expected by the action. */
KLazyLocalizedString description; /**< The description of the action. */
};
static ActionsModel &instance()
{
static ActionsModel _instance;
return _instance;
}
/**
* @brief Defines the model roles.
*/
enum Roles {
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);
/**
* @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:
ActionsModel() = default;
};

View File

@@ -1,134 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include "messageeventmodel.h"
#include <KLocalizedString>
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
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::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
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
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
) {
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = "<style>a {text-decoration: none;}</style>"; // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()["id"].toString(),
uniqueAuthors[0].toMap()["color"].toString(),
uniqueAuthors[0].toMap()["displayName"].toString());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{"author", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{"authorDisplayName", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
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
) {
break;
}
}
return stateEvents;
}
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
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
) {
break;
}
}
return uniqueAuthors;
}

View File

@@ -1,73 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#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, /**< 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;
/**
* @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 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.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
};

View File

@@ -1,77 +0,0 @@
// 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
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);
private:
int m_secondaryFilterRole = -1;
QString m_filterText;
QString m_fullText;
};

View File

@@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QRegularExpression>
#include <memory>
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
Q_GADGET
Q_PROPERTY(QString unicode MEMBER url)
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, /**< 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);
static CustomEmojiModel &instance()
{
static CustomEmojiModel _instance;
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:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
void fetchEmojis();
};

View File

@@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <csapi/definitions/client_device.h>
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, /**< 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;
/**
* @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;
Q_SIGNALS:
void connectionChanged();
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
};

View File

@@ -1,200 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#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, /**< 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 {
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, /**< The mime type of the message's file or media. */
FileMimetypeIcon, /**< The icon name for the mime type of a file. */
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, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
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. */
// For debugging
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)
explicit MessageEventModel(QObject *parent = nullptr);
~MessageEventModel() override;
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 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 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();
/**
* @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);
void refreshRow(int row);
private:
NeoChatRoom *m_currentRoom = nullptr;
QString lastReadEventId;
QPersistentModelIndex m_lastReadEventIndex;
int rowBelowInserted = -1;
bool movingEvent = false;
[[nodiscard]] int timelineBaseIndex() const;
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
[[nodiscard]] static QString renderDate(const QDateTime &timestamp);
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QVector<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_extraEvents;
Q_SIGNALS:
void roomChanged();
void fancyEffectsReasonFound(const QString &fancyEffect);
};

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