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
220 changed files with 34363 additions and 59598 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat", "id": "org.kde.neochat",
"branch": "master", "branch": "master",
"runtime": "org.kde.Platform", "runtime": "org.kde.Platform",
"runtime-version": "5.15-22.08", "runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk", "sdk": "org.kde.Sdk",
"command": "neochat", "command": "neochat",
"tags": [ "tags": [
@@ -28,17 +28,6 @@
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ] "sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
}, },
{
"name": "kquickcharts",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/frameworks/kquickcharts.git",
"branch": "kf5"
}
]
},
{ {
"name": "kquickimageeditor", "name": "kquickimageeditor",
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",

1
.gitignore vendored
View File

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

View File

@@ -4,11 +4,9 @@
include: 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/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.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml # 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.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.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 - 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 # SPDX-License-Identifier: BSD-2-Clause
Dependencies: Dependencies:
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5'] - 'on': ['@all']
'require': 'require':
'frameworks/extra-cmake-modules': '@stable' 'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable' 'frameworks/kcoreaddons': '@stable'
@@ -11,7 +11,6 @@ Dependencies:
'frameworks/kconfig': '@stable' 'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable' 'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable' 'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable' 'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable' 'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable' 'frameworks/sonnet': '@stable'
@@ -20,43 +19,15 @@ Dependencies:
'third-party/qtkeychain': '@latest' 'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest' 'third-party/cmark': '@latest'
'third-party/qcoro': '@latest' 'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5'] - 'on': ['Windows', 'Linux', 'FreeBSD']
'require': 'require':
'frameworks/qqc2-desktop-style': '@stable' 'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable' 'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable' 'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable' 'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'frameworks/kdbusaddons': '@stable' '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: Options:
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ] 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 License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
Files: android/res/drawable/splash.xml 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 License: BSD-2-Clause
Files: .gitignore Files: .gitignore
@@ -27,11 +27,11 @@ Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: BSD-2-Clause License: BSD-2-Clause
Files: src/neochatconfig.kcfg 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 License: BSD-2-Clause
Files: src/neochat.notifyrc Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org> Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause License: BSD-2-Clause
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png 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 License: CC0-1.0
Files: .flatpak-manifest.json 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 License: BSD-2-Clause

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ buildscript {
} }
dependencies { 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 { defaultConfig {
minSdkVersion qtMinSdkVersion minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion targetSdkVersion qtTargetSdkVersion
applicationId "org.kde.neochat"
namespace "org.kde.neochat"
versionCode timestamp
versionName projectVersionFull
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp] manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
} }

View File

@@ -8,9 +8,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test Quotient LINK_LIBRARIES neochat Qt::Test Quotient
TEST_NAME neochatroomtest 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() void NeoChatRoomTest::subtitleTextTest()
{ {
QCOMPARE(room->timelineSize(), 1); 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() void NeoChatRoomTest::eventTest()

View File

@@ -1,497 +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 <connection.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 recieveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
void receiveRichUserPill();
void receiveRichStrikethrough();
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl();
};
#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": 1235
}
}
],
"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::recieveRichInPlainOut()
{
const QString testInputString = QStringLiteral("a &amp; b");
const QString testOutputString = QStringLiteral("a & b");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), 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.");
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);
}
/**
* 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().back().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);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

View File

@@ -2,7 +2,7 @@
<!-- <!--
- SPDX-License-Identifier: CC0-1.0 - SPDX-License-Identifier: CC0-1.0
- SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org> - 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"> <component type="desktop">
<id>org.kde.neochat</id> <id>org.kde.neochat</id>
@@ -179,7 +179,7 @@
<category>Network</category> <category>Network</category>
</categories> </categories>
<developer_name>The KDE Community</developer_name> <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="az">KDE Cəmiyyəti</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name> <developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name> <developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
@@ -231,20 +231,6 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<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"> <release version="22.11" date="2022-11-30">
<url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url> <url>https://plasma-mobile.org/2022/11/30/plasma-mobile-gear-22-11/</url>
</release> </release>

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: CC0-1.0 # 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] [Desktop Entry]
Version=1.5 Version=1.5
Name=NeoChat 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,53 +1,52 @@
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu> # SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de> # 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 # SPDX-License-Identifier: BSD-2-Clause
add_library(neochat STATIC add_library(neochat STATIC
controller.cpp controller.cpp
actionshandler.cpp actionshandler.cpp
models/emojimodel.cpp emojimodel.cpp
emojitones.cpp emojitones.cpp
models/customemojimodel.cpp customemojimodel.cpp
clipboard.cpp clipboard.cpp
matriximageprovider.cpp matriximageprovider.cpp
models/messageeventmodel.cpp messageeventmodel.cpp
models/messagefiltermodel.cpp messagefiltermodel.cpp
models/roomlistmodel.cpp roomlistmodel.cpp
models/sortfilterspacelistmodel.cpp sortfilterspacelistmodel.cpp
spacehierarchycache.cpp spacehierarchycache.cpp
roommanager.cpp roommanager.cpp
neochatroom.cpp neochatroom.cpp
neochatuser.cpp neochatuser.cpp
models/userlistmodel.cpp userlistmodel.cpp
models/userfiltermodel.cpp userfiltermodel.cpp
models/publicroomlistmodel.cpp publicroomlistmodel.cpp
models/userdirectorylistmodel.cpp userdirectorylistmodel.cpp
models/keywordnotificationrulemodel.cpp keywordnotificationrulemodel.cpp
utils.cpp
notificationsmanager.cpp notificationsmanager.cpp
models/sortfilterroomlistmodel.cpp sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp chatdocumenthandler.cpp
models/devicesmodel.cpp devicesmodel.cpp
filetypesingleton.cpp filetypesingleton.cpp
login.cpp login.cpp
stickerevent.cpp stickerevent.cpp
models/webshortcutmodel.cpp webshortcutmodel.cpp
blurhash.cpp blurhash.cpp
blurhashimageprovider.cpp blurhashimageprovider.cpp
joinrulesevent.cpp joinrulesevent.cpp
models/collapsestateproxymodel.cpp collapsestateproxymodel.cpp
urlhelper.cpp urlhelper.cpp
windowcontroller.cpp windowcontroller.cpp
linkpreviewer.cpp linkpreviewer.cpp
models/completionmodel.cpp completionmodel.cpp
models/completionproxymodel.cpp completionproxymodel.cpp
models/actionsmodel.cpp actionsmodel.cpp
models/serverlistmodel.cpp serverlistmodel.cpp
models/statemodel.cpp statemodel.cpp
filetransferpseudojob.cpp filetransferpseudojob.cpp
models/searchmodel.cpp searchmodel.cpp
texthandler.cpp
models/locationsmodel.cpp
) )
add_executable(neochat-app add_executable(neochat-app
@@ -80,7 +79,7 @@ if(NOT ANDROID)
else() else()
target_sources(neochat PRIVATE trayicon.cpp) target_sources(neochat PRIVATE trayicon.cpp)
endif() 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_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM) target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif() endif()
@@ -88,14 +87,13 @@ endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE) if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_sources(neochat-app PRIVATE res_desktop.qrc) target_sources(neochat-app PRIVATE res_desktop.qrc)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER) target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp) target_sources(neochat PRIVATE runner.cpp)
else() else()
target_sources(neochat-app PRIVATE res_android.qrc) target_sources(neochat-app PRIVATE res_android.qrc)
endif() endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR}) 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) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK) if(NEOCHAT_FLATPAK)
@@ -105,9 +103,6 @@ endif()
if(ANDROID) if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc) target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL) 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_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL) target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS kirigami_package_breeze_icons(ICONS
@@ -176,12 +171,9 @@ if(ANDROID)
"window-new" "window-new"
"globe" "globe"
"visibility" "visibility"
"home"
"preferences-desktop-notification"
"computer-symbolic"
) )
else() 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}) install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif() endif()
@@ -189,12 +181,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat") set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif() endif()
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons) if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons) target_link_libraries(neochat PUBLIC KF5::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS) target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif() endif()
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets) if (TARGET KF5::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO) target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif() endif()

View File

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

View File

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

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: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#include "actionsmodel.h" #include "actionsmodel.h"
@@ -115,7 +115,7 @@ QVector<ActionsModel::Action> actions{
QStringLiteral("spoiler"), QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room) { [](const QString &text, NeoChatRoom *room) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML. // 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), QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text, RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(), room->chatBoxReplyId(),
@@ -158,12 +158,11 @@ QVector<ActionsModel::Action> actions{
return QString(); return QString();
} }
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text); if (room->currentState().get<RoomMemberEvent>(text)->membership() == Membership::Invite) {
if (roomMemberEvent && roomMemberEvent->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)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString(); 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)); Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString(); return QString();
} }
@@ -209,40 +208,6 @@ QVector<ActionsModel::Action> actions{
kli18n("<room alias or id>"), kli18n("<room alias or id>"),
kli18n("Joins the given room"), 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{ Action{
QStringLiteral("j"), QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room) { [](const QString &text, NeoChatRoom *room) {

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: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once #pragma once

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 // SPDX-License-Identifier: LGPL-2.0-or-later
#include "blurhashimageprovider.h" #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 // SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once #pragma once

View File

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

View File

@@ -7,9 +7,8 @@
#include <QQuickTextDocument> #include <QQuickTextDocument>
#include <QTextCursor> #include <QTextCursor>
#include "models/completionmodel.h" #include "completionmodel.h"
#include "models/userlistmodel.h" #include "userlistmodel.h"
#include "neochatroom.h"
class QTextDocument; class QTextDocument;
class NeoChatRoom; class NeoChatRoom;
@@ -18,14 +17,6 @@ class SyntaxHighlighter;
class ChatDocumentHandler : public QObject class ChatDocumentHandler : public QObject
{ {
Q_OBJECT 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(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged) Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged) Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
@@ -33,14 +24,11 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged) Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
public: public:
explicit ChatDocumentHandler(QObject *parent = nullptr); explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const; [[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document); void setDocument(QQuickTextDocument *document);
@@ -61,7 +49,6 @@ public:
void updateCompletions(); void updateCompletions();
CompletionModel *completionModel() const; CompletionModel *completionModel() const;
Q_SIGNALS: Q_SIGNALS:
void isEditChanged();
void documentChanged(); void documentChanged();
void cursorPositionChanged(); void cursorPositionChanged();
void roomChanged(); void roomChanged();
@@ -72,8 +59,6 @@ Q_SIGNALS:
private: private:
int completionStartIndex() const; int completionStartIndex() const;
bool m_isEdit;
QQuickTextDocument *m_document; QQuickTextDocument *m_document;
NeoChatRoom *m_room = nullptr; NeoChatRoom *m_room = nullptr;
@@ -83,9 +68,6 @@ private:
int m_selectionStart; int m_selectionStart;
int m_selectionEnd; int m_selectionEnd;
QString getText() const;
void pushMention(const Mention mention) const;
SyntaxHighlighter *m_highlighter = nullptr; SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None; CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;

View File

@@ -33,14 +33,13 @@ QImage Clipboard::image() const
QString Clipboard::saveImage(QString localPath) const QString Clipboard::saveImage(QString localPath) const
{ {
QString imageDir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))); if (!QDir().exists(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)))) {
QDir().mkdir(QStringLiteral("%1/screenshots").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
if (!QDir().exists(imageDir)) {
QDir().mkdir(imageDir);
} }
if (localPath.isEmpty()) { 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); QUrl url(localPath);
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
@@ -52,30 +51,21 @@ QString Clipboard::saveImage(QString localPath) const
return {}; return {};
} }
if (image.save(url.toLocalFile())) { QDir dir;
return localPath; if (!dir.exists(QFileInfo(url.fileName()).absoluteFilePath())) {
} else { dir.mkpath(QFileInfo(url.fileName()).absoluteFilePath());
return {};
} }
image.save(url.toLocalFile());
return localPath;
} }
void Clipboard::saveText(QString message) void Clipboard::saveText(QString message)
{ {
static QRegularExpression re(QStringLiteral("<[^>]*>")); QRegularExpression re("<[^>]*>");
auto *mineData = new QMimeData; // ownership is transferred to clipboard auto *mineData = new QMimeData; // ownership is transferred to clipboard
mineData->setHtml(message); mineData->setHtml(message);
mineData->setText(message.replace(re, QString())); mineData->setText(message.replace(re, ""));
m_clipboard->setMimeData(mineData); 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 QString saveImage(QString localPath = {}) const;
Q_INVOKABLE void saveText(QString message); Q_INVOKABLE void saveText(QString message);
Q_INVOKABLE void setImage(const QUrl &image);
private: private:
QClipboard *m_clipboard; 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,4 +1,4 @@
// 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 // SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionmodel.h" #include "completionmodel.h"
@@ -16,7 +16,7 @@ CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel()) , m_filterModel(new CompletionProxyModel())
, m_userListModel(new UserListModel(this)) , 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::textChanged, this, &CompletionModel::updateCompletion);
connect(this, &CompletionModel::roomChanged, this, [this]() { connect(this, &CompletionModel::roomChanged, this, [this]() {
@@ -145,7 +145,7 @@ void CompletionModel::updateCompletion()
m_filterModel->setFullText(m_fullText); m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text); m_filterModel->setFilterText(m_text);
m_filterModel->invalidate(); 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) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) { || (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel); 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 // SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once #pragma once
#include <QConcatenateTablesProxyModel>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <KConcatenateRowsProxyModel>
#include "roomlistmodel.h" #include "roomlistmodel.h"
class CompletionProxyModel; class CompletionProxyModel;
@@ -74,6 +75,6 @@ private:
UserListModel *m_userListModel; UserListModel *m_userListModel;
RoomListModel *m_roomListModel; RoomListModel *m_roomListModel;
QConcatenateTablesProxyModel *m_emojiModel; KConcatenateRowsProxyModel *m_emojiModel;
}; };
Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType); Q_DECLARE_METATYPE(CompletionModel::AutoCompletionType);

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: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#include "completionproxymodel.h" #include "completionproxymodel.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: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once #pragma once

View File

@@ -1,14 +1,10 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org> // 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 // SPDX-License-Identifier: GPL-3.0-only
#include "controller.h" #include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h> #include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <KConfig> #include <KConfig>
#include <KConfigGroup> #include <KConfigGroup>
@@ -41,7 +37,6 @@
#include <csapi/content-repo.h> #include <csapi/content-repo.h>
#include <csapi/logout.h> #include <csapi/logout.h>
#include <csapi/profile.h> #include <csapi/profile.h>
#include <jobs/downloadfilejob.h>
#include <qt_connection_util.h> #include <qt_connection_util.h>
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
@@ -133,7 +128,16 @@ Controller::Controller(QObject *parent)
if (AccountRegistry::instance().size() > oldAccountCount) { if (AccountRegistry::instance().size() > oldAccountCount) {
auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1]; auto connection = AccountRegistry::instance().accounts()[AccountRegistry::instance().size() - 1];
connect(connection, &Connection::syncDone, this, [=]() { 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(); oldAccountCount = AccountRegistry::instance().size();
@@ -142,16 +146,19 @@ Controller::Controller(QObject *parent)
} }
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
void Controller::handleNotifications(QPointer<Quotient::Connection> connection) void Controller::handleNotifications()
{ {
static QStringList initial; static bool initial = true;
static QStringList oldNotifications; 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(); const auto notifications = job->jsonData()["notifications"].toArray();
if (!initial.contains(connection->user()->id())) { if (initial) {
initial.append(connection->user()->id()); initial = false;
for (const auto &n : notifications) { for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString(); oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
} }
@@ -167,12 +174,10 @@ void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
continue; continue;
} }
oldNotifications += notification["event"].toObject()["event_id"].toString(); 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 exists, room is NOT active OR the application is NOT active, show notification
if (room if (room && !(room->id() == RoomManager::instance().currentRoom()->id() && QGuiApplication::applicationState() == Qt::ApplicationActive)) {
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
// The room might have been deleted (for example rejected invitation). // The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"].toObject()["sender"].toString()); 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") { if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED #ifdef Quotient_E2EE_ENABLED
auto decrypted = connection->decryptNotification(notification); auto decrypted = m_connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString(); body = decrypted["content"].toObject()["body"].toString();
#endif #endif
if (body.isEmpty()) { if (body.isEmpty()) {
@@ -376,11 +381,6 @@ void Controller::invokeLogin()
if (error == "Unrecognised access token") { if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked")); Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false); 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 { } else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error)); Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true); logout(connection, true);
@@ -611,11 +611,6 @@ void Controller::setActiveConnection(Connection *connection)
m_isOnline = true; m_isOnline = true;
Q_EMIT isOnlineChanged(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."));
}
});
} else { } else {
NeoChatConfig::self()->setActiveConnection(QString()); NeoChatConfig::self()->setActiveConnection(QString());
} }
@@ -644,17 +639,9 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
void Controller::createRoom(const QString &name, const QString &topic) void Controller::createRoom(const QString &name, const QString &topic)
{ {
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList()); 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())); 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 bool Controller::isOnline() const

View File

@@ -119,7 +119,7 @@ private:
bool hasWindowSystem() const; bool hasWindowSystem() const;
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
void handleNotifications(QPointer<Quotient::Connection> connection); void handleNotifications();
#endif #endif
private Q_SLOTS: private Q_SLOTS:
@@ -151,7 +151,6 @@ Q_SIGNALS:
void keyVerificationAccept(const QString &commitment); void keyVerificationAccept(const QString &commitment);
void keyVerificationKey(const QString &sas); void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged(); void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
public Q_SLOTS: public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout); void logout(Quotient::Connection *conn, bool serverSideLogout);

View File

@@ -4,8 +4,8 @@
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QRegularExpression>
#include <memory> #include <memory>
#include <QRegularExpression>
struct CustomEmoji { struct CustomEmoji {
QString name; // with :semicolons: QString name; // with :semicolons:

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 // SPDX-License-Identifier: GPL-2.0-or-later
#include "devicesmodel.h" #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); auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] { connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() { if (job->error() != 0) {
beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
endRemoveRows();
};
if (job->error() != BaseJob::Success) {
QJsonObject replyData = job->jsonData(); QJsonObject replyData = job->jsonData();
QJsonObject authData; QJsonObject authData;
authData["session"] = replyData["session"]; 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()}}; QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier; authData["identifier"] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData); auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess); connect(innerJob, &BaseJob::success, this, [this, index]() {
} else { beginRemoveRows(QModelIndex(), index, index);
onSuccess(); m_devices.remove(index);
endRemoveRows();
});
} }
}); });
} }

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 // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once

View File

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

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 // SPDX-License-Identifier: GPL-2.0-or-later
#include "login.h" #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 // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once

View File

@@ -42,39 +42,39 @@
#include "blurhashimageprovider.h" #include "blurhashimageprovider.h"
#include "chatdocumenthandler.h" #include "chatdocumenthandler.h"
#include "clipboard.h" #include "clipboard.h"
#include "collapsestateproxymodel.h"
#include "controller.h" #include "controller.h"
#include "customemojimodel.h"
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h" #include "filetypesingleton.h"
#include "joinrulesevent.h"
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "keywordnotificationrulemodel.h"
#include "login.h" #include "login.h"
#include "matriximageprovider.h" #include "matriximageprovider.h"
#include "models/collapsestateproxymodel.h" #include "messageeventmodel.h"
#include "models/customemojimodel.h" #include "messagefiltermodel.h"
#include "models/devicesmodel.h"
#include "models/emojimodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/locationsmodel.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 "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "searchmodel.h"
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
#include "pollhandler.h" #include "pollhandler.h"
#endif #endif
#include "publicroomlistmodel.h"
#include "roomlistmodel.h"
#include "roommanager.h" #include "roommanager.h"
#include "serverlistmodel.h"
#include "sortfilterroomlistmodel.h"
#include "sortfilterspacelistmodel.h"
#include "spacehierarchycache.h" #include "spacehierarchycache.h"
#include "urlhelper.h" #include "urlhelper.h"
#include "userdirectorylistmodel.h"
#include "userfiltermodel.h"
#include "userlistmodel.h"
#include "webshortcutmodel.h"
#include "windowcontroller.h" #include "windowcontroller.h"
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
#include <keyverificationsession.h> #include <keyverificationsession.h>
@@ -82,19 +82,15 @@
#ifdef HAVE_COLORSCHEME #ifdef HAVE_COLORSCHEME
#include "colorschemer.h" #include "colorschemer.h"
#endif #endif
#include "models/completionmodel.h" #include "completionmodel.h"
#include "models/statemodel.h"
#include "neochatuser.h" #include "neochatuser.h"
#include "statemodel.h"
#ifdef HAVE_RUNNER #ifdef HAVE_RUNNER
#include "runner.h" #include "runner.h"
#include <QDBusConnection> #include <QDBusConnection>
#endif #endif
#ifdef Q_OS_WINDOWS
#include <Windows.h>
#endif
using namespace Quotient; using namespace Quotient;
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
@@ -157,7 +153,7 @@ int main(int argc, char *argv[])
KAboutLicense::GPL_V3, KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2022 KDE Community")); 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("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.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("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")); about.addCredit(i18n("Alexey Rusakov"), i18n("Maintainer of Quotient"), QStringLiteral("Kitsune-Ral@users.sf.net"));
@@ -236,7 +232,6 @@ int main(int argc, char *argv[])
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel"); qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel"); qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel"); qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
#ifdef QUOTIENT_07 #ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler"); qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif #endif

View File

@@ -27,6 +27,7 @@
#include <KLocalizedString> #include <KLocalizedString>
#include "neochatuser.h" #include "neochatuser.h"
#include "utils.h"
using namespace Quotient; using namespace Quotient;
@@ -53,9 +54,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[UserMarkerRole] = "userMarker"; roles[UserMarkerRole] = "userMarker";
roles[ShowAuthorRole] = "showAuthor"; roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection"; roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
roles[ReadMarkersStringRole] = "readMarkersString";
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction"; roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited"; roles[IsEditedRole] = "isEdited";
roles[SourceRole] = "source"; roles[SourceRole] = "source";
@@ -66,9 +64,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[VerifiedRole] = "verified"; roles[VerifiedRole] = "verified";
roles[DisplayNameForInitialsRole] = "displayNameForInitials"; roles[DisplayNameForInitialsRole] = "displayNameForInitials";
roles[AuthorDisplayNameRole] = "authorDisplayName"; roles[AuthorDisplayNameRole] = "authorDisplayName";
roles[IsNameChangeRole] = "isNameChange";
roles[IsAvatarChangeRole] = "isAvatarChange";
roles[IsRedactedRole] = "isRedacted"; roles[IsRedactedRole] = "isRedacted";
roles[GenericDisplayRole] = "genericDisplay";
roles[IsPendingRole] = "isPending";
return roles; return roles;
} }
@@ -177,7 +175,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}); });
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) { connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) {
Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole});
if (i == 0) { if (i == 0) {
return; // No need to move anything, just refresh return; // No need to move anything, just refresh
} }
@@ -216,12 +213,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
} }
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole}); 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::newFileTransfer, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
@@ -448,7 +439,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
case EventTypeRole: case EventTypeRole:
return DelegateType::ReadMarker; return DelegateType::ReadMarker;
case TimeRole: { 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; const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
} }
@@ -471,14 +462,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_currentRoom->eventToString(evt, Qt::RichText); 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 (role == FormattedBodyRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) { if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasTextContent() && e->mimeType().name() != "text/plain") { if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
@@ -510,8 +493,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return DelegateType::Audio; return DelegateType::Audio;
case MessageEventType::Video: case MessageEventType::Video:
return DelegateType::Video; return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default: default:
break; break;
} }
@@ -566,9 +547,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (auto e = eventCast<const RoomMessageEvent>(&evt)) { if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if(e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some // Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the // EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base // content JSON stored in EventContent::Base
@@ -582,7 +560,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
if (role == HighlightRole) { if (role == HighlightRole) {
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt); return m_currentRoom->isEventHighlighted(&evt);
} }
if (role == FileMimetypeIcon) { if (role == FileMimetypeIcon) {
@@ -610,25 +588,12 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == SpecialMarksRole) { if (role == SpecialMarksRole) {
if (isPending) { 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(); return pendingIt->deliveryStatus();
} }
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) { auto *memberEvent = timelineIt->viewAs<RoomMemberEvent>();
return EventStatus::Hidden; if (memberEvent) {
} if ((memberEvent->isJoin() || memberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return EventStatus::Hidden; return EventStatus::Hidden;
} }
} }
@@ -815,65 +780,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return false; return false;
} }
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) { if (role == ReactionRole) {
const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation()); const auto &annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation());
if (annotations.isEmpty()) { if (annotations.isEmpty()) {
@@ -974,12 +880,23 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
} }
if (role == IsRedactedRole) { if (role == IsNameChangeRole) {
return evt.isRedacted(); auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
if (roomMemberEvent) {
return roomMemberEvent->isRename();
}
return false;
} }
if (role == IsPendingRole) { if (role == IsAvatarChangeRole) {
return row < m_currentRoom->pendingEvents().size(); auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt);
if (roomMemberEvent) {
return roomMemberEvent->isAvatarUpdate();
}
return false;
}
if (role == IsRedactedRole) {
return evt.isRedacted();
} }
return {}; return {};
@@ -1049,9 +966,6 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
for (auto it = timelineBottom; it != limit; ++it) { for (auto it = timelineBottom; it != limit; ++it) {
auto evt = it->event(); auto evt = it->event();
auto e = eventCast<const RoomMessageEvent>(evt); auto e = eventCast<const RoomMessageEvent>(evt);
if (!e) {
continue;
}
auto content = (*it)->contentJson(); auto content = (*it)->contentJson();

View File

@@ -26,7 +26,6 @@ public:
Encrypted, Encrypted,
ReadMarker, ReadMarker,
Poll, Poll,
Location,
Other, Other,
}; };
Q_ENUM(DelegateType); Q_ENUM(DelegateType);
@@ -46,7 +45,6 @@ public:
AnnotationRole, AnnotationRole,
UserMarkerRole, UserMarkerRole,
FormattedBodyRole, FormattedBodyRole,
GenericDisplayRole,
MimeTypeRole, MimeTypeRole,
FileMimetypeIcon, FileMimetypeIcon,
@@ -58,9 +56,6 @@ public:
ShowAuthorRole, ShowAuthorRole,
ShowSectionRole, ShowSectionRole,
ReadMarkersRole, /**< QVariantList of users at the event for read marker tracking. */
ReadMarkersStringRole, /**< QString with the display name and mxID of the users at the event. */
ShowReadMarkersRole, /**< bool with whether there are any other user read markers to be shown. */
ReactionRole, ReactionRole,
IsEditedRole, IsEditedRole,
@@ -74,8 +69,9 @@ public:
DisplayNameForInitialsRole, DisplayNameForInitialsRole,
// The displayname for the event's sender; for name change events, the old displayname // The displayname for the event's sender; for name change events, the old displayname
AuthorDisplayNameRole, AuthorDisplayNameRole,
IsNameChangeRole,
IsAvatarChangeRole,
IsRedactedRole, IsRedactedRole,
IsPendingRole,
LastRole, // Keep this last LastRole, // Keep this last
}; };
Q_ENUM(EventRoles) Q_ENUM(EventRoles)

View File

@@ -11,20 +11,23 @@ using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent) MessageFilterModel::MessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
{ {
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] { connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
invalidateFilter(); beginResetModel();
endResetModel();
}); });
connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] { connect(NeoChatConfig::self(), &NeoChatConfig::ShowRenameChanged, this, [this] {
invalidateFilter(); beginResetModel();
endResetModel();
}); });
connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] { connect(NeoChatConfig::self(), &NeoChatConfig::ShowAvatarUpdateChanged, this, [this] {
invalidateFilter(); beginResetModel();
endResetModel();
}); });
connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] { connect(NeoChatConfig::self(), &NeoChatConfig::ShowDeletedMessagesChanged, this, [this] {
invalidateFilter(); beginResetModel();
endResetModel();
}); });
} }
@@ -32,11 +35,18 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
{ {
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); 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()) { if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
return false; return false;
} }
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) { if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false; return false;
} }
@@ -47,5 +57,9 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
return false; return false;
} }
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == MessageEventModel::State) {
return false;
}
return true; return true;
} }

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::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
}
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::EventTypeRole)
!= 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::EventTypeRole)
!= 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::EventTypeRole)
!= 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,40 +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 : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
StateEventsRole,
AuthorListRole,
};
[[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;
/**
* @brief QString aggregating 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,102 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "locationsmodel.h"
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &LocationsModel::roomChanged, this, [=]() {
for (const auto &event : m_room->messageEvents()) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
});
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
const auto latitude = parts[0].toFloat();
const auto longitude = parts[1].toFloat();
beginInsertRows(QModelIndex(), m_locations.size(), m_locations.size() + 1);
m_locations += LocationData{
.eventId = event->id(),
.latitude = latitude,
.longitude = longitude,
.text = event->contentJson()["body"].toString(),
.author = dynamic_cast<NeoChatUser *>(m_room->user(event->senderId())),
};
endInsertRows();
}
NeoChatRoom *LocationsModel::room() const
{
return m_room;
}
void LocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room) {
disconnect(this, nullptr, m_room, nullptr);
}
m_room = room;
Q_EMIT roomChanged();
}
QHash<int, QByteArray> LocationsModel::roleNames() const
{
return {
{LongitudeRole, "longitude"},
{LatitudeRole, "latitude"},
{TextRole, "text"},
};
}
QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
{
auto row = index.row();
if (roleName == LongitudeRole) {
return m_locations[row].longitude;
} else if (roleName == LatitudeRole) {
return m_locations[row].latitude;
} else if (roleName == TextRole) {
return m_locations[row].text;
}
return {};
}
int LocationsModel::rowCount(const QModelIndex &parent) const
{
return m_locations.size();
}

View File

@@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include "neochatroom.h"
#include <events/roommessageevent.h>
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
};
Q_ENUM(Roles)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
explicit LocationsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
Q_SIGNALS:
void roomChanged();
private:
QPointer<NeoChatRoom> m_room;
struct LocationData {
QString eventId;
float latitude;
float longitude;
QString text;
NeoChatUser *author;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);
};

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net> // SPDX-FileCopyrightText: Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org> // SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.1-or-later // SPDX-License-Identifier: LGPL-2.1-or-later
#include "neochataccountregistry.h" #include "neochataccountregistry.h"

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> // SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net>
// SPDX-FileCopyrightText: Tobias Fella <tobias.fella@kde.org> // SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.1-or-later // SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once #pragma once

View File

@@ -25,10 +25,18 @@
<label>Background transparency value</label> <label>Background transparency value</label>
<default>0.3</default> <default>0.3</default>
</entry> </entry>
<entry name="ShowNotifications" type="bool">
<label>Show notifications</label>
<default>true</default>
</entry>
<entry name="MergeRoomList" type="bool"> <entry name="MergeRoomList" type="bool">
<label>Merge Room Lists</label> <label>Merge Room Lists</label>
<default>false</default> <default>false</default>
</entry> </entry>
<entry name="ShowLeaveJoinEvent" type="bool">
<label>Show leave and join events in the timeline</label>
<default>true</default>
</entry>
<entry name="AllowQuickEdit" type="bool"> <entry name="AllowQuickEdit" type="bool">
<label>Use s/text/replacement syntax to edit your last message.</label> <label>Use s/text/replacement syntax to edit your last message.</label>
<default>false</default> <default>false</default>
@@ -37,6 +45,9 @@
<label>"Show your messages on the right</label> <label>"Show your messages on the right</label>
<default>true</default> <default>true</default>
</entry> </entry>
<entry name="RoomListPageWidth" type="int">
<default>-1</default>
</entry>
<entry name="RoomDrawerWidth" type="int"> <entry name="RoomDrawerWidth" type="int">
<default>-1</default> <default>-1</default>
</entry> </entry>
@@ -68,14 +79,6 @@
<label>Use a compact room list layout</label> <label>Use a compact room list layout</label>
<default>false</default> <default>false</default>
</entry> </entry>
<entry name="ShowStateEvent" type="bool">
<label>Show state events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowLeaveJoinEvent" type="bool">
<label>Show leave and join events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowRename" type="bool"> <entry name="ShowRename" type="bool">
<label>Show rename events in the timeline</label> <label>Show rename events in the timeline</label>
<default>true</default> <default>true</default>
@@ -109,10 +112,6 @@
<label>Show avatar in the room drawer</label> <label>Show avatar in the room drawer</label>
<default>true</default> <default>true</default>
</entry> </entry>
<entry name="Collapsed" type="bool">
<label>Save the collapsed state of the room list</label>
<default>false</default>
</entry>
</group> </group>
<group name="NetworkProxy"> <group name="NetworkProxy">
<entry name="ProxyType" type="Enum"> <entry name="ProxyType" type="Enum">

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