Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
1bf23c491a Don't erase existing reply-to relationships when editing messages
Signed-off-by: Joshua Goins <josh@redstrate.com>
2023-01-04 23:36:30 -05:00
797 changed files with 132694 additions and 410503 deletions

View File

@@ -1,2 +0,0 @@
[General]
disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp,i18n"

View File

@@ -1,6 +0,0 @@
; SPDX-FileCopyrightText: None
; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings]
kde/applications/neochat.packageAppx=True
libs/qt.qtMajorVersion=6

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.9",
"runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -12,60 +12,24 @@
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=fallback-x11",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--filesystem=xdg-download",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets",
"--talk-name=org.kde.kuiserver",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"cleanup": [
"/include",
"/lib/*.a",
"/lib/cmake",
"/lib/pkgconfig",
"/share/ndk-modules"
],
"modules": [
{
"name": "kirigamiaddons",
"config-opts": [
"-DBUILD_TESTING=OFF"
],
"config-opts": [ "-DBUILD_TESTING=OFF" ],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/libraries/kirigami-addons.git"
}
]
},
{
"name": "opencv",
"config-opts": [
"-DBUILD_TESTS=OFF",
"-DWITH_GTK=OFF",
"-DBUILD_LIST=core,imgproc"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/opencv/opencv"
}
],
"builddir": true
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
},
{
"name": "kquickimageeditor",
"config-opts": [
"-DBUILD_WITH_QT6=ON",
"-DBUILD_TESTING=OFF"
],
"buildsystem": "cmake-ninja",
"sources": [
{
@@ -77,19 +41,16 @@
{
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DOLM_TESTS=OFF"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.matrix.org/matrix-org/olm.git",
"tag": "3.2.16",
"tag": "3.2.10",
"x-checker-data": {
"type": "git",
"tag-pattern": "^([\\d.]+)$"
},
"commit": "7e0c8277032e40308987257b711b38af8d77cc69"
"commit": "9908862979147a71dc6abaecd521be526ae77be1"
}
]
},
@@ -101,13 +62,13 @@
"-Dvapi=false",
"-Dgtk_doc=false",
"-Dintrospection=false",
"-Dcrypto=disabled"
"-Dgcrypt=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsecret/0.21/libsecret-0.21.7.tar.xz",
"sha256": "6b452e4750590a2b5617adc40026f28d2f4903de15f1250e1d1c40bfd68ed55e",
"url": "https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz",
"sha256": "3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d",
"x-checker-data": {
"type": "gnome",
"name": "libsecret",
@@ -122,22 +83,20 @@
"sources": [
{
"type": "archive",
"url": "https://github.com/frankosterfeld/qtkeychain/archive/refs/tags/0.15.0.tar.gz",
"sha256": "f4254dc8f0933b06d90672d683eab08ef770acd8336e44dfa030ce041dc2ca22",
"url": "https://github.com/frankosterfeld/qtkeychain/archive/v0.13.2.tar.gz",
"sha256": "20beeb32de7c4eb0af9039b21e18370faf847ac8697ab3045906076afbc4caa5",
"x-checker-data": {
"type": "anitya",
"project-id": 4138,
"stable-only": true,
"url-template": "https://github.com/frankosterfeld/qtkeychain/archive/refs/tags/$version.tar.gz"
"url-template": "https://github.com/frankosterfeld/qtkeychain/archive/v$version.tar.gz"
}
}
],
"config-opts": [
"-DBUILD_WITH_QT6=ON",
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DLIB_INSTALL_DIR=/app/lib",
"-DBUILD_TRANSLATIONS=NO",
"-DBUILD_TESTING=OFF"
"-DBUILD_TRANSLATIONS=NO"
]
},
{
@@ -147,50 +106,43 @@
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "0.9.6.1",
"branch": "dev",
"disable-submodules": true
}
],
"config-opts": [
"-DBUILD_WITH_QT6=ON",
"-DQuotient_ENABLE_E2EE=ON",
"-DBUILD_TESTING=OFF"
"-DQuotient_ENABLE_E2EE=ON"
]
},
{
"name": "cmark",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMARK_TESTS=OFF",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=/app"
],
"sources": [
{
"type": "git",
"url": "https://github.com/commonmark/cmark.git"
}
],
"config-opts": [
"-DCMARK_TESTS=OFF",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=/app"
],
"builddir": true
},
{
"name": "kunifiedpush",
"name": "qcoro",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DENABLE_TESTING=OFF",
"-DKUNIFIEDPUSH_CLIENT_ONLY=ON"
],
"sources": [
{
"type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz",
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518",
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.7.0.tar.gz",
"sha256": "23ef0217926e67c8d2eb861cf91617da2f7d8d5a9ae6c62321b21448b1669210",
"x-checker-data": {
"type": "anitya",
"project-id": 8763,
"project-id": 236236,
"stable-only": true,
"url-template": "https://download.kde.org/stable/release-service/$version/src/kunifiedpush-$version.tar.xz"
"url-template": "https://github.com/danvratil/qcoro/archive/refs/tags/v$version.tar.gz"
}
}
]
@@ -198,15 +150,14 @@
{
"name": "neochat",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DBUILD_TESTING=OFF",
"-DNEOCHAT_FLATPAK=ON"
],
"sources": [
{
"type": "dir",
"path": "."
}
],
"config-opts": [
"-DNEOCHAT_FLATPAK=ON"
]
}
]

6
.gitignore vendored
View File

@@ -1,4 +1,4 @@
build*
build
.clang-format
.DS_Store
.kdev4/
@@ -9,7 +9,3 @@ compile_commands.json
kate.project.ctags.*
*.user
.flatpak-builder/
.idea/
cmake-build-*
src/res.generated.qrc
.qmlls.ini

View File

@@ -2,20 +2,11 @@
# SPDX-License-Identifier: CC0-1.0
include:
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/json-validation.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.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/linux.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/freebsd.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -1,45 +1,33 @@
# SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6'
'libraries/kunifiedpush': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
- 'on': ['@all']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
'libraries/kirigami-addons': '@latest'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'
Options:
per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]

43
.reuse/dep5 Normal file
View File

@@ -0,0 +1,43 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: NeoChat
Upstream-Contact: Carl Schwan <carlschwan@kde.org>
Files: 128-logo.png icons/* logo.png org.kde.neochat.svg org.kde.neochat.tray.svg android/res/drawable/neochat.png
Copyright: 2020 Carson Black <uhhadd@gmail.com>
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
Files: android/res/drawable/splash.xml
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: .gitignore
Copyright: None
License: CC0-1.0
Files: .gitlab/issue_templates/bug.md
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: CC0-1.0
Files: src/res.qrc src/res_android.qrc src/res_desktop.qrc
Copyright: None
License: CC0-1.0
Files: cmake/Flatpak/99-noto-mono-color-emoji.conf
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: BSD-2-Clause
Files: src/neochatconfig.kcfg
Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause

View File

@@ -1,30 +1,25 @@
# SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carl@carlschwan.eu>
# SPDX-FileCopyrightText: 2020-2021 Nicolas Fella <nicolas.fella@gmx.de>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "12")
set(RELEASE_SERVICE_VERSION_MICRO "3")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat)
set(PROJECT_VERSION "22.11")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF5_MIN_VERSION "5.91.0")
set(QT_MIN_VERSION "5.15.2")
set(KF_MIN_VERSION "6.17")
set(QT_MIN_VERSION "6.9")
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_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.17)
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -36,12 +31,6 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
include(ECMQmlModule)
include(ECMDeprecationSettings)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -50,39 +39,32 @@ if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg TextToSpeech WebView)
set_package_properties(Qt6 PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme IconThemes)
set_package_properties(KF6 PROPERTIES
find_package(KF5 ${KF5_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF5 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami PROPERTIES
set_package_properties(KF5Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 1.10.0 REQUIRED)
find_package(KF5KirigamiAddons 0.6 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
endif ()
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
if(ANDROID)
find_package(OpenSSL)
@@ -91,27 +73,20 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
find_package(ICU 61.0 COMPONENTS uc)
set_package_properties(ICU PROPERTIES
TYPE REQUIRED
PURPOSE "Unicode library"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.9.3)
set_package_properties(QuotientQt6 PROPERTIES
find_package(Quotient 0.6)
set_package_properties(Quotient PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
@@ -128,9 +103,6 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
ecm_find_qmlmodule(QtLocation)
ecm_find_qmlmodule(org.kde.prison)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -140,34 +112,22 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro6 0.4 COMPONENTS Core Network REQUIRED)
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF6DocTools ${KF_MIN_VERSION})
set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
find_package(KF5DocTools ${KF5_MIN_VERSION})
set_package_properties(KF5DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
option(WITH_UNIFIEDPUSH "Build with KUnifiedPush support" ON)
if (ANDROID OR APPLE OR WIN32 OR HAIKU)
set(WITH_UNIFIEDPUSH OFF)
endif()
if (WITH_UNIFIEDPUSH)
find_package(KUnifiedPush)
set_package_properties(KUnifiedPush PROPERTIES
TYPE REQUIRED
PURPOSE "Push notification support"
URL "https://invent.kde.org/libraries/kunifiedpush"
)
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
set(BUILD_TESTING FALSE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()
ki18n_install(po)
@@ -180,22 +140,17 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
if (BUILD_TESTING AND Quotient_VERSION_MINOR GREATER 6)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
# add_subdirectory(appiumtests)
if (NOT ANDROID)
add_subdirectory(memorytests)
endif()
endif()
if(KF6DocTools_FOUND)
if(KF5DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
if (NOT ANDROID)
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
@@ -208,9 +163,3 @@ file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
# Fixes the test by excluding this directory
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
ecm_qt_install_logging_categories(
EXPORT NEOCHAT
FILE neochat.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)

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

109
README.md
View File

@@ -1,94 +1,69 @@
<!--
SPDX-FileCopyrightText: 2020-2021 Carl Schwan <carlschwan@kde.org>
SPDX-FileCopyrightText: 2020-2024 Tobias Fella <tobias.fella@kde.org>
SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
SPDX-License-Identifier: CC0-1.0
-->
# NeoChat
A Qt/QML based Matrix client.
NeoChat is a client for Matrix, the decentralized communication protocol for instant
messaging. It is a fork of Spectral, using KDE frameworks, most notably Kirigami,
KConfig and KI18n.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://apps.kde.org/store_badges/snapstore/en.svg'/></a>
## Introduction
NeoChat is a client for [Matrix](https://matrix.org), the decentralized communication protocol for instant
messaging.
## Get it
NeoChat is based on KDE frameworks and as [libQuotient](https://github.com/quotient-im/libQuotient), a
Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
A stable release [is available](https://apps.kde.org/neochat) for download for Linux distributions.
Along with the stable release, a Flatpak version is available for the nightly
version:
```
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kdeapps.flatpakrepo
flatpak install kdeapps org.kde.neochat
```
A nightly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/NeoChat_Nightly_android-arm64/).
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
## Features
NeoChat aims to be a fully featured application for the Matrix specification. As such most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the fact that the Matrix spec is constantly
evolving, but the aim remains to provide eventual support for the entire spec.
* Sending messages
* Sending files from clipboard and filesystem
* Reply to message (right-click on a message to access menu)
* Start a private chat (but not encrypted)
* Show notifications, for the moment there is only a global switch
to disable it. We plan to implement the configuration part of the
specification soon.
* Autocompletion of usernames in chat
* Emoji picker
* Basic room setting page
* Send and accept invitations
* /rainbow <message> (very important)
* /me <message>
Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:
- Polls - MSC3381
- Sticker Packs - MSC2545
- Location Events - MSC3488
## Get it
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
Nightly builds for Linux and Windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
Nightly builds for Android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
Nightly Flatpaks are available from [KDE's nightly Flatpak repository](https://userbase.kde.org/Tutorials/Flatpak).
## Building NeoChat
The best way to build KDE apps during development is to use `kdesrc-build`. The full instructions for this can be found on
the KDE community website's get involved section under [development](https://community.kde.org/Get_Involved/development). This
is primarily aimed at Linux development.
For Windows and Android [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Running
Just start the executable in your preferred way - either from the build directory or from the installed location.
## Tests
Tests are in the repository under [autotests](autotests) and [appiumtests](appiumtests).
The project has CI setup to test new commits to the repository. All tests are expected to pass for a merge request to
be complete.
## Current build status
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg)
Currently the number of tests is limited, but growing. If anyone wants to help improve this, those
contributions would be especially welcome.
## Contributing
As is the case throughout the KDE ecosystem contributions are welcome from all. The code base is managed in the
[NeoChat repository](https://invent.kde.org/network/neochat) of the KDE Gitlab instance.
- [Code of Conduct](https://kde.org/code-of-conduct)
- [Report a Bug](https://bugs.kde.org/enter_bug.cgi?format=guided&product=neochat)
- [Feature Request](https://community.kde.org/Infrastructure/GitLab#Submitting_a_merge_request)
- [Create a Merge Request](https://community.kde.org/Infrastructure/GitLab#Submitting_a_merge_request)
- [Translation](https://community.kde.org/Get_Involved/translation)
NeoChat is still missing a few features to become a full-featured
Matrix client (most notably encryption support and video chat support).
We welcome contributions in this direction.
## Contact
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://go.kde.org/matrix/#/#neochat:kde.org). See [Matrix](https://community.kde.org/Matrix) for more details.
You can reach the maintainers at [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org), if you are already on Matrix.
Development happens in http://invent.kde.org/network/neochat (not in GitHub).
## Acknowledgement
NeoChat utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
This program utilizes [libQuotient](https://github.com/quotient-im/libQuotient/)
library and some C++ models from [Quaternion](https://github.com/quotient-im/Quaternion/).
NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).
This program is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).
## License

View File

@@ -1,96 +0,0 @@
# SPDX-FileCopyrightText: none
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "NeoChat"
SPDX-PackageSupplier = "Carl Schwan <carlschwan@kde.org>"
[[annotations]]
path = ["128-logo.png", "icons/**", "logo.png", "org.kde.neochat.svg", "org.kde.neochat.tray.svg", "android/res/drawable/neochat.png", "android/neochat-playstore.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Carson Black <uhhadd@gmail.com>"
SPDX-License-Identifier = "LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL"
[[annotations]]
path = "android/res/drawable/splash.xml"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".gitignore"
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ".gitlab/issue_templates/bug.md"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Carl Schwan <carlschwan@kde.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ["src/res.qrc", "src/res_android.qrc", "src/res_desktop.qrc"]
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "cmake/Flatpak/99-noto-mono-color-emoji.conf"
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Carl Schwan <carlschwan@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "src/app/neochatconfig.kcfg"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020-2021 Carl Schwan <carlschwan@kde.org>, Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "src/app/neochat.notifyrc"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ["src/qml/confetti.png", "src/qml/glowdot.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2021 Alexey Andreyev <aa13q@ya.ru>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ".flatpak-manifest.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020-2022 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "autotests/data/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "none"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "appiumtests/data/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = "src/purpose/purposeplugin.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "memorytests/memtest-sync.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".contextProperties.ini"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"

View File

@@ -9,13 +9,12 @@
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:installLocation="auto">
<application android:name="org.qtproject.qt.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"
android:exported="true">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -23,6 +22,7 @@
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="neochat-app"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
@@ -38,6 +38,8 @@
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Splash screen -->
@@ -55,6 +57,5 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- for Android >= 33 -->
</manifest>

View File

@@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:8.6.0'
classpath 'com.android.tools.build:gradle:3.6.4'
}
}
@@ -23,7 +23,8 @@ repositories {
apply plugin: 'com.android.application'
apply from: '../ecm-version.gradle'
apply from: '../version.gradle'
def timestamp = (int)(new Date().getTime()/1000)
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
@@ -34,7 +35,7 @@ android {
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qtAndroidDir - holds the path to qt android files
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
@@ -43,20 +44,16 @@ android {
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
@@ -76,11 +73,7 @@ android {
defaultConfig {
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
applicationId "org.kde.neochat"
namespace "org.kde.neochat"
versionCode ecmVersionCode
versionName ecmVersionName
manifestPlaceholders = [versionName: ecmVersionName, versionCode: ecmVersionCode]
manifestPlaceholders = [versionName: projectVersionFull, versionCode: timestamp]
}
packagingOptions {
@@ -91,7 +84,6 @@ android {
exclude 'lib/*/*_imageformats_qtga_*'
exclude 'lib/*/*_imageformats_qtiff_*'
exclude 'lib/*/*_qmltooling_*'
exclude 'lib/*/*_multimedia_ffmpeg*' // temporary qt6 android fix
}
aaptOptions {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: BSD-3-Clause
ext {
projectVersionFull = "@NEOCHAT_VERSION@"
}

View File

@@ -1,28 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
if(NOT BUILD_TESTING OR NOT CMAKE_SYSTEM_NAME MATCHES "Linux")
return()
endif()
find_package(SeleniumWebDriverATSPI)
set_package_properties(SeleniumWebDriverATSPI PROPERTIES
DESCRIPTION "Server component for selenium tests using Linux accessibility infrastructure"
PURPOSE "Needed for GUI tests"
URL "https://invent.kde.org/sdk/selenium-webdriver-at-spi"
TYPE OPTIONAL
)
if(NOT SeleniumWebDriverATSPI_FOUND)
return()
endif()
add_test(
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
)
add_test(
NAME openuserdetailstest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/openuserdetailstest.py
)

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
import os
import subprocess
import sys
import unittest
import time
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class CreateRoomTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors --test")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
def setUp(self):
pass
def tearDown(self):
if not self._outcome.result.wasSuccessful():
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_create_room(self):
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
self.driver.find_element(by=AppiumBy.NAME, value="Show Menu").click()
self.driver.find_element(by=AppiumBy.NAME, value="Create a Room").click()
self.driver.find_element(by=AppiumBy.NAME, value="Name:").send_keys("Super awesome room name")#
time.sleep(0.1) # without this, the second half of the text is sent to the topic field?!
self.driver.find_element(by=AppiumBy.NAME, value="Topic:").send_keys("There are not enough raccoons here")
time.sleep(0.1)
self.driver.find_element(by=AppiumBy.NAME, value="Create Room").click()
time.sleep(0.1)
self.driver.find_element(by=AppiumBy.NAME, value="Super awesome room name").click()
self.driver.find_element(by=AppiumBy.NAME, value="Show Room Information").click()
self.driver.find_element(by=AppiumBy.NAME, value="There are not enough raccoons here")
if __name__ == '__main__':
unittest.main()

View File

@@ -1,78 +0,0 @@
{
"next_batch": "batch1234",
"rooms": {
"join": {
"!newroom123321:localhost:1234": {
"state": {
"events": [
{
"type": "m.room.member",
"state_key": "@user:localhost:1234",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_0:localhost:1234",
"room_id": "!newroom123321:localhost:1234",
"content": {
"avatar_url": "",
"displayname": "A Display Name",
"membership": "join",
"reason": "Nothing"
},
"unsigned": {
"age": 1234
}
},
{
"type": "m.room.name",
"state_key": "",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_1:localhost:1234",
"room_id": "!newroom123321:localhost:1234",
"content": {
"name": "Super awesome room name"
},
"unsigned": {
"age": 1234
}
},
{
"type": "m.room.topic",
"state_key": "",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_2:localhost:1234",
"room_id": "!newroom123321:localhost:1234",
"content": {
"topic": "There are not enough raccoons here"
},
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"type": "m.room.message",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_1:localhost:1234",
"room_id": "!newroom123321:localhost:1234",
"content": {
"body": "This is a message",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
"msgtype": "m.text"
},
"unsigned": {
"age": 1234
}
}
]
}
}
}
}
}

View File

@@ -1,3 +0,0 @@
{
"next_batch": "batch1234"
}

View File

@@ -1,90 +0,0 @@
{
"next_batch": "batch1234",
"rooms": {
"join": {
"!room_id_1234:localhost:1234": {
"state": {
"events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{
"type": "m.room.member",
"state_key": "@user:localhost:1234",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_0:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"avatar_url": "",
"displayname": "A Display Name",
"membership": "join",
"reason": "Nothing"
},
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"content": {
"m.federate": true,
"predecessor": {
"event_id": "$something:example.org",
"room_id": "!oldroom:example.org"
},
"room_version": "11"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "",
"type": "m.room.create",
"unsigned": {
"age": 1234,
"membership": "join"
}
},
{
"type": "m.room.message",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_1:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"body": "This is a message",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
"msgtype": "m.text"
},
"unsigned": {
"age": 1234
}
}
]
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import json
from flask import Flask, request, abort
import os
app = Flask(__name__)
next_sync_payload = ""
@app.route("/_matrix/client/v3/login", methods=["GET"])
def login_get():
result = dict()
result["flows"] = [dict()]
result["flows"][0]["type"] = "m.login.password"
return result
@app.route("/_matrix/client/v3/account/whoami", methods=["GET"])
def whoami():
result = dict()
result["device_id"] = "device_id_1234"
result["user_id"] = "@user:localhost:1234"
return result
@app.route("/_matrix/client/v3/login", methods=["POST"])
def login_post():
data = request.get_json()
if data["identifier"]["user"] != "user" or data["password"] != "1234":
abort(403)
print(data)
result = dict()
result["access_token"] = "token_login"
result["device_id"] = "device_1234"
result["user_id"] = "@user:localhost:1234"
return result
def load_json(name):
parts = __file__.split("/")
parts.pop()
datadir = "/".join(parts)
return json.loads(open(f"{datadir}/data/{name}.json").read())
@app.route("/_matrix/client/r0/sync")
def sync():
global next_sync_payload
result = dict()
if len(next_sync_payload) > 0:
result = load_json(next_sync_payload)
next_sync_payload = ""
else:
result = load_json("sync_response_no_rooms") if ("login" in request.headers.get("Authorization")) else load_json("sync_response_rooms")
return result
@app.route("/.well-known/matrix/client")
def well_known():
reply = dict()
reply["m.homeserver"] = dict()
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
return reply
@app.route("/_matrix/client/v3/profile/<id>")
def profile(id):
reply = dict()
reply["avatar_url"] = "mxc://localhost:1234/asdf1234"
reply["displayname"] = "User123"
return reply
@app.route("/_matrix/client/v3/keys/upload", methods=["POST"])
def upload_keys():
reply = dict()
return reply
@app.route("/_matrix/client/v3/createRoom", methods=["POST"])
def create_room():
global next_sync_payload
data = request.get_json()
if data["name"] != "Super awesome room name" or data["topic"] != "There are not enough raccoons here":
return dict(), 400
response = dict()
response["room_id"] = "!newroom123321:localhost:1234"
next_sync_payload = "sync_response_new_room"
return response
@app.route("/_matrix/client/v3/publicRooms", methods=["POST"])
def public_rooms():
if request.get_json()["filter"]["generic_search_term"] == "forbidden":
data = dict()
data["errcode"] = "M_FORBIDDEN"
data["error"] = "You are not allowed to search for this. Go to https://wikipedia.org for more information"
return data, 403
return dict()
if __name__ == "__main__":
app.run(ssl_context='adhoc', port=1234)

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import os
import subprocess
import sys
import unittest
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class LoginTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
def setUp(self):
pass
def tearDown(self):
if not self._outcome.result.wasSuccessful():
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_login(self):
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Join some rooms to get started").click()
if __name__ == '__main__':
unittest.main()

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import os
import subprocess
import sys
import unittest
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class OpenUserDetailsTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors --test")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
def setUp(self):
pass
def tearDown(self):
if not self._outcome.result.wasSuccessful():
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_open_sheet(self):
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
try:
self.driver.find_element(by=AppiumBy.NAME, value="Expand Normal").click()
except:
pass
self.driver.find_element(by=AppiumBy.NAME, value="Empty room (!room_id_1234:localhost:1234)").click()
self.driver.find_element(by=AppiumBy.NAME, value="A Display Name").click()
self.driver.find_element(by=AppiumBy.NAME, value="Account Details")
if __name__ == '__main__':
unittest.main()

View File

@@ -3,104 +3,8 @@
enable_testing()
add_library(neochat_server STATIC server.cpp)
target_link_libraries(neochat_server PUBLIC Qt::HttpServer QuotientQt6)
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
LINK_LIBRARIES neochat Qt::Test Quotient
TEST_NAME neochatroomtest
)
ecm_add_test(
texthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME texthandlertest
)
ecm_add_test(
delegatesizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME mediasizehelpertest
)
ecm_add_test(
eventhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test(
timelinemessagemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME timelinemessagemodeltest
)
ecm_add_test(
windowcontrollertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME windowcontrollertest
)
ecm_add_test(
pollhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME pollhandlertest
)
ecm_add_test(
reactionmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME reactionmodeltest
)
ecm_add_test(
linkpreviewertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME linkpreviewertest
)
ecm_add_test(
messagecontentmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME messagecontentmodeltest
)
ecm_add_test(
actionstest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME actionstest
)
ecm_add_test(
servernoticestest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME servernoticestest
)
ecm_add_test(
roommanagertest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)

View File

@@ -1,151 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include <QSignalSpy>
#include <QVariantList>
#include "accountmanager.h"
#include "chatbarcache.h"
#include "models/actionsmodel.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
//TODO: rainbow, rainbowme, plain, spoiler, me, join, knock, j, part, leave, nick, roomnick, myroomnick, ignore, unignore, react, ban, unban, kick
class ActionsTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
NeoChatRoom *room = nullptr;
void expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message);
Server server;
private Q_SLOTS:
void initTestCase();
void testActions();
void testActions_data();
void testInvite();
};
void ActionsTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = accountManager->accounts()->front();
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
server.inviteUser(roomId, u"@invited:example.com"_s);
server.banUser(roomId, u"@banned:example.com"_s);
server.joinUser(roomId, u"@example:example.com"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void ActionsTest::testActions_data()
{
QTest::addColumn<QString>("command");
QTest::addColumn<std::optional<QString>>("resultText");
QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type");
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\\\_(ツ)\\_/¯ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("lenny") << u"/lenny Hello"_s << std::make_optional(u"( ͡° ͜ʖ ͡°) Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("unflip") << u"/unflip Hello"_s << std::make_optional(u"┬──┬ ( ゜-゜ノ) Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("rainbow") << u"/rainbow Hello"_s << std::optional<QString>() << std::optional<Quotient::RoomMessageEvent::MsgType>();
QTest::newRow("rainbowme") << u"/rainbowme Hello"_s << std::optional<QString>() << std::optional<Quotient::RoomMessageEvent::MsgType>();
QTest::newRow("plain") << u"/plain <b>Hello</b>"_s << std::optional<QString>() << std::optional<Quotient::RoomMessageEvent::MsgType>();
QTest::newRow("spoiler") << u"/spoiler Hello"_s << std::optional<QString>() << std::optional<Quotient::RoomMessageEvent::MsgType>();
QTest::newRow("me") << u"/me Hello"_s << std::make_optional(u"Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Emote);
QTest::newRow("notice") << u"/notice Hello"_s << std::make_optional(u"Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Notice);
QTest::newRow("message") << u"Hello"_s << std::make_optional(u"Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("invite") << u"/invite @foo:example.com"_s << std::optional<QString>() << std::optional<Quotient::RoomMessageEvent::MsgType>();
//TODO: join, knock, j, part, leave, nick, roomnick, myroomnick, ignore, unignore, react, ban, unban, kick
}
void ActionsTest::testActions()
{
QFETCH(QString, command);
QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache();
cache->setText(command);
auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result));
}
static ActionsModel::Action findAction(const QString &name)
{
for (const auto &action : ActionsModel::allActions()) {
if (action.prefix == name) {
return action;
}
}
return {};
}
void ActionsTest::expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message)
{
auto action = findAction(actionName);
QSignalSpy spy(room, &NeoChatRoom::showMessage);
auto result = action.handle(args, room, nullptr);
auto expected = QVariantList {type, message};
auto signal = spy.takeFirst();
QCOMPARE(signal, expected);
}
void ActionsTest::testInvite()
{
expectMessage(u"invite"_s, u"foo"_s, MessageType::Error, u"'foo' does not look like a matrix id."_s);
expectMessage(u"invite"_s, u"@invited:example.com"_s, MessageType::Information, u"@invited:example.com is already invited to this room."_s);
QCOMPARE(room->memberState(u"@invited:example.com"_s), Membership::Invite);
expectMessage(u"invite"_s, u"@banned:example.com"_s, MessageType::Information, u"@banned:example.com is banned from this room."_s);
QCOMPARE(room->memberState(u"@banned:example.com"_s), Membership::Ban);
expectMessage(u"invite"_s, connection->userId(), MessageType::Positive, u"You are already in this room."_s);
QCOMPARE(room->memberState(connection->userId()), Membership::Join);
expectMessage(u"invite"_s, u"@example:example.com"_s, MessageType::Information, u"@example:example.com is already in this room."_s);
QCOMPARE(room->memberState(u"@example:example.com"_s), Membership::Join);
QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Leave);
expectMessage(u"invite"_s, u"@user:example.com"_s, MessageType::Positive, u"@user:example.com was invited into this room."_s);
QSignalSpy spy(room, &NeoChatRoom::changed);
QVERIFY(spy.wait());
auto tries = 0;
while (room->memberState(u"@user:example.com"_s) != Membership::Invite) {
QVERIFY(spy.wait());
tries += 1;
if (tries > 3) {
QVERIFY(false);
}
}
QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite);
}
QTEST_MAIN(ActionsTest)
#include "actionstest.moc"

View File

@@ -1,176 +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 <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTest>
#include <QSignalSpy>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include "chatbarcache.h"
#include "neochatroom.h"
#include "testutils.h"
using namespace Quotient;
class ChatBarCacheTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void replyMissingUser();
void edit();
void attachment();
};
void ChatBarCacheTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
}
void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::noRoom()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(u"$153456789:example.org"_s);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(u"$153456789:example.org"_s);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
}
void ChatBarCacheTest::replyMissingUser()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room
room->syncNewEvents(u"test-min-sync-extra-sync.json"_s);
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
}
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(u"$153456789:example.org"_s));
});
chatBarCache->setEditId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), u"some/path"_s);
}
QTEST_MAIN(ChatBarCacheTest)
#include "chatbarcachetest.moc"

View File

@@ -1,36 +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 "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIUXbyWfTfcvVLrVB1qx36pW/7IkwMwDQYJKoZIhvcNAQEL
BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE
CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yNDEyMjQxNTAxMDNaGA8yNTcyMDcy
NDE1MDEwM1owQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc
MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKlxZ540TQ1uUDAR7ZJ9ue0PzcD2dPmblIIddyekvZS59V7X
drhamclXpHE2EelR87Sexst0BaHH/jmrHwxCtwbeXHZ8ueJHkGHJ5DLZCCiwfG+Q
gml7wlSXxXz37vie2tdlZh2yJSM8yvLAYceHb2zOskaGvul7ZITIS0JrPc3o6VZk
+MYGkYtA2JfUsv3jH4oQbxOf7RXqhWNAXbB+3hlwRBwMIdyoBNK6YS9QSrTeS9jj
UqgO5QmaQZOVvpaPf1Y/rHHLd2Qa6+a/cCJ1sr2biagb75AihpQFsK/oy6D1PP70
zTe7hPWn/efEpmtCV7CQ8ti4cRu0Kjy0T8grtCsCAwEAAaMhMB8wHQYDVR0OBBYE
FIFlylzwADNLfgTDNkhFeFelaEDxMA0GCSqGSIb3DQEBCwUAA4IBAQBQ2rw4GLIU
v+GY7Qru9LttkrQPd2bZXKxDMd/jT+wjmMVtqS4MAsCuDYwaYLjU1aWyqy0mN+lY
A17kD0VjBNBy45sYqkZveY0ks8mCScBemtrIDmjz2tiueecBIEASwEPBOZgv5/MV
cz864FiChF+2r8Zl8bhycGy9DEpRjzYKvIQWSDHQ3zpuh3iBnjfoieLHWX2kKCpk
ouS3V6485rHNCWsZT5IcCwfBFQkOuWRJpIazpz4AfwZh1TK9+bgiKA5EyZjSNrKw
xGQSpMSTRQMB0/FOCL/AixhN9unVFUViqUcdtSfoHE1VyBHv9kDT/cYms/Xl4B0t
/ZSQJ/D/Km1+
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpcWeeNE0NblAw
Ee2SfbntD83A9nT5m5SCHXcnpL2UufVe13a4WpnJV6RxNhHpUfO0nsbLdAWhx/45
qx8MQrcG3lx2fLniR5BhyeQy2QgosHxvkIJpe8JUl8V89+74ntrXZWYdsiUjPMry
wGHHh29szrJGhr7pe2SEyEtCaz3N6OlWZPjGBpGLQNiX1LL94x+KEG8Tn+0V6oVj
QF2wft4ZcEQcDCHcqATSumEvUEq03kvY41KoDuUJmkGTlb6Wj39WP6xxy3dkGuvm
v3AidbK9m4moG++QIoaUBbCv6Mug9Tz+9M03u4T1p/3nxKZrQlewkPLYuHEbtCo8
tE/IK7QrAgMBAAECggEAH9qmeKrra2F4KLlOGNKS//qPGz4Z+ozhi95/NpA1Zb7Z
3pUSCBFcROo5i2D3WA4kiymoRLpQjrv60puVcCggoWVvK4VCKsR6Y6/hOx/q9T9M
fWrE4ZC3FVEc+uPfZJT0nja9TkrdyXSV0LITD8Ap1eI7yJ9vR5R/bqj64QcpLMrU
QeoQIy1oTMR+qdjj33duyRwBZU3Yf8FRB2iW6OILZ8hzFo1jngec7dph9a1RK4e0
mEPdc9ywsKlDM7P0Y7zdmjar5XtQn87GiwNhz23f1fzCC2axLtOW0Xm4e4Qumehb
WrIi6Vfq8IWMglU7QrBJ7iR0Ls+XoKA5GxomV2IJZQKBgQDoIkOl5YGPQ3iGR+WK
e5/2Ml4G/uURzYiOlzSsyfoPXyO4EI2BJd5HkH+EvfgRx4xKkxUZRJdzR7llYPl8
BFYcFitvhO8SbD0mNAB5YW7f+3v1pgEN2umzoKd389Zx5WqTZ7YB1VG5RN/Q1JJL
2JM0Xgamq2vNtx3roRPxDBeW7QKBgQC63R/bmACJbgIzfaVBX4Zie3NQG0/Hf+gF
LnBwUmQDZOR7MY+kSiIUVMn3NuZRiCSCFBVwApruyK8r535JCibTVm5PWjvhFddY
LgaPOCKGlm9TLScjoH1pErYgG3uJ4nXeRfXhg4mco6EkrC7RzQywrd0VDoqpuc1Y
EKfEsYk8dwKBgE+mSh3nNOBKX1V73+f3aTiZqaeu2DyWkG+UtE9BclrJ40Cp9VPG
AZH+o7KRWEgJdzqzYv7riSfWCWgesRv7hOxYMwktzLY+i3DLUQpVAy05ZhwwnJX7
ckrfKfc/pGoqNLplUI8qecMfPciy14vMwR2r0Y5orTHFzi9mcqg35PQ1AoGAW2LX
OLq+0HdHhk0Va8I+450CSRQCUUvhed87SANTPEG0Z/dWC3/h6NWKrGdh/k+5oxAV
Z+EuSkdFPBCLt0bKtCKZ8h7sF+lplotz08kdQXsC2MfFU2wiySdIgK1QHp/tCxZl
6LM+sqdnoJrAjwRcB3AQJkMlV1ox7ba/hbdZqYMCgYBS6+JUXSSASpm5ZHd32a8m
xwryEZ7H6Hek6lvMHdxmwoKat5dCavxw64nrtyeeGZpg1W3zLLyamF9x/8kMyr6y
KKvtBfJ5sCvAbt80o9Pbs6R3yDB3AKiD3s3PQK7lol1nhE/8IbsF2r8JEQVcYd/k
oBzkl7MrMyLhhaCqSxwqQQ==
-----END PRIVATE KEY-----

View File

@@ -1,438 +0,0 @@
{
"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:kde.org"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:example.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:kde.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim2:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:kde.org",
"state_key": "@bob:kde.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
"membership": "join"
},
"event_id": "$143273582443PhrSh:example.org",
"origin_server_ts": 1432735824659,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"state_key": "@newline:example.org",
"type": "m.room.member",
"unsigned": {
"age": 12345
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:kde.org"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "Thread root",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$threadroot:example.org",
"origin_server_ts": 1690821582879,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "Thread message 1",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadroot:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage1:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "Thread message 2",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadmessage1:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage2:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "A message from someone who thought it was a good idea to put newlines in their display name.",
"msgtype": "m.text"
},
"event_id": "$153456889:example.org",
"origin_server_ts": 14327358246589,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1230
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,24 +0,0 @@
{
"timeline": {
"events": [
{
"content": {
"body": "https://kde.org",
"format": "org.matrix.custom.html",
"formatted_body": "https://kde.org",
"msgtype": "m.text"
},
"origin_server_ts": 1704648567967,
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 112
},
"event_id": "$validlink:example.org",
"room_id": "!test:example.org"
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,35 +0,0 @@
{
"timeline": {
"events": [
{
"content": {
"body": "* ",
"format": "org.matrix.custom.html",
"formatted_body": "no link",
"m.new_content": {
"body": "",
"format": "org.matrix.custom.html",
"formatted_body": "no link",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$validlink:example.org",
"rel_type": "m.replace"
},
"msgtype": "m.text",
"type": "m.room.message"
},
"origin_server_ts": 1704648614969,
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 65
},
"event_id": "$nolink:example.org",
"room_id": "!test:example.org"
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,105 +0,0 @@
{
"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": {
"displayname": "Example",
"membership": "join"
},
"event_id": "$exampleMember:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@example:example.org"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"displayname": "Example Changed",
"membership": "join"
},
"event_id": "$exampleMemberChnage:example.org",
"origin_server_ts": 2,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$exampleMember:example.org",
"prev_content": {
"displayname": "Example",
"membership": "join"
},
"prev_sender": "@example:example.org",
"age": 1234
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,20 +0,0 @@
{
"state": {
"events": [
{
"content": {
"membership": "leave"
},
"event_id": "$1432735824666PhrSA:example.org",
"origin_server_ts": 1432735824666,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$143273582443PhrSn:example.org"
}
}
]
}
}

View File

@@ -1,130 +0,0 @@
{
"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": {
"displayname": "Example",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273blorb3PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:kde.org",
"state_key": "@bob:kde.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Invited",
"membership": "invite"
},
"event_id": "$asdfpj443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@invited:example.com",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Banned",
"membership": "ban"
},
"event_id": "$asdfpj443PhrSnasfd:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@banned:example.com",
"state_key": "@banned:example.com",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 1,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,14 +0,0 @@
{
"content": {
"body": "www.example.org https://kde.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -1,22 +0,0 @@
{
"timeline": {
"events": [
{
"content": {
"body": "New plain message",
"msgtype": "m.text"
},
"event_id": "$pendingmerge:example.org",
"origin_server_ts": 123456,
"room_id":"#myroom:kde.org",
"sender":"@bob:kde.org",
"type":"m.room.message",
"unsigned": {
"transaction_id": "17017181543521"
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,38 +0,0 @@
{
"timeline": {
"events": [
{
"content": {
"org.matrix.msc1767.text": "test\n1. option1\n2. option 2",
"org.matrix.msc3381.poll.start": {
"answers": [
{
"id": "option1",
"org.matrix.msc1767.text": "option1text"
},
{
"id": "option2",
"org.matrix.msc1767.text": "option2text"
}
],
"kind": "org.matrix.msc3381.poll.undisclosed",
"max_selections": 1,
"question": {
"body": "test",
"msgtype": "m.text",
"org.matrix.msc1767.text": "test"
}
}
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "org.matrix.msc3381.poll.start",
"unsigned": {
"age": 1232
}
}
]
}
}

View File

@@ -1,44 +0,0 @@
{
"timeline": {
"events": [
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545183,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159121
},
"event_id": "$163456790:example.org",
"age": 390159121
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "😆",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545184,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159122
},
"event_id": "$163456791:example.org",
"age": 390159122
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,220 +0,0 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:matrix.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:example.org",
"state_key": "@bob:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
"membership": "join"
},
"event_id": "$143273582443PhrSh:example.org",
"origin_server_ts": 1432735824659,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"state_key": "@newline:example.org",
"type": "m.room.member",
"unsigned": {
"age": 12345
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,139 +0,0 @@
{
"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
}
},
{
"content": {
"displayname": "Example",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,165 +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 <QQuickItem>
#include <QTest>
#include "delegatesizehelper.h"
class DelegateSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void risingPercentage_data();
void risingPercentage();
void fallingPercentage_data();
void fallingPercentage();
void equalPercentage_data();
void equalPercentage();
void equalBreakpoint_data();
void equalBreakpoint();
};
void DelegateSizeHelperTest::risingPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(100) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(0) << qreal(0);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(100) << qreal(200);
QTest::newRow("three hundred") << qreal(300) << int(100) << qreal(300);
}
void DelegateSizeHelperTest::risingPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
auto item = QQuickItem();
item.setWidth(parentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setParentItem(&item);
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(0);
delegateSizeHelper.setEndPercentWidth(100);
QCOMPARE(delegateSizeHelper.availablePercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.availableWidth(), currentWidth);
}
void DelegateSizeHelperTest::fallingPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(100) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(100) << qreal(100);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(0) << qreal(0);
QTest::newRow("three hundred") << qreal(300) << int(0) << qreal(0);
}
void DelegateSizeHelperTest::fallingPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
auto item = QQuickItem();
item.setWidth(parentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setParentItem(&item);
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(100);
delegateSizeHelper.setEndPercentWidth(0);
QCOMPARE(delegateSizeHelper.availablePercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.availableWidth(), currentWidth);
}
void DelegateSizeHelperTest::equalPercentage_data()
{
QTest::addColumn<qreal>("parentWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("zero") << qreal(0) << int(50) << qreal(0);
QTest::newRow("one hundred") << qreal(100) << int(50) << qreal(50);
QTest::newRow("one fifty") << qreal(150) << int(50) << qreal(75);
QTest::newRow("two hundred") << qreal(200) << int(50) << qreal(100);
QTest::newRow("three hundred") << qreal(300) << int(50) << qreal(150);
}
void DelegateSizeHelperTest::equalPercentage()
{
QFETCH(qreal, parentWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
auto item = QQuickItem();
item.setWidth(parentWidth);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setParentItem(&item);
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(200);
delegateSizeHelper.setStartPercentWidth(50);
delegateSizeHelper.setEndPercentWidth(50);
QCOMPARE(delegateSizeHelper.availablePercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.availableWidth(), currentWidth);
}
void DelegateSizeHelperTest::equalBreakpoint_data()
{
QTest::addColumn<int>("startPercentageWidth");
QTest::addColumn<int>("endPercentageWidth");
QTest::addColumn<int>("currentPercentageWidth");
QTest::addColumn<qreal>("currentWidth");
QTest::newRow("start higher") << int(100) << int(0) << int(100) << qreal(1000);
QTest::newRow("equal") << int(50) << int(50) << int(50) << qreal(500);
QTest::newRow("end higher") << int(0) << int(100) << int(100) << qreal(1000);
}
/**
* We expect a default return except in the case where the two percentages are
* equal as that case can be calculated without dividing by zero.
*/
void DelegateSizeHelperTest::equalBreakpoint()
{
QFETCH(int, startPercentageWidth);
QFETCH(int, endPercentageWidth);
QFETCH(int, currentPercentageWidth);
QFETCH(qreal, currentWidth);
auto item = QQuickItem();
item.setWidth(1000);
DelegateSizeHelper delegateSizeHelper;
delegateSizeHelper.setParentItem(&item);
delegateSizeHelper.setStartBreakpoint(100);
delegateSizeHelper.setEndBreakpoint(100);
delegateSizeHelper.setStartPercentWidth(startPercentageWidth);
delegateSizeHelper.setEndPercentWidth(endPercentageWidth);
QCOMPARE(delegateSizeHelper.availablePercentageWidth(), currentPercentageWidth);
QCOMPARE(delegateSizeHelper.availableWidth(), currentWidth);
}
QTEST_GUILESS_MAIN(DelegateSizeHelperTest)
#include "delegatesizehelpertest.moc"

View File

@@ -1,338 +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 "eventhandler.h"
#include <KFormat>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include <qcbormap.h>
#include <qtestcase.h>
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
#include "utils.h"
#include "testutils.h"
using namespace Quotient;
class EventHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void authorDisplayName();
void nullAuthorDisplayName();
void singleLineSidplayName();
void nullSingleLineDisplayName();
void time();
void nullTime();
void timeString();
void highlighted();
void nullHighlighted();
void hidden();
void nullHidden();
void body();
void nullBody();
void genericBody_data();
void genericBody();
void nullGenericBody();
void markdownBody();
void markdownBodyReply();
void subtitle();
void nullSubtitle();
void mediaInfo();
void nullMediaInfo();
void replyAuthor();
void nullReplyAuthor();
void location();
void nullLocation();
};
void EventHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-eventhandler-sync.json"_s);
}
void EventHandlerTest::authorDisplayName()
{
QCOMPARE(EventHandler::authorDisplayName(room, room->messageEvents().at(1).get()), u"before"_s);
}
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with room set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with event set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(room, nullptr), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, room->messageEvents().at(11).get()), "Look at me I put newlines in my display name"_L1);
}
void EventHandlerTest::nullSingleLineDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with room set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with event set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, nullptr), QString());
}
void EventHandlerTest::time()
{
const auto event = room->messageEvents().at(0).get();
QCOMPARE(EventHandler::time(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::time(room, pendingIt->event(), true), pendingIt->lastUpdated());
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
}
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr.");
QCOMPARE(EventHandler::time(nullptr, nullptr), QDateTime());
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
QCOMPARE(EventHandler::time(room, nullptr), QDateTime());
}
void EventHandlerTest::timeString()
{
const auto event = room->messageEvents().at(0).get();
KFormat format;
QCOMPARE(EventHandler::timeString(room, event, false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s),
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::LocalTime)).toString(u"hh:mm"_s));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::ShortFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::ShortFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::LongFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::LongFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::LongFormat));
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
}
void EventHandlerTest::highlighted()
{
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(2).get()), true);
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with room set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(nullptr, nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with event set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(room, nullptr), false);
}
void EventHandlerTest::hidden()
{
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(3).get()), true);
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with room set to nullptr.");
QCOMPARE(EventHandler::isHidden(nullptr, nullptr), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with event set to nullptr.");
QCOMPARE(EventHandler::isHidden(room, nullptr), false);
}
void EventHandlerTest::body()
{
const auto event = room->messageEvents().at(0).get();
QCOMPARE(EventHandler::richBody(room, event), u"<b>This is an example<br>text message</b>"_s);
QCOMPARE(EventHandler::richBody(room, event, true), u"<b>This is an example text message</b>"_s);
QCOMPARE(EventHandler::plainBody(room, event), u"This is an example\ntext message"_s);
QCOMPARE(EventHandler::plainBody(room, event, true), u"This is an example text message"_s);
}
void EventHandlerTest::nullBody()
{
QTest::ignoreMessage(QtWarningMsg, "richBody called with room set to nullptr.");
QCOMPARE(EventHandler::richBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "richBody called with event set to nullptr.");
QCOMPARE(EventHandler::richBody(room, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with room set to nullptr.");
QCOMPARE(EventHandler::plainBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with event set to nullptr.");
QCOMPARE(EventHandler::plainBody(room, nullptr), QString());
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << u"<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message"_s;
QTest::newRow("member") << 1 << u"<a href=\"https://matrix.to/#/@example:example.org\">after</a> changed their display name and updated their avatar"_s;
QTest::newRow("message 2") << 2 << u"<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message"_s;
QTest::newRow("reaction") << 3 << u"Unknown event"_s;
QTest::newRow("video") << 4 << u"<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message"_s;
}
void EventHandlerTest::genericBody()
{
QFETCH(int, eventNum);
QFETCH(QString, output);
QCOMPARE(EventHandler::genericBody(room, room->messageEvents().at(eventNum).get()), output);
}
void EventHandlerTest::nullGenericBody()
{
QTest::ignoreMessage(QtWarningMsg, "genericBody called with room set to nullptr.");
QCOMPARE(EventHandler::genericBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "genericBody called with event set to nullptr.");
QCOMPARE(EventHandler::genericBody(room, nullptr), QString());
}
void EventHandlerTest::markdownBody()
{
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(0).get()), u"This is an example\ntext message"_s);
}
void EventHandlerTest::markdownBodyReply()
{
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(5).get()), u"reply"_s);
}
void EventHandlerTest::subtitle()
{
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(0).get()), u"after: This is an example text message"_s);
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(2).get()),
u"after: This is a highlight @bob:kde.org and this is a link https://kde.org"_s);
}
void EventHandlerTest::nullSubtitle()
{
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with room set to nullptr.");
QCOMPARE(EventHandler::subtitleText(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with event set to nullptr.");
QCOMPARE(EventHandler::subtitleText(room, nullptr), QString());
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
auto mediaInfo = EventHandler::mediaInfo(room, event);
auto thumbnailInfo = mediaInfo["tempInfo"_L1].toMap();
QCOMPARE(mediaInfo["source"_L1], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_L1)));
QCOMPARE(mediaInfo["mimeType"_L1], u"video/mp4"_s);
QCOMPARE(mediaInfo["mimeIcon"_L1], u"video-mp4"_s);
QCOMPARE(mediaInfo["size"_L1], 62650636);
QCOMPARE(mediaInfo["duration"_L1], 10);
QCOMPARE(mediaInfo["width"_L1], 1920);
QCOMPARE(mediaInfo["height"_L1], 1080);
QCOMPARE(thumbnailInfo["source"_L1], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_L1)));
QCOMPARE(thumbnailInfo["mimeType"_L1], u"image/jpeg"_s);
QCOMPARE(thumbnailInfo["mimeIcon"_L1], u"image-jpeg"_s);
QCOMPARE(thumbnailInfo["size"_L1], 382249);
QCOMPARE(thumbnailInfo["width"_L1], 800);
QCOMPARE(thumbnailInfo["height"_L1], 450);
}
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with room set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(nullptr, nullptr), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with event set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(room, nullptr), QVariantMap());
}
void EventHandlerTest::replyAuthor()
{
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->member(replyEvent->senderId());
auto eventHandlerReplyAuthor = EventHandler::replyAuthor(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id());
QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id());
QCOMPARE(eventHandlerReplyAuthor.displayName(), replyAuthor.displayName());
QCOMPARE(eventHandlerReplyAuthor.avatarUrl(), replyAuthor.avatarUrl());
QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId());
QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color());
QCOMPARE(EventHandler::replyAuthor(room, room->messageEvents().at(0).get()), RoomMember());
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with room set to nullptr.");
QCOMPARE(EventHandler::replyAuthor(nullptr, nullptr), RoomMember());
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with event set to nullptr. Returning empty user.");
QCOMPARE(EventHandler::replyAuthor(room, nullptr), RoomMember());
}
void EventHandlerTest::location()
{
QCOMPARE(EventHandler::latitude(room->messageEvents().at(7).get()), u"51.7035"_s.toFloat());
QCOMPARE(EventHandler::longitude(room->messageEvents().at(7).get()), u"-1.14394"_s.toFloat());
QCOMPARE(EventHandler::locationAssetType(room->messageEvents().at(7).get()), u"m.pin"_s);
}
void EventHandlerTest::nullLocation()
{
QTest::ignoreMessage(QtWarningMsg, "latitude called with event set to nullptr.");
QCOMPARE(EventHandler::latitude(nullptr), -100.0);
QTest::ignoreMessage(QtWarningMsg, "longitude called with event set to nullptr.");
QCOMPARE(EventHandler::longitude(nullptr), -200.0);
QTest::ignoreMessage(QtWarningMsg, "locationAssetType called with event set to nullptr.");
QCOMPARE(EventHandler::locationAssetType(nullptr), QString());
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -1,103 +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 "linkpreviewer.h"
#include "utils.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "testutils.h"
using namespace Quotient;
class LinkPreviewerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
void multipleLinkPreviewsMatch_data();
void multipleLinkPreviewsMatch();
void linkPreviewsReject_data();
void linkPreviewsReject();
};
void LinkPreviewerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:example.org"_s);
room = new TestUtils::TestRoom(connection, u"!test:example.org"_s);
}
void LinkPreviewerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("inputString");
QTest::addColumn<QUrl>("testOutputLink");
QTest::newRow("plainHttps") << u"https://kde.org"_s << QUrl(u"https://kde.org"_s);
QTest::newRow("richHttps") << u"<a href=\"https://kde.org\">Rich Link</a>"_s << QUrl(u"https://kde.org"_s);
QTest::newRow("richHttpsLinkDescription") << u"<a href=\"https://kde.org\">https://kde.org</a>"_s << QUrl(u"https://kde.org"_s);
}
void LinkPreviewerTest::linkPreviewsMatch()
{
QFETCH(QString, inputString);
QFETCH(QUrl, testOutputLink);
auto link = LinkPreviewer::linkPreviews(inputString)[0];
QCOMPARE(link, testOutputLink);
}
void LinkPreviewerTest::multipleLinkPreviewsMatch_data()
{
QTest::addColumn<QString>("inputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("multipleHttps") << u"www.example.org https://kde.org"_s << QList{QUrl(u"www.example.org"_s), QUrl(u"https://kde.org"_s)};
QTest::newRow("multipleHttps1Invalid") << u"www.example.org mxc://example.org/SEsfnsuifSDFSSEF"_s << QList{QUrl(u"www.example.org"_s)};
}
void LinkPreviewerTest::multipleLinkPreviewsMatch()
{
QFETCH(QString, inputString);
QFETCH(QList<QUrl>, testOutputLinks);
auto links = LinkPreviewer::linkPreviews(inputString);
QCOMPARE(links, testOutputLinks);
}
void LinkPreviewerTest::linkPreviewsReject_data()
{
QTest::addColumn<QString>("inputString");
QTest::newRow("mxc") << u"mxc://example.org/SEsfnsuifSDFSSEF"_s;
QTest::newRow("matrixTo") << u"https://matrix.to/#/@alice:example.org"_s;
QTest::newRow("noSpace") << u"testhttps://kde.org"_s;
}
void LinkPreviewerTest::linkPreviewsReject()
{
QFETCH(QString, inputString);
auto links = LinkPreviewer::linkPreviews(inputString);
QCOMPARE(links.empty(), true);
}
QTEST_MAIN(LinkPreviewerTest)
#include "linkpreviewertest.moc"

View File

@@ -1,74 +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 <qglobal.h>
#include <qtestcase.h>
#include <cmath>
#include "mediasizehelper.h"
#include "neochatconfig.h"
class MediaSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void uninitialized();
void limits_data();
void limits();
};
void MediaSizeHelperTest::uninitialized()
{
MediaSizeHelper mediasizehelper;
mediasizehelper.setMaxSize(540, 540);
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
}
void MediaSizeHelperTest::limits_data()
{
QTest::addColumn<qreal>("mediaWidth");
QTest::addColumn<qreal>("mediaHeight");
QTest::addColumn<qreal>("contentMaxWidth");
QTest::addColumn<qreal>("contentMaxHeight");
QTest::addColumn<QSize>("currentSize");
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
QTest::newRow("limit by content width tall media")
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
QTest::newRow("limit by content height wide media")
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
}
void MediaSizeHelperTest::limits()
{
QFETCH(qreal, mediaWidth);
QFETCH(qreal, mediaHeight);
QFETCH(qreal, contentMaxWidth);
QFETCH(qreal, contentMaxHeight);
QFETCH(QSize, currentSize);
MediaSizeHelper mediasizehelper;
mediasizehelper.setMaxSize(540, 540);
mediasizehelper.setMediaWidth(mediaWidth);
mediasizehelper.setMediaHeight(mediaHeight);
mediasizehelper.setContentMaxWidth(contentMaxWidth);
mediasizehelper.setContentMaxHeight(contentMaxHeight);
QCOMPARE(mediasizehelper.currentSize(), currentSize);
}
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
#include "mediasizehelpertest.moc"

View File

@@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include "models/eventmessagecontentmodel.h"
#include "neochatconnection.h"
#include "testutils.h"
using namespace Quotient;
using namespace Qt::Literals::StringLiterals;
class MessageContentModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
private Q_SLOTS:
void initTestCase();
void missingEvent();
};
void MessageContentModelTest::initTestCase()
{
connection = new NeoChatConnection;
}
void MessageContentModelTest::missingEvent()
{
auto room = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
auto model1 = EventMessageContentModel(room, u"$153456789:example.org"_s);
QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading…"_s);
auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true);
QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply…"_s);
room->syncNewEvents(u"test-min-sync.json"_s);
QCOMPARE(model1.rowCount(), 2);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Author);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::ComponentTypeRole), MessageComponentType::Text);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::DisplayRole), u"<b>This is an example<br>text message</b>"_s);
}
QTEST_MAIN(MessageContentModelTest)
#include "messagecontentmodeltest.moc"

View File

@@ -5,30 +5,138 @@
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "neochatroom.h"
#include "testutils.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 NeoChatRoomTest : public QObject {
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void subtitleTextTest();
void eventTest();
};
void NeoChatRoomTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
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));
}
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->subtitleText(), QStringLiteral("@example:example.org: This is an example text message"));
}
void NeoChatRoomTest::eventTest()
@@ -36,5 +144,5 @@ void NeoChatRoomTest::eventTest()
QCOMPARE(room->timelineSize(), 1);
}
QTEST_GUILESS_MAIN(NeoChatRoomTest)
QTEST_MAIN(NeoChatRoomTest)
#include "neochatroomtest.moc"

View File

@@ -1,73 +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 <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "events/pollevent.h"
#include "pollhandler.h"
#include "testutils.h"
using namespace Quotient;
class PollHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void nullObject();
void poll();
};
void PollHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-pollhandlerstart-sync.json"_s);
}
// Basically don't crash.
void PollHandlerTest::nullObject()
{
auto pollHandler = PollHandler();
QCOMPARE(pollHandler.hasEnded(), false);
QCOMPARE(pollHandler.numAnswers(), 0);
QCOMPARE(pollHandler.question(), QString());
QCOMPARE(pollHandler.kind(), PollKind::Disclosed);
}
void PollHandlerTest::poll()
{
auto startEvent = eventCast<const PollStartEvent>(room->messageEvents().at(0).get());
auto pollHandler = PollHandler(room, startEvent->id());
QList<Quotient::EventContent::Answer> options = {EventContent::Answer{"option1"_L1, "option1"_L1}, EventContent::Answer{"option2"_L1, "option2"_L1}};
const auto answer0 = pollHandler.answerAtRow(0);
const auto answer1 = pollHandler.answerAtRow(1);
QCOMPARE(pollHandler.hasEnded(), false);
QCOMPARE(pollHandler.numAnswers(), 2);
QCOMPARE(pollHandler.question(), u"test"_s);
QCOMPARE(answer0.id, "option1"_L1);
QCOMPARE(answer1.id, "option2"_L1);
QCOMPARE(answer0.text, "option1text"_L1);
QCOMPARE(answer1.text, "option2text"_L1);
QCOMPARE(pollHandler.answerCountAtId(answer0.id), 0);
QCOMPARE(pollHandler.answerCountAtId(answer1.id), 0);
QCOMPARE(pollHandler.checkMemberSelectedId(connection->userId(), answer0.id), false);
QCOMPARE(pollHandler.checkMemberSelectedId(connection->userId(), answer1.id), false);
QCOMPARE(pollHandler.kind(), PollKind::Undisclosed);
}
QTEST_GUILESS_MAIN(PollHandlerTest)
#include "pollhandlertest.moc"

View File

@@ -1,73 +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 <QSignalSpy>
#include <QTest>
#include "models/reactionmodel.h"
#include <Quotient/events/roommessageevent.h>
#include "models/eventmessagecontentmodel.h"
#include "testutils.h"
using namespace Quotient;
class ReactionModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
EventMessageContentModel *parentModel;
private Q_SLOTS:
void initTestCase();
void basicReaction();
void newReaction();
};
void ReactionModelTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s);
parentModel = new EventMessageContentModel(room, "123456"_L1);
}
void ReactionModelTest::basicReaction()
{
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto model = ReactionModel(parentModel, event->id(), room);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), u"<span style=\"font-family: 'emoji';\">👍</span>"_s);
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), u"👍"_s);
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole), u"Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"_s);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false);
}
void ReactionModelTest::newReaction()
{
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto model = new ReactionModel(parentModel, event->id(), room);
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole), u"Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"_s);
QSignalSpy spy(model, SIGNAL(modelReset()));
room->syncNewEvents(u"test-reactionmodel-extra-sync.json"_s);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), u"😆"_s);
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
u"Alice Margatroid and Bob reacted with <span style=\"font-family: 'emoji';\">👍</span>"_s);
delete model;
}
QTEST_MAIN(ReactionModelTest)
#include "reactionmodeltest.moc"

View File

@@ -1,141 +0,0 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <QVariantList>
#include "accountmanager.h"
#include "models/actionsmodel.h"
#include "roommanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
class RoomManagerTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
void testMaximizeMedia();
void testResolveMatrixLinks();
};
void RoomManagerTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
QVERIFY(connection);
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
RoomManager::instance().setConnection(connection);
QSignalSpy roomSpy(&RoomManager::instance(), &RoomManager::currentRoomChanged);
RoomManager::instance().resolveResource(room->id());
QVERIFY(roomSpy.size() > 0);
}
void RoomManagerTest::testMaximizeMedia()
{
QSignalSpy spy(&RoomManager::instance(), &RoomManager::showMaximizedMedia);
QSignalSpy syncSpy(connection, &Connection::syncDone);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for empty event id");
RoomManager::instance().maximizeMedia(QString());
QVERIFY(!spy.wait(10));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for unknown event id \"Doesn't exist\"");
RoomManager::instance().maximizeMedia(u"Doesn't exist"_s);
QVERIFY(!spy.wait(10));
const auto eventWithoutMedia = server.sendEvent(room->id(),
u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"format"_s, u"org.matrix.custom.html"_s},
{u"formatted_body"_s, u"Foo"_s},
{u"msgtype"_s, u"m.text"_s},
}));
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(eventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(eventWithoutMedia);
QVERIFY(!spy.wait(10));
// NOTE: This is supposed to test that maximizing pending media works correctly. This probably doesn't work in the UI yet, but at least the backend supports
// it. If the server ever learns how to process events, this becomes pointless and we need to find a way of preventing *these* events from arriving
auto pendingEventWithoutMedia = room->postText(u"Hello"_s);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(pendingEventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(pendingEventWithoutMedia);
QVERIFY(!spy.wait(10));
const auto eventWithMedia = server.sendEvent(room->id(),
u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
RoomManager::instance().maximizeMedia(eventWithMedia);
QVERIFY(spy.size() == 1);
QVERIFY(spy[0][0] == 0);
auto pendingEventWithMedia = room->postJson(u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
RoomManager::instance().maximizeMedia(pendingEventWithMedia);
QVERIFY(spy.size() == 2);
QVERIFY(spy[1][0] == 0);
}
void RoomManagerTest::testResolveMatrixLinks()
{
// Test if resolving a non-joined room will bring up the confirmation dialog.
const QSignalSpy askToJoinSpy(&RoomManager::instance(), &RoomManager::askJoinRoom);
RoomManager::instance().resolveResource(QStringLiteral("matrix:r/testbuild:matrix.org"), QStringLiteral("join"));
QTRY_COMPARE(askToJoinSpy.size(), 1);
}
QTEST_MAIN(RoomManagerTest)
#include "roommanagertest.moc"

View File

@@ -1,371 +0,0 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "server.h"
#include <QFile>
#include <QHttpServerResponder>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QSslCertificate>
#include <QSslKey>
#include <QUuid>
#include <Quotient/networkaccessmanager.h>
using namespace Qt::Literals::StringLiterals;
QString generateEventId()
{
return u"$"_s + QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64());
}
QString generateRoomId()
{
return u"!%1:localhost:1234"_s
.arg(QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64()))
.replace(u'/', QChar());
}
Server::Server()
{
}
void Server::start()
{
QObject::connect(Quotient::NetworkAccessManager::instance(),
&QNetworkAccessManager::sslErrors,
Quotient::NetworkAccessManager::instance(),
[](QNetworkReply *reply) {
reply->ignoreSslErrors();
});
m_server.route(u"/.well-known/matrix/client"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
responder.write(QJsonDocument(QJsonObject{
{u"m.homeserver"_s, QJsonObject{{u"base_url"_s, u"https://localhost:1234"_s}}},
}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/versions"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
responder.write(QJsonDocument(QJsonObject{
{u"versions"_s,
QJsonArray{
u"v1.0"_s,
u"v1.1"_s,
u"v1.2"_s,
u"v1.3"_s,
u"v1.4"_s,
u"v1.5"_s,
u"v1.6"_s,
u"v1.7"_s,
u"v1.8"_s,
u"v1.9"_s,
u"v1.10"_s,
u"v1.11"_s,
u"v1.12"_s,
u"v1.13"_s,
}},
}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/v3/capabilities"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
responder.write(
QJsonDocument(QJsonObject{{u"capabilities"_s,
QJsonObject{
{u"m.room_versions"_s, QJsonObject{{u"m.available"_s, QJsonObject{{u"1"_s, u"stable"_s}}}, {u"default"_s, u"1"_s}}},
}}}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/v3/account/whoami"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
responder.write(QJsonDocument(QJsonObject{
{u"device_id"_s, u"device_id_1234"_s},
{u"user_id"_s, u"@user:localhost:1234"_s},
}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Post, [](QHttpServerResponder &responder) {
// TODO
// if data["identifier"]["user"] != "user" or data["password"] != "1234":
// abort(403)
responder.write(QJsonDocument(QJsonObject{
{u"access_token"_s, u"token_login"_s},
{u"device_id"_s, u"device_1234"_s},
{u"user_id"_s, u"@user:localhost:1234"_s},
}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) {
responder.write(QJsonDocument(QJsonObject{
{u"flows"_s, QJsonArray{QJsonObject{{u"type"_s, u"m.login.password"_s}}}},
}),
QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
QHttpServerRequest::Method::Post,
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString(),
.roomId = roomId,
};
m_state += changes;
responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync);
QSslConfiguration config;
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly));
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config);
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
qFatal() << "Server failed to listen on a port.";
return;
} else {
qWarning() << "Server listening";
}
}
QString Server::createRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {},
};
m_state += changes;
return roomId;
}
void Server::inviteUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
}
void Server::banUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.bans += Changes::BanUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
}
void Server::joinUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.joins += Changes::JoinUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
}
QString Server::createServerNoticesRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {u"m.server_notice"_s},
};
m_state += changes;
return roomId;
}
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
changes.events += Changes::Event{
.fullJson = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId}},
};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
for (const auto &change : m_state.mid(token)) {
for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents;
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, newRoom.initialMembers[0]},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : newRoom.initialMembers) {
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
auto room = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents}}}};
QJsonArray roomAccountData;
QJsonObject tags;
for (const auto &tag : newRoom.tags) {
tags[tag] = QJsonObject();
}
if (!tags.empty()) {
roomAccountData += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
}
if (roomAccountData.size() > 0) {
room[u"account_data"] = QJsonObject{{u"events"_s, roomAccountData}};
}
joinRooms[newRoom.roomId] = room;
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &invitation : change.invitations) {
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, invitation.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, invitation.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(invitation.roomId)) {
auto room = joinRooms[invitation.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[invitation.roomId] = room;
} else {
joinRooms[invitation.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &ban : change.bans) {
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, ban.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, ban.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(ban.roomId)) {
auto room = joinRooms[ban.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[ban.roomId] = room;
} else {
joinRooms[ban.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &join : change.joins) {
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, join.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, join.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(join.roomId)) {
auto room = joinRooms[join.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[join.roomId] = room;
} else {
joinRooms[join.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
timeline += event.fullJson;
if (joinRooms.contains(event.fullJson[u"room_id"_s].toString())) {
auto room = joinRooms[event.fullJson[u"room_id"_s].toString()].toObject();
room[u"timeline"_s] = QJsonObject{{u"events"_s, timeline}};
joinRooms[event.fullJson[u"room_id"_s].toString()] = room;
} else {
joinRooms[event.fullJson[u"room_id"_s].toString()] = QJsonObject{
{u"timeline"_s, QJsonObject{{u"events"_s, timeline}}},
};
}
}
}
QJsonObject syncData = {
// {u"account_data"_s, QJsonObject {}},
// {u"presence"_s, QJsonObject {}},
{u"next_batch"_s, QString::number(m_state.size())},
};
QJsonObject rooms;
if (!joinRooms.isEmpty()) {
rooms[u"join"_s] = joinRooms;
}
if (!rooms.empty()) {
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
}

View File

@@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QHttpServer>
#include <QJsonObject>
#include <QSslServer>
struct Changes {
struct NewRoom {
QStringList initialMembers;
QString roomId;
QStringList tags;
};
QList<NewRoom> newRooms;
struct InviteUser {
QString userId;
QString roomId;
};
QList<InviteUser> invitations;
struct BanUser {
QString userId;
QString roomId;
};
QList<BanUser> bans;
struct JoinUser {
QString userId;
QString roomId;
};
QList<JoinUser> joins;
struct Event {
QJsonObject fullJson;
};
QList<Event> events;
};
struct RoomData {
QStringList members;
QString id;
QStringList tags;
};
class Server : public QObject
{
Q_OBJECT
public:
Server();
void start();
/**
* Create a room and place the user with id matrixId in it.
* Returns the room's id
*/
QString createRoom(const QString &matrixId);
void inviteUser(const QString &roomId, const QString &matrixId);
void banUser(const QString &roomId, const QString &matrixId);
void joinUser(const QString &roomId, const QString &matrixId);
/**
* Create a server notices room.
*/
QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
private:
QHttpServer m_server;
QSslServer m_sslServer;
void sync(const QHttpServerRequest &request, QHttpServerResponder &responder);
QList<Changes> m_state;
};

View File

@@ -1,87 +0,0 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <KLocalizedString>
#include <Quotient/connection.h>
#include <Quotient/eventstats.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "accountmanager.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
class ServerNoticesTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
void test();
};
void ServerNoticesTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
QVERIFY(connection);
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
RoomManager::instance().setConnection(connection);
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void ServerNoticesTest::test()
{
auto roomTreeModel = RoomManager::instance().roomTreeModel();
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 0);
auto sortFilterRoomTreeModel = RoomManager::instance().sortFilterRoomTreeModel();
const auto roomId = server.createServerNoticesRoom(u"@user:localhost:1234"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
const auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(connection->room(roomId)->isServerNoticeRoom());
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 1);
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"Foo"_s},
{u"format"_s, u"org.matrix.custom.html"_s},
{u"formatted_body"_s, u"Foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
sortFilterRoomTreeModel->invalidate();
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 0);
room->markAllMessagesAsRead();
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
}
QTEST_GUILESS_MAIN(ServerNoticesTest)
#include "servernoticestest.moc"

View File

@@ -1,56 +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 <QTest>
#include <Quotient/events/event.h>
#include <Quotient/syncdata.h>
#include "neochatroom.h"
namespace Quotient
{
class Connection;
}
namespace TestUtils
{
class TestRoom : public NeoChatRoom
{
public:
TestRoom(Quotient::Connection *connection, const QString &roomName, const QString &syncFileName = {})
: NeoChatRoom(connection, roomName, Quotient::JoinState::Join)
{
syncNewEvents(syncFileName);
}
void update(Quotient::SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
void syncNewEvents(const QString &syncFileName)
{
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
Q_UNUSED(testSyncFile.open(QIODevice::ReadOnly));
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
update(std::move(roomData));
}
}
};
template<Quotient::EventClass EventT>
inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
{
if (!eventFileName.isEmpty()) {
QFile testEventFile;
testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName);
Q_UNUSED(testEventFile.open(QIODevice::ReadOnly));
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson);
}
return nullptr;
}
}

View File

@@ -1,695 +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 <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include <Kirigami/Platform/PlatformTheme>
#include "enums/messagecomponenttype.h"
#include "models/customemojimodel.h"
#include "neochatconnection.h"
#include "testutils.h"
using namespace Quotient;
class TextHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void allowedAttributes();
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void addStyle_data();
void addStyle();
void dontAddStyle_data();
void dontAddStyle();
void sendSimpleStringCase();
void sendSingleParaMarkup();
void sendMultipleSectionMarkup();
void sendBadLinks();
void sendEscapeCode();
void sendCodeClass();
void sendCustomEmoji();
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void sendCustomTags_data();
void sendCustomTags();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
void receiveRichInPlainOut_data();
void receiveRichInPlainOut();
void receivePlainStripHtml();
void receivePlainStripMarkup();
void receiveStripNewlines();
void receiveRichUserPill();
void receiveRichStrikethrough();
void receiveRichtextIn();
void receiveRichMxcUrl();
void receiveRichPlainUrl_data();
void receiveRichPlainUrl();
void receiveRichEdited_data();
void receiveRichEdited();
void receiveLineSeparator();
void receiveRichCodeUrl();
void receiveRichColor();
void componentOutput_data();
void componentOutput();
void updateSpoiler_data();
void updateSpoiler();
};
void TextHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
connection->setAccountData(u"im.ponies.user_emotes"_s,
QJsonObject{{"images"_L1,
QJsonObject{{"test"_L1,
QJsonObject{{"body"_L1, "Test custom emoji"_L1},
{"url"_L1, "mxc://example.org/test"_L1},
{"usage"_L1, QJsonArray{"emoticon"_L1}}}}}}});
CustomEmojiModel::instance().setConnection(static_cast<NeoChatConnection *>(connection));
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-texthandler-sync.json"_s);
}
void TextHandlerTest::allowedAttributes()
{
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1S = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
const QString testOutputString1R = u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
// Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2S = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
const QString testOutputString2R =
u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a><a href='https://kde.org' style=\"text-decoration: none;\">link</a>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R);
}
void TextHandlerTest::stripDisallowedTags()
{
const QString testInputString = u"<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>"_s;
const QString testOutputString = u"<p>Allowed</p> <span>Allowed</span> Disallowed"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::stripDisallowedAttributes()
{
const QString testInputString = u"<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>"_s;
const QString testOutputString = u"Test"_s;
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 = u"<pre><code></code></pre>"_s;
const QString testOutputString = u"<pre><code></code></pre>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::addStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\"><tr><th style=\"border: 1px solid black; padding: 3px;\">Company</th><th style=\"border: 1px solid black; padding: 3px;\">Contact</th><th style=\"border: 1px solid black; padding: 3px;\">Country</th></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Alfreds Futterkiste</td><td style=\"border: 1px solid black; padding: 3px;\">Maria Anders</td><td style=\"border: 1px solid black; padding: 3px;\">Germany</td></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Centro comercial Moctezuma</td><td style=\"border: 1px solid black; padding: 3px;\">Francisco Chang</td><td style=\"border: 1px solid black; padding: 3px;\">Mexico</td></tr></table>"_s;
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
theme->alternateBackgroundColor().name());
}
void TextHandlerTest::addStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::dontAddStyle_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\">link</a>"_s;
QTest::newRow("table")
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s;
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
<< u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
}
void TextHandlerTest::dontAddStyle()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = u"This data should just be left alone."_s;
const QString testOutputString = u"This data should just be left alone."_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendSingleParaMarkup()
{
const QString testInputString =
u"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`."_s;
const QString testOutputString =
u"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>."_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendMultipleSectionMarkup()
{
const QString testInputString =
u"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```"_s;
const QString testOutputString =
u"<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>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendBadLinks()
{
const QString testInputString = u"[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)"_s;
const QString testOutputString = u"<a>link</a>, <img alt=\"image\">"_s;
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 = u"```\n<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```"_s;
const QString testOutputString =
u"<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>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCodeClass()
{
const QString testInputString = u"```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>"_s;
const QString testOutputString = u"<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCustomEmoji()
{
const QString testInputString = u":test:"_s;
const QString testOutputString =
u"<img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" />"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCustomEmojiCode_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("inline") << u"`:test:`"_s << u"<code>:test:</code>"_s;
QTest::newRow("block") << u"```\n:test:\n```"_s << u"<pre><code>:test:\n</code></pre>"_s;
}
// Custom emojis in code blocks should be left alone.
void TextHandlerTest::sendCustomEmojiCode()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::sendCustomTags_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
// spoiler
QTest::newRow("incomplete spoiler") << u"||test"_s << u"||test"_s;
QTest::newRow("complete spoiler") << u"||test||"_s << u"<span data-mx-spoiler>test</span>"_s;
QTest::newRow("multiple spoiler") << u"||apple||banana||pear||"_s << u"<span data-mx-spoiler>apple</span>banana<span data-mx-spoiler>pear</span>"_s;
QTest::newRow("inside code block spoiler") << u"```||apple||```"_s << u"<code>||apple||</code>"_s;
QTest::newRow("outside code block spoiler") << u"||apple|| ```||banana||``` ||pear||"_s
<< u"<span data-mx-spoiler>apple</span> <code>||banana||</code> <span data-mx-spoiler>pear</span>"_s;
QTest::newRow("complex spoiler") << u"Between `formFactor == Horizontal||Vertical` and `location == top||left||bottom||right`"_s
<< u"Between <code>formFactor == Horizontal||Vertical</code> and <code>location == top||left||bottom||right</code>"_s;
// strikethrough
QTest::newRow("incomplete strikethrough") << u"~~test"_s << u"~~test"_s;
QTest::newRow("complete strikethrough") << u"~~test~~"_s << u"<del>test</del>"_s;
QTest::newRow("inside code block strikethrough") << u"```~~apple~~```"_s << u"<code>~~apple~~</code>"_s;
QTest::newRow("outside code block strikethrough") << u"~~apple~~ ```~~banana~~``` ~~pear~~"_s
<< u"<del>apple</del> <code>~~banana~~</code> <del>pear</del>"_s;
}
void TextHandlerTest::sendCustomTags()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
const QString testInputString = u"Test...<br/>...ing"_s;
const QString testRichOutputString = u"Test...<br/>...ing"_s;
const QString testPlainOutputString = u"Test...\n...ing"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString =
u"<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."_s;
const QString testOutputString = u"Reply message."_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichInPlainOut_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("ampersand") << u"a &amp; b"_s << u"a & b"_s;
QTest::newRow("quote") << u"&quot;a and b&quot;"_s << u"\"a and b\""_s;
QTest::newRow("new line") << u"new<br>line"_s << u"new\nline"_s;
QTest::newRow("unescape") << u"can&#x27;t"_s << u"can't"_s;
}
void TextHandlerTest::receiveRichInPlainOut()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainTextIn()
{
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
const QString testOutputStringRich =
u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
// Make sure quotes are maintained in a plain string.
const QString testInputString2 = u"last line is \"Time to switch to a new topic.\""_s;
const QString testOutputString2 = u"last line is \"Time to switch to a new topic.\""_s;
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 = u"Test\nmany\nnew\nlines."_s;
const QString testInputStringRich = u"Test<br>many<br />new<br>lines."_s;
const QString testOutputString = u"Test many new lines."_s;
const QString testInputStringPlain2 = u"* List\n* Items"_s;
const QString testOutputString2 = u"List Items"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputStringPlain);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringRich);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);
testTextHandler.setData(testInputStringPlain2);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
}
/**
* For a plain text output of a received string all html is stripped except for
* code which is unescaped if it's html.
*/
void TextHandlerTest::receivePlainStripHtml()
{
const QString testInputString = u"<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>"_s;
const QString testOutputString = u"Test Some code <strong>with tags</strong>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}
void TextHandlerTest::receivePlainStripMarkup()
{
const QString testInputString = u"**bold** `<p>inline code</p>` *italic*"_s;
const QString testOutputString = u"bold <p>inline code</p> italic"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}
void TextHandlerTest::receiveRichUserPill()
{
const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s;
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\" style=\"text-decoration: none;\">@alice:example.org</a></b>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichStrikethrough()
{
const QString testInputString = u"<p><del>Test</del></p>"_s;
const QString testOutputString = u"<s>Test</s>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichtextIn()
{
const QString testInputString = u"<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>"_s;
const QString testOutputString = u"<p>Test</p> <pre><code>Some code &lt;strong&gt;with tags&lt;/strong&gt;</code></pre>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::receiveRichMxcUrl()
{
const QString testInputString =
u"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" alt=\"image\">"_s;
const QString testOutputString =
u"<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\">"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
}
void TextHandlerTest::receiveRichPlainUrl_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("output");
// 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.
QTest::addRow("link 1")
<< u"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>"_s
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">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\" style=\"text-decoration: none;\">Link already rich</a>"_s;
// Another real case. The linkification wasn't handling it when a single link
// contains what looks like and email. It was broken into 3 but needs to
// be just single link.
QTest::addRow("link 2")
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\" style=\"text-decoration: none;\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
QTest::addRow("email")
<< uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< uR"(<a href="mailto:email@example.com" style="text-decoration: none;">email@example.com</a> <a href="mailto:email@example.com" style="text-decoration: none;">Link already rich</a>)"_s;
QTest::addRow("mxid")
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">Link already rich</a></b>"_s;
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;
}
/**
* 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()
{
QFETCH(QString, input);
QFETCH(QString, output);
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), output);
}
void TextHandlerTest::receiveRichEdited_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("basic") << u"Edited"_s << u"Edited <span style=\"color:%1\">(edited)</span>"_s.arg(theme ? theme->disabledTextColor().name() : u"#000000"_s);
QTest::newRow("multiple paragraphs") << u"<p>Edited</p>\n<p>Edited</p>"_s
<< u"<p>Edited</p>\n<p>Edited <span style=\"color:%1\">(edited)</span></p>"_s.arg(
theme ? theme->disabledTextColor().name() : u"#000000"_s);
}
void TextHandlerTest::receiveRichEdited()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
const auto event = eventCast<const Quotient::RoomMessageEvent>(room->messageEvents().at(2).get());
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event, false, event->isReplaced()), testOutputString);
}
void TextHandlerTest::receiveLineSeparator()
{
auto text = u"foo\u2028bar"_s;
TextHandler textHandler;
textHandler.setData(text);
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), u"foo bar"_s);
}
void TextHandlerTest::receiveRichCodeUrl()
{
auto input = u"<code>https://kde.org</code>"_s;
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
}
void TextHandlerTest::receiveRichColor()
{
const QString testInputString =
u"<span data-mx-color=\"#ff00be\">¯</span><span data-mx-color=\"#ff3b1d\">\\</span><span data-mx-color=\"#ffa600\">_</span><span data-mx-color=\"#64d200\">(</span><span data-mx-color=\"#00e261\">ツ</span><span data-mx-color=\"#00e7ff\">)</span><span data-mx-color=\"#00e1ff\">_</span><span data-mx-color=\"#00bdff\">/</span><span data-mx-color=\"#ff60ff\">¯</span>"_s;
const QString testOutputString =
u"<span style=\"color: #ff00be;\">¯</span><span style=\"color: #ff3b1d;\">\\</span><span style=\"color: #ffa600;\">_</span><span style=\"color: #64d200;\">(</span><span style=\"color: #00e261;\">ツ</span><span style=\"color: #00e7ff;\">)</span><span style=\"color: #00e1ff;\">_</span><span style=\"color: #00bdff;\">/</span><span style=\"color: #ff60ff;\">¯</span>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::componentOutput_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<MessageComponent>>("testOutputComponents");
QTest::newRow("multiple paragraphs") << u"<p>Text</p>\n<p>Text</p>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Text, u"Text"_s, {}}};
QTest::newRow("code") << u"<p>Text</p>\n<pre><code class=\"language-html\">Some code\n</code></pre>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Code, u"Some code"_s, QVariantMap{{u"class"_s, u"html"_s}}}};
QTest::newRow("quote") << u"<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Quote, u"“blockquote”"_s, {}}};
QTest::newRow("multiple paragraph quote") << u"<blockquote>\n<p>blockquote</p>\n<p>next paragraph</p>\n</blockquote>"_s
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Quote, u"<p>“blockquote</p>\n<p>next paragraph”</p>"_s, {}}};
QTest::newRow("no tag first paragraph") << u"Text\n<p>Text</p>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Text, u"Text"_s, {}}};
QTest::newRow("no tag last paragraph") << u"<p>Text</p>\nText"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"Text"_s, {}},
MessageComponent{MessageComponentType::Text, u"Text"_s, {}}};
QTest::newRow("inline code") << u"<p><code>https://kde.org</code></p>\n<p>Text</p>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"<code>https://kde.org</code>"_s, {}},
MessageComponent{MessageComponentType::Text, u"Text"_s, {}}};
QTest::newRow("inline code single block") << u"<code>https://kde.org</code>"_s
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, u"<code>https://kde.org</code>"_s, {}}};
QTest::newRow("long start tag")
<< u"Ah, you mean something like<br/><pre data-md=\"```\"><code class=\"language-qml\"># main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you defined the id"_s
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, u"Ah, you mean something like<br/>"_s, {}},
MessageComponent{
MessageComponentType::Code,
u"# main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n someTextProperty: someField.text\n}"_s,
QVariantMap{{u"class"_s, u"qml"_s}}},
MessageComponent{MessageComponentType::Text, u"Sure you can, it's still local to the same file where you defined the id"_s, {}}};
}
void TextHandlerTest::componentOutput()
{
QFETCH(QString, testInputString);
QFETCH(QList<MessageComponent>, testOutputComponents);
TextHandler testTextHandler;
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents);
}
void TextHandlerTest::updateSpoiler_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::addColumn<bool>("spoilerRevealed");
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QTest::newRow("same length") << u"<span data-mx-spoiler style=\"color: #123456; background: #123456;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("different length") << u"<span data-mx-spoiler style=\"color: short; background: looooooooooong;\">Test<span>"_s
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
theme->alternateBackgroundColor().name())
<< false;
QTest::newRow("spoiler revealed")
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(theme->alternateBackgroundColor().name())
<< u"<span data-mx-spoiler style=\"color: %1; background: %2;\">Test<span>"_s.arg(theme->textColor().name(), theme->alternateBackgroundColor().name())
<< true;
}
void TextHandlerTest::updateSpoiler()
{
QFETCH(QString, testInputString);
QFETCH(QString, testOutputString);
QFETCH(bool, spoilerRevealed);
QCOMPARE(TextHandler::updateSpoilerText(this, testInputString, spoilerRevealed), testOutputString);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

View File

@@ -1,222 +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 <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "models/timelinemessagemodel.h"
#include "neochatroom.h"
#include "testutils.h"
using namespace Quotient;
class TimelineMessageModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TimelineMessageModel *model = nullptr;
private Q_SLOTS:
void initTestCase();
void init();
void switchEmptyRoom();
void switchSyncedRoom();
void simpleTimeline();
void syncNewEvents();
void pendingEvent();
void disconnect();
void idToRow();
void cleanup();
};
void TimelineMessageModelTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
}
void TimelineMessageModelTest::init()
{
QCOMPARE(model, nullptr);
model = new TimelineMessageModel;
}
// Make sure that basic empty rooms can be switched without crashing.
void TimelineMessageModelTest::switchEmptyRoom()
{
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s);
QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *)));
QCOMPARE(model->room(), nullptr);
model->setRoom(firstRoom);
QCOMPARE(spy.count(), 1);
QCOMPARE(model->room()->id(), u"#firstRoom:kde.org"_s);
model->setRoom(secondRoom);
QCOMPARE(spy.count(), 2);
QCOMPARE(model->room()->id(), u"#secondRoom:kde.org"_s);
model->setRoom(nullptr);
QCOMPARE(spy.count(), 3);
QCOMPARE(model->room(), nullptr);
}
// Make sure that rooms with some events can be switched without crashing
void TimelineMessageModelTest::switchSyncedRoom()
{
auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
QSignalSpy spy(model, SIGNAL(roomChanged(NeoChatRoom *, NeoChatRoom *)));
QCOMPARE(model->room(), nullptr);
model->setRoom(firstRoom);
QCOMPARE(spy.count(), 1);
QCOMPARE(model->room()->id(), u"#firstRoom:kde.org"_s);
model->setRoom(secondRoom);
QCOMPARE(spy.count(), 2);
QCOMPARE(model->room()->id(), u"#secondRoom:kde.org"_s);
model->setRoom(nullptr);
QCOMPARE(spy.count(), 3);
QCOMPARE(model->room(), nullptr);
}
void TimelineMessageModelTest::simpleTimeline()
{
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-messageventmodel-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(model->data(model->index(0), TimelineMessageModel::DelegateTypeRole), DelegateType::State);
QCOMPARE(model->data(model->index(0)), u"changed their display name to Example Changed"_s);
QCOMPARE(model->data(model->index(1)), u"<b>This is an example<br>text message</b>"_s);
QCOMPARE(model->data(model->index(1), TimelineMessageModel::DelegateTypeRole), DelegateType::Message);
QCOMPARE(model->data(model->index(1), TimelineMessageModel::EventIdRole), u"$153456789:example.org"_s);
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
QCOMPARE(model->data(model->index(-1)), QVariant());
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
QCOMPARE(model->data(model->index(model->rowCount())), QVariant());
}
// Sync some events into the TimelineMessageModel's current room and don't crash.
void TimelineMessageModelTest::syncNewEvents()
{
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s);
QSignalSpy spy(room, SIGNAL(aboutToAddNewMessages(Quotient::RoomEventsRange)));
model->setRoom(room);
QCOMPARE(model->rowCount(), 0);
room->syncNewEvents(u"test-messageventmodel-sync.json"_s);
QCOMPARE(model->rowCount(), 2);
QCOMPARE(spy.count(), 1);
}
// Check the adding of pending events to the room doesn't cause any issues in the model.
void TimelineMessageModelTest::pendingEvent()
{
QSignalSpy spyInsert(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)));
QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(const QModelIndex &, int, int)));
QSignalSpy spyChanged(model, SIGNAL(dataChanged(const QModelIndex, const QModelIndex, const QList<int> &)));
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s);
model->setRoom(room);
QCOMPARE(model->rowCount(), 0);
#if Quotient_VERSION_MINOR > 9
auto txnId = room->postText("New plain message"_L1);
#else
auto txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 1);
room->discardMessage(txnId);
QCOMPARE(model->rowCount(), 0);
QCOMPARE(spyRemove.count(), 1);
#if Quotient_VERSION_MINOR > 9
txnId = room->postText("New plain message"_L1);
#else
txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 2);
// We need to manually set the transaction ID of the new message as it will be
// different every time.
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s);
QVERIFY(testSyncFile.open(QIODevice::ReadOnly));
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
auto root = testSyncJson.object();
auto timeline = root["timeline"_L1].toObject();
auto events = timeline["events"_L1].toArray();
auto firstEvent = events[0].toObject();
firstEvent.insert("unsigned"_L1, QJsonObject{{"transaction_id"_L1, txnId}});
events[0] = firstEvent;
timeline.insert("events"_L1, events);
root.insert("timeline"_L1, timeline);
testSyncJson.setObject(root);
SyncRoomData roomData(u"@bob:kde.org"_s, JoinState::Join, testSyncJson.object());
room->update(std::move(roomData));
QCOMPARE(model->rowCount(), 1);
// The model will throw multiple data changed signals we need the one that refreshes
// the IsPendingRole.
QCOMPARE(spyChanged.count() > 0, true);
auto isPendingChanged = false;
for (auto signal : spyChanged) {
auto roles = signal.at(2).toList();
if (roles.contains(TimelineMessageModel::IsPendingRole)) {
isPendingChanged = true;
}
}
QCOMPARE(isPendingChanged, true);
}
// Make sure that the signals are disconnecting correctly when a room is switched.
void TimelineMessageModelTest::disconnect()
{
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s);
model->setRoom(room);
QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)));
model->setRoom(nullptr);
room->syncNewEvents(u"test-messageventmodel-sync.json"_s);
QCOMPARE(spy.count(), 0);
}
void TimelineMessageModelTest::idToRow()
{
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
}
void TimelineMessageModelTest::cleanup()
{
delete model;
model = nullptr;
QCOMPARE(model, nullptr);
}
QTEST_MAIN(TimelineMessageModelTest)
#include "timelinemessagemodeltest.moc"

View File

@@ -1,79 +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 <QQmlApplicationEngine>
#include <QTest>
#include <QWindow>
#include <KConfig>
#include <KSharedConfig>
#include <KWindowConfig>
#include "windowcontroller.h"
class WindowControllerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void nullWindow();
void showAndRaise();
void toggle();
void cleanup();
};
// Basically don't crash when no window is set.
void WindowControllerTest::nullWindow()
{
auto &instance = WindowController::instance();
QCOMPARE(instance.window(), nullptr);
instance.showAndRaiseWindow({});
instance.toggleWindow();
}
void WindowControllerTest::showAndRaise()
{
auto &instance = WindowController::instance();
QWindow window;
instance.setWindow(&window);
QCOMPARE(window.isVisible(), false);
instance.showAndRaiseWindow({});
QCOMPARE(window.isVisible(), true);
}
void WindowControllerTest::cleanup()
{
auto &instance = WindowController::instance();
instance.setWindow(nullptr);
QCOMPARE(instance.window(), nullptr);
}
void WindowControllerTest::toggle()
{
auto &instance = WindowController::instance();
QWindow window;
instance.setWindow(&window);
QCOMPARE(window.isVisible(), false);
instance.toggleWindow();
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.isVisible(), false);
// A window is classed as visible by qt when minimized but to the user this is not visible.
// So in this case we expect to show it even though visibility is technically true.
window.setVisibility(QWindow::Minimized);
QCOMPARE(window.windowState(), Qt::WindowMinimized);
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), false);
}
QTEST_MAIN(WindowControllerTest)
#include "windowcontrollertest.moc"

View File

@@ -2,4 +2,4 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,36 +0,0 @@
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline_memtest
main.cpp
)
target_link_libraries(timeline_memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline_memtest PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
Qt::Gui
Qt::QuickControls2
Qt::Widgets
KF6::I18nQml
QuotientQt6
LibNeoChat
Timeline
)
ecm_add_qml_module(timeline_memtest URI org.kde.neochat.timeline_memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline_memtest
QML_FILES
Main.qml
SOURCES
memtesttimelinemodel.cpp
memtesttimelinemodel.h
DEPENDENCIES
QtCore
QtQuick
IMPORTS
org.kde.neochat.timeline
)

View File

@@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
QQC2.ApplicationWindow {
id: root
title: "Timeline Memory Test"
minimumWidth: Kirigami.Units.gridUnit * 30
minimumHeight: Kirigami.Units.gridUnit * 30
visible: true
QQC2.ScrollView {
width: root.width
height: root.height
contentItem: ListView {
cacheBuffer: 1000000
model: messageFilterModel
delegate: EventDelegate {
room: memTestTimelineModel.room
}
}
}
}

View File

@@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <KLocalizedQmlContext>
#include <KLocalizedString>
#include "memtesttimelinemodel.h"
#include "models/messagefiltermodel.h"
using namespace Qt::StringLiterals;
int main(int argc, char **argv)
{
QApplication app(argc, argv);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
QQmlApplicationEngine engine;
KLocalization::setupLocalizedContext(&engine);
MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel;
MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
engine.loadFromModule("org.kde.neochat.timeline_memtest", "Main");
return app.exec();
}

View File

@@ -1,379 +0,0 @@
{
"ephemeral": {
"events": [
{
"content": {
"$1000000000000:example.org": {
"m.read": {
"@alice:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000000:example.org": {
"m.read": {
"@bob:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@tim:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@example:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@jeff:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@tina:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@sally:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@fred:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Example",
"membership": "join"
},
"event_id": "$143273582555PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "alice:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:example.org",
"state_key": "@bob:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Tim",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "tim:example.org",
"state_key": "@tim:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Jeff",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "jeff:example.org",
"state_key": "@jeff:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Tina",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "tina:example.org",
"state_key": "@tina:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Sally",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "sally:example.org",
"state_key": "@sally:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Fred",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "fred:example.org",
"state_key": "@fred:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": 0,
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": 1,
"origin_server_ts": 1000000000001,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": 1,
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000002,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": 2,
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": 0
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000003,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": 3,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": 4,
"origin_server_ts": 1000000000004,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": 5,
"origin_server_ts": 1000000000005,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": 6,
"origin_server_ts": 1000000000006,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": 7,
"origin_server_ts": 1000000000007,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "memtesttimelinemodel.h"
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/roommessageevent.h>
using namespace Quotient;
MemTestTimelineModel::MemTestTimelineModel(QObject *parent)
: MessageModel(parent)
{
beginResetModel();
m_connection = Connection::makeMockConnection(u"@bob:example.org"_s);
m_room = new MemTestRoom(m_connection, u"#memtestroom:example.org"_s, u"memtest-sync.json"_s);
for (const auto &eventIt : m_room->messageEvents()) {
Q_EMIT newEventAdded(eventIt.event());
}
endResetModel();
}
std::optional<std::reference_wrapper<const RoomEvent>> MemTestTimelineModel::getEventForIndex(QModelIndex index) const
{
return *m_room->messageEvents().at(index.row()).event();
}
int MemTestTimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_room->messageEvents().size();
}
#include "moc_memtesttimelinemodel.cpp"

View File

@@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include <Quotient/syncdata.h>
#include "models/messagemodel.h"
namespace Quotient
{
class Connection;
}
class NeoChatRoom;
class MemTestRoom : public NeoChatRoom
{
public:
MemTestRoom(Quotient::Connection *connection, const QString &roomName, const QString &syncFileName = {})
: NeoChatRoom(connection, roomName, Quotient::JoinState::Join)
{
syncNewEvents(syncFileName);
}
void update(Quotient::SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
void syncNewEvents(const QString &syncFileName)
{
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
auto ok = testSyncFile.open(QIODevice::ReadOnly);
if (!ok) {
qWarning() << "Failed to open" << testSyncFile.fileName() << testSyncFile.errorString();
}
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject();
timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100);
testSyncJson["timeline"_L1] = timelineJson;
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson);
update(std::move(roomData));
}
}
QJsonArray multiplyEvents(QJsonArray events, int factor)
{
QJsonArray newArray;
int eventNum = 0;
int ts = 0;
for (int i = 0; i < factor; ++i) {
for (const auto &event : events) {
auto eventObject = event.toObject();
auto contentJson = eventObject["content"_L1].toObject();
if (contentJson.contains("m.relates_to"_L1)) {
auto relatesToJson = contentJson["m.relates_to"_L1].toObject();
if (relatesToJson.contains("m.in_reply_to"_L1)) {
auto replyJson = relatesToJson["m.in_reply_to"_L1].toObject();
const auto currentId = eventObject["event_id"_L1].toInt();
const auto currentReplyId = replyJson["event_id"_L1].toInt();
replyJson["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum - (currentId - currentReplyId)));
relatesToJson["m.in_reply_to"_L1] = replyJson;
} else if (relatesToJson.contains("event_id"_L1)) {
const auto currentId = eventObject["event_id"_L1].toInt();
const auto currentRelationId = relatesToJson["event_id"_L1].toInt();
relatesToJson["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum - (currentId - currentRelationId)));
}
contentJson["m.relates_to"_L1] = relatesToJson;
eventObject["content"_L1] = contentJson;
}
eventObject["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum));
eventObject["origin_server_ts"_L1] = ts;
auto unsignedJson = eventObject["unsigned"_L1].toObject();
unsignedJson["age"_L1] = ts;
eventObject["unsigned"_L1] = unsignedJson;
newArray.append(eventObject);
++eventNum;
++ts;
}
}
return newArray;
}
};
/**
* @class MemTestTimelineModel
*
* This is a special version of the MessageModel design to load an unchanging set
* of events from a json file so that timeline memory optimisations can be measured.
*/
class MemTestTimelineModel : public MessageModel
{
Q_OBJECT
QML_ELEMENT
public:
explicit MemTestTimelineModel(QObject *parent = nullptr);
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
private:
QPointer<Quotient::Connection> m_connection;
std::vector<Quotient::RoomEventPtr> m_events;
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
[Desktop Entry]
Version=1.5
Name=NeoChat
Name[ar]=نيوتشات
Name[ast]=NeoChat
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
@@ -12,14 +11,10 @@ Name[cs]=NeoChat
Name[de]=NeoChat
Name[el]=NeoChat
Name[en_GB]=NeoChat
Name[eo]=NeoChat
Name[es]=NeoChat
Name[eu]=NeoChat
Name[fi]=NeoChat
Name[fr]=NeoChat
Name[gl]=NeoChat
Name[he]=NeoChat
Name[hi]=नियोचैट
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
@@ -28,7 +23,6 @@ Name[it]=NeoChat
Name[ka]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
Name[lv]=NeoChat
Name[nl]=NeoChat
Name[nn]=NeoChat
Name[pa]=ਨਿਓ-ਚੈਟ
@@ -37,15 +31,14 @@ Name[pt]=NeoChat
Name[pt_BR]=NeoChat
Name[ro]=NeoChat
Name[ru]=NeoChat
Name[sa]=नवचैट्
Name[sk]=NeoChat
Name[sl]=NeoChat
Name[sv]=NeoChat
Name[ta]=நியோச்சாட்
Name[tr]=NeoChat
Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat
GenericName=Matrix Client
GenericName[ar]=عميل ماتركس
GenericName[az]=Matrix Müştərisi
@@ -55,14 +48,10 @@ GenericName[cs]=Klient protokolu Matrix
GenericName[de]=Matrix-Programm
GenericName[el]=Εφαρμογή του Matrix
GenericName[en_GB]=Matrix Client
GenericName[eo]=Matrix-Kliento
GenericName[es]=Cliente para Matrix
GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix »
GenericName[gl]=Cliente de Matrix
GenericName[he]=לקוח Matrix
GenericName[hi]=मैट्रिक्स क्लाइंट
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
@@ -70,8 +59,7 @@ GenericName[ie]=Cliente de Matrix
GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programa
GenericName[lv]=„Matrix“ klients
GenericName[lt]=Matrix kliento programą
GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ
@@ -80,48 +68,50 @@ GenericName[pt]=Cliente de Matrix
GenericName[pt_BR]=Cliente Matrix
GenericName[ro]=Client Matrix
GenericName[ru]=Клиент Matrix
GenericName[sa]=मैट्रिक्स क्लाइंट
GenericName[sk]=Matrix Client
GenericName[sl]=Odjemalec Matrix
GenericName[sv]=Matrix-klient
GenericName[ta]=Matrix வாங்கி
GenericName[tr]=Matrix İstemcisi
GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix
Comment[ar]=دردش على ماتركس
Comment[ca]=Xat a Matrix
Comment[ca@valencia]=Xat a Matrix
Comment[de]=Über Matrix unterhalten
Comment[en_GB]=Chat on Matrix
Comment[eo]=Babilo en Matrix
Comment[es]=Chat en Matrix
Comment[eu]=Berriketa Matrix-en
Comment[fi]=Keskustele Matrixissä
Comment[fr]=Clavarder sur Matrix
Comment[gl]=Charle en Matrix
Comment[he]=התכתבות דרך Matrix
Comment[hi]=मैट्रिक्स पर चैट करें
Comment[hu]=Csevegés Matrixon
Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[ko]=Matrix에서 대화하기
Comment[lv]=Tērzējiet Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[pt_BR]=Bate papo na Matrix
Comment[ro]=Discutați pe Matrix
Comment[ru]=Общение в Matrix
Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु
Comment[sl]=Klepet na Matrixu
Comment[sv]=Chatta på Matrix
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix üzerinde sohbet edin
Comment[uk]=Спілкування у Matrix
Comment[zh_CN]=Matrix 上聊天
Comment[zh_TW]= Matrix 上聊天
Comment=Client for the Matrix protocol
Comment[ar]=عميل لميفاق ماتركس
Comment[az]=Matrix protokolu üçün müştəri
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
Comment[de]=Programm für das Matrix-Protokoll
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
Comment[en_GB]=Client for the Matrix protocol
Comment[es]=Cliente para el protocolo Matrix
Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix
Comment[ie]=Un cliente del protocol Matrix
Comment[it]=Client per il protocollo Matrix
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
Comment[nl]=Client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
Comment[pl]=Program obsługi protokołu Matriksa
Comment[pt]=Cliente para o protocolo Matrix
Comment[pt_BR]=Cliente para o protocolo Matrix
Comment[ro]=Client pentru protocolul Matrix
Comment[ru]=Клиент для протокола Matrix
Comment[sk]=Klient protokolu Matrix
Comment[sl]=Odjemalec za protokol Matrix
Comment[sv]=Klient för protokollet Matrix
Comment[ta]=Matrix நெறிமுறைக்கான வாங்கி
Comment[tr]=Matrix protokolü için istemci
Comment[uk]=Клієнт протоколу Matrix
Comment[x-test]=xxClient for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的客户端
MimeType=x-scheme-handler/matrix;
Exec=neochat %u
Terminal=false

View File

@@ -1,8 +1 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M3 3H19V14H8.68787L4 18.1019V14H3V3ZM4 13H5V15.8981L8.31213 13H18V4H4V13Z" fill="currentColor"/>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M17 15.2929L14.8536 13.1465L14.1465 13.8536L18 17.7071V13.5H17V15.2929Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 6H15V7H5V6Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 8H13V9H5V8Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 10H11V11H5V10Z" fill="currentColor"/>
</svg>
<svg width="22" height="22" fill="none" version="1.1" id="svg13" xmlns="http://www.w3.org/2000/svg"><style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 928 B

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

@@ -77,7 +77,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
></term>
<listitem>
<para
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:example.org o matrix:r/root:example.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:exemple.org o matrix:r/root:exemple.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
</listitem>
</varlistentry>
</variablelist>
@@ -97,7 +97,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>Vegeu també</title>
<simplelist>
<member
>Una llista de les preguntes més freqüents quant a Matrix <ulink url="https://matrix.org/faq/"
>Una llista de les preguntes més freqüents quan a Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member

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

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