Compare commits
1 Commits
master
...
work/featu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4139421e8e |
@@ -1,2 +0,0 @@
|
||||
[General]
|
||||
disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp,i18n"
|
||||
@@ -1,6 +0,0 @@
|
||||
; SPDX-FileCopyrightText: None
|
||||
; SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/applications/neochat.packageAppx=True
|
||||
libs/qt.qtMajorVersion=6
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "org.kde.neochat",
|
||||
"branch": "master",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.10",
|
||||
"runtime-version": "5.15-21.08",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "neochat",
|
||||
"tags": [
|
||||
@@ -12,47 +12,18 @@
|
||||
"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": "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
|
||||
},
|
||||
{
|
||||
"name": "kquickimageeditor",
|
||||
"config-opts": [
|
||||
"-DBUILD_WITH_QT6=ON",
|
||||
"-DBUILD_TESTING=OFF"
|
||||
],
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [
|
||||
{
|
||||
@@ -64,20 +35,16 @@
|
||||
{
|
||||
"name": "olm",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"config-opts": [
|
||||
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
|
||||
"-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"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -89,13 +56,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",
|
||||
@@ -110,22 +77,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -135,50 +100,42 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/quotient-im/libQuotient.git",
|
||||
"branch": "dev",
|
||||
"disable-submodules": true
|
||||
"branch": "dev"
|
||||
}
|
||||
],
|
||||
"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.3/src/kunifiedpush-25.08.3.tar.xz",
|
||||
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a",
|
||||
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.4.0.tar.gz",
|
||||
"sha256": "0e68b3f0ce7bf521ffbdd731464d2d60d8d7a39a749b551ed26855a1707d86d1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -186,15 +143,14 @@
|
||||
{
|
||||
"name": "neochat",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"config-opts": [
|
||||
"-DBUILD_TESTING=OFF",
|
||||
"-DNEOCHAT_FLATPAK=ON"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-DNEOCHAT_FLATPAK=ON"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
8
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
build*
|
||||
build
|
||||
.clang-format
|
||||
.DS_Store
|
||||
.kdev4/
|
||||
@@ -7,9 +7,3 @@ compile_commands.json
|
||||
.cache/
|
||||
.vscode/
|
||||
kate.project.ctags.*
|
||||
*.user
|
||||
.flatpak-builder/
|
||||
.idea/
|
||||
cmake-build-*
|
||||
src/res.generated.qrc
|
||||
.qmlls.ini
|
||||
|
||||
@@ -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
|
||||
|
||||
63
.kde-ci.yml
@@ -1,45 +1,28 @@
|
||||
# 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'
|
||||
- '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'
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@stable'
|
||||
'frameworks/kio': '@stable'
|
||||
'frameworks/kwindowsystem': '@stable'
|
||||
'frameworks/sonnet': '@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
|
||||
enable-lsan: True
|
||||
require-passing-tests-on: [ 'Linux', 'Windows' ]
|
||||
|
||||
47
.reuse/dep5
Normal file
@@ -0,0 +1,47 @@
|
||||
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: qtquickcontrols2.conf
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: android/res/drawable/splash.xml
|
||||
Copyright: 2020 Tobias Fella <fella@posteo.de>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: */qmldir .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: res.qrc res_android.qrc 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: imports/NeoChat/Component/confetti.png imports/NeoChat/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
|
||||
132
CMakeLists.txt
@@ -1,84 +1,68 @@
|
||||
# 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 "26")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||
project(NeoChat)
|
||||
set(PROJECT_VERSION "22.09")
|
||||
|
||||
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)
|
||||
include(KDEInstallDirs)
|
||||
include(ECMFindQmlModule)
|
||||
include(KDECMakeSettings)
|
||||
include(ECMAddTests)
|
||||
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()
|
||||
|
||||
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
|
||||
|
||||
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
|
||||
if(NEOCHAT_FLATPAK)
|
||||
include(cmake/Flatpak.cmake)
|
||||
endif()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -87,27 +71,21 @@ 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)
|
||||
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.sonnet 1.0)
|
||||
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.5)
|
||||
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/"
|
||||
@@ -124,9 +102,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
|
||||
@@ -136,34 +111,19 @@ 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} COMPONENTS Core QUIET)
|
||||
if(NOT QCoro${QT_MAJOR_VERSION}_FOUND)
|
||||
find_package(QCoro REQUIRED)
|
||||
endif()
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
find_package(KF6DocTools ${KF_MIN_VERSION})
|
||||
set_package_properties(KF6DocTools 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)
|
||||
@@ -177,21 +137,7 @@ add_definitions(-DQT_NO_FOREACH)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer QuickTest)
|
||||
add_subdirectory(autotests)
|
||||
# add_subdirectory(appiumtests)
|
||||
if (NOT ANDROID)
|
||||
add_subdirectory(memorytests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(KF6DocTools_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)
|
||||
@@ -204,9 +150,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}
|
||||
)
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
Creative Commons Attribution-ShareAlike 4.0 International
|
||||
|
||||
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
|
||||
|
||||
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described.
|
||||
|
||||
Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
Section 1 – Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||
|
||||
Section 2 – Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
A. reproduce and Share the Licensed Material, in whole or in part; and
|
||||
|
||||
B. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section 6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
|
||||
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
|
||||
|
||||
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
|
||||
|
||||
Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified form), You must:
|
||||
|
||||
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||
|
||||
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||
|
||||
Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
|
||||
|
||||
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
|
||||
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
|
||||
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
0
LICENSES/MIT.txt
Normal file → Executable file
104
README.md
@@ -1,89 +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).
|
||||
|
||||

|
||||
|
||||
## 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 Matrix spec 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>
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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 uses [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
|
||||
|
||||
|
||||
96
REUSE.toml
@@ -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"
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB |
6
android/version.gradle.in
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
ext {
|
||||
projectVersionFull = "@NEOCHAT_VERSION@"
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"next_batch": "batch1234"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,148 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
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 Qt::HttpServer neochat_server
|
||||
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 Qt::HttpServer neochat_server
|
||||
TEST_NAME chatbarcachetest
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
modeltest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
|
||||
TEST_NAME modeltest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
blockcachetest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME blockcachetest
|
||||
)
|
||||
|
||||
macro(add_qml_tests)
|
||||
if (WIN32)
|
||||
set(_extra_args -platform offscreen)
|
||||
endif()
|
||||
|
||||
foreach(test ${ARGV})
|
||||
add_test(NAME ${test}
|
||||
COMMAND qmltest
|
||||
${_extra_args}
|
||||
-input ${test}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
add_executable(qmltest qmltest.cpp
|
||||
chatkeyhelpertesthelper.h
|
||||
chatmarkdownhelpertestwrapper.h
|
||||
chattextitemhelpertesthelper.h
|
||||
)
|
||||
qt_add_qml_module(qmltest URI NeoChatTestUtils)
|
||||
|
||||
target_link_libraries(qmltest
|
||||
PRIVATE
|
||||
Qt6::Qml
|
||||
Qt6::QuickTest
|
||||
LibNeoChat
|
||||
LibNeoChatplugin
|
||||
)
|
||||
|
||||
add_qml_tests(
|
||||
chattextitemhelpertest.qml
|
||||
chatmarkdownhelpertest.qml
|
||||
chatkeyhelpertest.qml
|
||||
)
|
||||
@@ -1,153 +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 "blockcache.h"
|
||||
#include "chatbarcache.h"
|
||||
#include "enums/messagecomponenttype.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(room);
|
||||
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(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"
|
||||
@@ -1,52 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 "blockcache.h"
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
|
||||
using namespace Block;
|
||||
|
||||
class BlockCacheTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void toStringTest_data();
|
||||
void toStringTest();
|
||||
};
|
||||
|
||||
void BlockCacheTest::toStringTest_data()
|
||||
{
|
||||
QTest::addColumn<QString>("inputString");
|
||||
QTest::addColumn<MessageComponentType::Type>("itemType");
|
||||
QTest::addColumn<QString>("outputstring");
|
||||
|
||||
QTest::newRow("plainText") << u"test string"_s << MessageComponentType::Text << u"test string"_s;
|
||||
QTest::newRow("list") << u"- list 1\n- list 2\n- list 3\n"_s << MessageComponentType::Text << u"- list 1\n- list 2\n- list 3"_s;
|
||||
QTest::newRow("code") << u"for (some code) {\n\n do something\n\n}"_s << MessageComponentType::Code
|
||||
<< u"```\nfor (some code) {\n do something\n}\n```"_s;
|
||||
QTest::newRow("quote") << u"\"this is a quote\""_s << MessageComponentType::Quote << u"> this is a quote"_s;
|
||||
QTest::newRow("heading") << u"# heading\n\nnext line"_s << MessageComponentType::Text << u"# heading\n\nnext line"_s;
|
||||
}
|
||||
|
||||
void BlockCacheTest::toStringTest()
|
||||
{
|
||||
QFETCH(QString, inputString);
|
||||
QFETCH(MessageComponentType::Type, itemType);
|
||||
QFETCH(QString, outputstring);
|
||||
|
||||
Cache cache;
|
||||
cache += CacheItem{
|
||||
.type = itemType,
|
||||
.content = QTextDocumentFragment::fromMarkdown(inputString),
|
||||
};
|
||||
|
||||
QCOMPARE(cache.toString(), outputstring);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BlockCacheTest)
|
||||
#include "blockcachetest.moc"
|
||||
@@ -1,177 +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 <KLocalizedString>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "blockcache.h"
|
||||
#include "chatbarcache.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include "server.h"
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class ChatBarCacheTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
Server server;
|
||||
QString eventId;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void empty();
|
||||
void reply();
|
||||
void replyMissingUser();
|
||||
void edit();
|
||||
void attachment();
|
||||
};
|
||||
|
||||
void ChatBarCacheTest::initTestCase()
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
server.start();
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
auto accountManager = new AccountManager(true, this);
|
||||
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
|
||||
|
||||
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||
eventId = server.sendEvent(roomId,
|
||||
u"m.room.message"_s,
|
||||
QJsonObject{
|
||||
{u"body"_s, u"foo"_s},
|
||||
{u"msgtype"_s, u"m.text"_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);
|
||||
|
||||
server.joinUser(room->id(), u"@foo:server.com"_s);
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::empty()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
|
||||
QCOMPARE(chatBarCache->cache().toString(), 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::reply()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::replyMissingUser()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_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
|
||||
|
||||
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||
server.sendStateEvent(room->id(), u"m.room.member"_s, u"@foo:server.com"_s, {{u"membership"_s, u"leave"_s}});
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
|
||||
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
|
||||
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::edit()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||
QCOMPARE(oldEventId, QString());
|
||||
QCOMPARE(newEventId, eventId);
|
||||
});
|
||||
chatBarCache->setEditId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), true);
|
||||
QCOMPARE(chatBarCache->editId(), eventId);
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::attachment()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
|
||||
chatBarCache->setEditId(eventId);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
|
||||
QCOMPARE(chatBarCache->cache().toString(), 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"
|
||||
@@ -1,88 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatKeyHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
event.accepted = testHelper.keyHelper.handleKey(event.key, event.modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
ChatTextItemHelper {
|
||||
id: textItemHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
ChatKeyHelperTestHelper {
|
||||
id: testHelper
|
||||
|
||||
textItem: textItemHelper
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyUp
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledUp"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyDown
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledDown"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyDelete
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledDelete"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyBackSpace
|
||||
target: testHelper.keyHelper
|
||||
signalName: "unhandledBackspace"
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
textEdit.clear();
|
||||
spyUp.clear();
|
||||
spyDown.clear();
|
||||
spyDelete.clear();
|
||||
spyBackSpace.clear();
|
||||
textEdit.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cleanupTestCase(): void {
|
||||
testHelper.textItem = null;
|
||||
textItemHelper.textItem = null;
|
||||
}
|
||||
|
||||
function test_upDown(): void {
|
||||
textEdit.insert(0, "line 1\nline 2\nline 3")
|
||||
textEdit.cursorPosition = 0;
|
||||
keyClick(Qt.Key_Up);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 0);
|
||||
keyClick(Qt.Key_Down);
|
||||
compare(spyUp.count, 1);
|
||||
compare(spyDown.count, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include "chatkeyhelper.h"
|
||||
#include "chattextitemhelper.h"
|
||||
|
||||
class ChatKeyHelperTestHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT)
|
||||
|
||||
public:
|
||||
explicit ChatKeyHelperTestHelper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_keyHelper(new ChatKeyHelper(this))
|
||||
{
|
||||
}
|
||||
|
||||
ChatTextItemHelper *textItem() const
|
||||
{
|
||||
return m_keyHelper->textItem();
|
||||
}
|
||||
void setTextItem(ChatTextItemHelper *textItem)
|
||||
{
|
||||
if (textItem == m_keyHelper->textItem()) {
|
||||
return;
|
||||
}
|
||||
m_keyHelper->setTextItem(textItem);
|
||||
Q_EMIT textItemChanged();
|
||||
}
|
||||
|
||||
ChatKeyHelper *keyHelper() const
|
||||
{
|
||||
return m_keyHelper;
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
|
||||
private:
|
||||
QPointer<ChatKeyHelper> m_keyHelper;
|
||||
};
|
||||
@@ -1,173 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatMarkdownHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
|
||||
textFormat: TextEdit.RichText
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: textEdit2
|
||||
}
|
||||
|
||||
ChatMarkdownHelperTestWrapper {
|
||||
id: chatMarkdownHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyItem
|
||||
target: chatMarkdownHelper
|
||||
signalName: "textItemChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyUnhandledFormat
|
||||
target: chatMarkdownHelper
|
||||
signalName: "unhandledBlockFormat"
|
||||
}
|
||||
|
||||
function initTestCase(): void {
|
||||
textEdit.forceActiveFocus();
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
chatMarkdownHelper.clear();
|
||||
compare(chatMarkdownHelper.checkText(""), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
}
|
||||
|
||||
function test_item(): void {
|
||||
spyItem.clear();
|
||||
compare(chatMarkdownHelper.textItem, textEdit);
|
||||
chatMarkdownHelper.textItem = textEdit2;
|
||||
compare(chatMarkdownHelper.textItem, textEdit2);
|
||||
chatMarkdownHelper.textItem = textEdit;
|
||||
compare(chatMarkdownHelper.textItem, textEdit);
|
||||
}
|
||||
|
||||
function test_textFormat_data() {
|
||||
return [
|
||||
{tag: "bold", input: "**b** ", outText: ["*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold], [RichFormat.Bold], []], unhandled: 0},
|
||||
{tag: "italic", input: "*i* ", outText: ["*", "i", "i*", "i "], outFormats: [[], [RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "heading 1", input: "# h", outText: ["#", "# ", "h"], outFormats: [[], [], [RichFormat.Bold, RichFormat.Heading1]], unhandled: 0},
|
||||
{tag: "heading 2", input: "## h", outText: ["#", "##", "## ", "h"], outFormats: [[], [], [], [RichFormat.Bold, RichFormat.Heading2]], unhandled: 0},
|
||||
{tag: "heading 3", input: "### h", outText: ["#", "##", "###", "### ", "h"], outFormats: [[], [], [], [], [RichFormat.Bold, RichFormat.Heading3]], unhandled: 0},
|
||||
{tag: "heading 4", input: "#### h", outText: ["#", "##", "###", "####", "#### ", "h"], outFormats: [[], [], [], [], [], [RichFormat.Bold, RichFormat.Heading4]], unhandled: 0},
|
||||
{tag: "heading 5", input: "##### h", outText: ["#", "##", "###", "####", "#####", "##### ", "h"], outFormats: [[], [], [], [], [], [], [RichFormat.Bold, RichFormat.Heading5]], unhandled: 0},
|
||||
{tag: "heading 6", input: "###### h", outText: ["#", "##", "###", "####", "#####", "######", "###### ", "h"], outFormats: [[], [], [], [], [], [] ,[], [RichFormat.Bold, RichFormat.Heading6]], unhandled: 0},
|
||||
{tag: "quote", input: "> q", outText: [">", "> ", "q"], outFormats: [[], [], []], unhandled: 1},
|
||||
{tag: "quote - no space", input: ">q", outText: [">", "q"], outFormats: [[], [], []], unhandled: 1},
|
||||
{tag: "unorderedlist 1", input: "* l", outText: ["*", "* ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "unorderedlist 2", input: "- l", outText: ["-", "- ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 1", input: "1. l", outText: ["1", "1.", "1. ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "orderedlist 2", input: "1) l", outText: ["1", "1)", "1) ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
|
||||
{tag: "inline code", input: "`c` ", outText: ["`", "c", "c`", "c "], outFormats: [[], [RichFormat.InlineCode], [RichFormat.InlineCode], []], unhandled: 0},
|
||||
{tag: "code", input: "``` ", outText: ["`", "``", "```", " "], outFormats: [[], [], [], []], unhandled: 1},
|
||||
{tag: "strikethrough", input: "~~s~~ ", outText: ["~", "~~", "s", "s~", "s~~", "s "], outFormats: [[], [], [RichFormat.Strikethrough], [RichFormat.Strikethrough], [RichFormat.Strikethrough], []], unhandled: 0},
|
||||
{tag: "underline", input: "_u_ ", outText: ["_", "u", "u_", "u "], outFormats: [[], [RichFormat.Underline], [RichFormat.Underline], []], unhandled: 0},
|
||||
{tag: "multiple closable", input: "***_~~t~~_*** ", outText: ["*", "**", "*", "_", "~", "~~", "t", "t~", "t~~", "t_", "t*", "t**", "t*", "t "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
|
||||
{tag: "nonclosable closable", input: "* **b** ", outText: ["*", "* ", "*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.UnorderedList], [RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.UnorderedList]], unhandled: 0},
|
||||
{tag: "not at line start", input: " 1) ", outText: [" ", " 1", " 1)", " 1) "], outFormats: [[], [], [], []], unhandled: 0},
|
||||
]
|
||||
}
|
||||
|
||||
function test_textFormat(data): void {
|
||||
spyUnhandledFormat.clear();
|
||||
compare(spyUnhandledFormat.count, 0);
|
||||
|
||||
for (let i = 0; i < data.input.length; i++) {
|
||||
keyClick(data.input[i]);
|
||||
compare(chatMarkdownHelper.checkText(data.outText[i]), true);
|
||||
compare(chatMarkdownHelper.checkFormats(data.outFormats[i]), true);
|
||||
}
|
||||
|
||||
compare(spyUnhandledFormat.count, data.unhandled);
|
||||
}
|
||||
|
||||
function test_backspace(): void {
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("*"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("**"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("b"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("o");
|
||||
compare(chatMarkdownHelper.checkText("bo"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("l");
|
||||
compare(chatMarkdownHelper.checkText("bol"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("d");
|
||||
compare(chatMarkdownHelper.checkText("bold"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(Qt.Key_Backspace);
|
||||
compare(chatMarkdownHelper.checkText("bol"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(Qt.Key_Backspace);
|
||||
compare(chatMarkdownHelper.checkText("bo"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("bo*"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick("*");
|
||||
compare(chatMarkdownHelper.checkText("bo**"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
keyClick(" ");
|
||||
compare(chatMarkdownHelper.checkText("bo "), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
|
||||
function test_cursorMove(): void {
|
||||
keyClick("t");
|
||||
keyClick("e");
|
||||
keyClick("s");
|
||||
keyClick("t");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
keyClick("*");
|
||||
keyClick("*");
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("testb"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
|
||||
textEdit.cursorPosition = 2;
|
||||
keyClick("*");
|
||||
keyClick("*");
|
||||
keyClick("b");
|
||||
compare(chatMarkdownHelper.checkText("tebstb"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
|
||||
function test_insertText(): void {
|
||||
textEdit.insert(0, "test");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
textEdit.insert(4, "**b");
|
||||
compare(chatMarkdownHelper.checkText("test**b"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "test");
|
||||
compare(chatMarkdownHelper.checkText("test"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
textEdit.insert(2, "**b");
|
||||
compare(chatMarkdownHelper.checkText("te**bst"), true);
|
||||
compare(chatMarkdownHelper.checkFormats([]), true);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include "chatmarkdownhelper.h"
|
||||
#include "chattextitemhelper.h"
|
||||
#include "enums/richformat.h"
|
||||
|
||||
class ChatMarkdownHelperTestWrapper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QML text Item the ChatMerkdownHelper is handling.
|
||||
*/
|
||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
public:
|
||||
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
|
||||
, m_textItem(new ChatTextItemHelper(this))
|
||||
{
|
||||
m_chatMarkdownHelper->setTextItem(m_textItem);
|
||||
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
|
||||
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
|
||||
}
|
||||
|
||||
QQuickItem *textItem() const
|
||||
{
|
||||
return m_textItem->textItem();
|
||||
}
|
||||
void setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
m_textItem->setTextItem(textItem);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkText(const QString &text)
|
||||
{
|
||||
const auto doc = m_textItem->document();
|
||||
if (!doc) {
|
||||
return false;
|
||||
}
|
||||
return text == doc->toPlainText();
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFormats(QList<RichFormat::Format> formats)
|
||||
{
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return RichFormat::formatsAtCursor(cursor) == formats;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void clear()
|
||||
{
|
||||
auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor.select(QTextCursor::Document);
|
||||
cursor.removeSelectedText();
|
||||
cursor.setBlockCharFormat(RichFormat::charFormatForFormat(RichFormat::Paragraph));
|
||||
cursor.setBlockFormat(RichFormat::blockFormatForFormat(RichFormat::Paragraph));
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
void unhandledBlockFormat(RichFormat::Format format);
|
||||
|
||||
private:
|
||||
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
|
||||
QPointer<ChatTextItemHelper> m_textItem;
|
||||
};
|
||||
@@ -1,301 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 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 QtTest
|
||||
|
||||
import org.kde.neochat.libneochat
|
||||
|
||||
import NeoChatTestUtils
|
||||
|
||||
TestCase {
|
||||
name: "ChatTextItemHelperTest"
|
||||
|
||||
TextEdit {
|
||||
id: textEdit
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: textEdit2
|
||||
}
|
||||
|
||||
ChatTextItemHelper {
|
||||
id: textItemHelper
|
||||
|
||||
textItem: textEdit
|
||||
}
|
||||
|
||||
ChatTextItemHelperTestHelper {
|
||||
id: testHelper
|
||||
|
||||
textItem: textItemHelper
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyItem
|
||||
target: textItemHelper
|
||||
signalName: "textItemChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyContentsChanged
|
||||
target: textItemHelper
|
||||
signalName: "contentsChanged"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyContentsChange
|
||||
target: textItemHelper
|
||||
signalName: "contentsChange"
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: spyCursor
|
||||
target: textItemHelper
|
||||
signalName: "cursorPositionChanged"
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
testHelper.setFixedChars("", "");
|
||||
textEdit.clear();
|
||||
textEdit2.clear();
|
||||
spyItem.clear();
|
||||
spyContentsChange.clear();
|
||||
spyContentsChanged.clear();
|
||||
spyCursor.clear();
|
||||
}
|
||||
|
||||
function cleanupTestCase(): void {
|
||||
testHelper.textItem = null;
|
||||
textItemHelper.textItem = null;
|
||||
}
|
||||
|
||||
function test_item(): void {
|
||||
compare(textItemHelper.textItem, textEdit);
|
||||
compare(spyItem.count, 0);
|
||||
textItemHelper.textItem = textEdit2;
|
||||
compare(textItemHelper.textItem, textEdit2);
|
||||
compare(spyItem.count, 1);
|
||||
textItemHelper.textItem = textEdit;
|
||||
compare(textItemHelper.textItem, textEdit);
|
||||
compare(spyItem.count, 2);
|
||||
}
|
||||
|
||||
function test_fixedChars(): void {
|
||||
textEdit.forceActiveFocus();
|
||||
testHelper.setFixedChars("1", "2");
|
||||
compare(textEdit.text, "12");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 0);
|
||||
keyClick("b");
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 1);
|
||||
keyClick(Qt.Key_Left);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
keyClick(Qt.Key_Left);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 3);
|
||||
keyClick(Qt.Key_Right);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 4);
|
||||
keyClick(Qt.Key_Right);
|
||||
compare(textEdit.text, "1b2");
|
||||
compare(textEdit.cursorPosition, 2);
|
||||
compare(spyCursor.count, 5);
|
||||
}
|
||||
|
||||
function test_document(): void {
|
||||
// We can't get to the QTextDocument from QML so we have to use a helper function.
|
||||
compare(testHelper.compareDocuments(textEdit.textDocument), true);
|
||||
|
||||
textEdit.insert(0, "test text");
|
||||
compare(testHelper.lineCount(), 1);
|
||||
textEdit.insert(textEdit.text.length, "\ntest text");
|
||||
compare(testHelper.lineCount(), 2);
|
||||
textEdit.clear()
|
||||
compare(textEdit.text.length, 0);
|
||||
}
|
||||
|
||||
function test_takeFirstBlock(): void {
|
||||
textEdit.insert(0, "test text");
|
||||
compare(testHelper.firstBlockText(), "test text");
|
||||
compare(textEdit.text.length, 0);
|
||||
textEdit.insert(0, "test text\nmore test text");
|
||||
compare(testHelper.firstBlockText(), "test text");
|
||||
compare(textEdit.text, "more test text");
|
||||
compare(testHelper.firstBlockText(), "more test text");
|
||||
compare(textEdit.text, "");
|
||||
compare(textEdit.text.length, 0);
|
||||
}
|
||||
|
||||
function test_fillFragments(): void {
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
textEdit.cursorPosition = 16;
|
||||
compare(testHelper.checkFragments("before fragment", "mid fragment", "after fragment"), true);
|
||||
textEdit.clear();
|
||||
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
|
||||
textEdit.cursorPosition = 29;
|
||||
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
|
||||
textEdit.clear();
|
||||
}
|
||||
|
||||
function test_insertFragment(): void {
|
||||
testHelper.insertFragment("test text");
|
||||
compare(textEdit.text, "test text");
|
||||
compare(textEdit.cursorPosition, 9);
|
||||
testHelper.insertFragment("beginning ", 1);
|
||||
compare(textEdit.text, "beginning test text");
|
||||
compare(textEdit.cursorPosition, 10);
|
||||
testHelper.insertFragment(" end", 2);
|
||||
compare(textEdit.text, "beginning test text end");
|
||||
compare(textEdit.cursorPosition, 23);
|
||||
textEdit.clear();
|
||||
|
||||
testHelper.insertFragment("test text", 0, true);
|
||||
compare(textEdit.text, "test text");
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
}
|
||||
|
||||
function test_cursor(): void {
|
||||
// We can't get to the QTextCursor from QML so we have to use a helper function.
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get the appropriate content and cursor change signals when inserting text.
|
||||
textEdit.insert(0, "test text")
|
||||
compare(spyContentsChange.count, 1);
|
||||
compare(spyContentsChange.signalArguments[0][0], 0);
|
||||
compare(spyContentsChange.signalArguments[0][1], 0);
|
||||
compare(spyContentsChange.signalArguments[0][2], 9);
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 1);
|
||||
compare(spyCursor.signalArguments[0][0], true);
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get only get a cursor change signal when moving the cursor.
|
||||
textEdit.cursorPosition = 4;
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
compare(spyCursor.signalArguments[1][0], false);
|
||||
textEdit.selectAll();
|
||||
compare(spyContentsChanged.count, 1);
|
||||
compare(spyCursor.count, 2);
|
||||
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
|
||||
compare(textEdit.cursorPosition, testHelper.cursorPosition());
|
||||
// Check we get the appropriate content and cursor change signals when removing text.
|
||||
textEdit.clear();
|
||||
compare(spyContentsChange.count, 2);
|
||||
compare(spyContentsChange.signalArguments[1][0], 0);
|
||||
compare(spyContentsChange.signalArguments[1][1], 9);
|
||||
compare(spyContentsChange.signalArguments[1][2], 0);
|
||||
compare(spyContentsChanged.count, 2);
|
||||
compare(spyCursor.count, 3);
|
||||
compare(spyCursor.signalArguments[2][0], true);
|
||||
}
|
||||
|
||||
function test_setCursor(): void {
|
||||
textEdit.insert(0, "test text");
|
||||
compare(textEdit.cursorPosition, 9);
|
||||
compare(spyCursor.count, 1);
|
||||
testHelper.setCursorPosition(5);
|
||||
compare(textEdit.cursorPosition, 5);
|
||||
compare(spyCursor.count, 2);
|
||||
testHelper.setCursorPosition(1);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
compare(spyCursor.count, 3);
|
||||
|
||||
textEdit.cursorVisible = false;
|
||||
compare(textEdit.cursorVisible, false);
|
||||
testHelper.setCursorVisible(true);
|
||||
compare(textEdit.cursorVisible, true);
|
||||
testHelper.setCursorVisible(false);
|
||||
compare(textEdit.cursorVisible, false);
|
||||
}
|
||||
|
||||
function test_setCursorFromTextItem(): void {
|
||||
textEdit.insert(0, "line 1\nline 2");
|
||||
textEdit2.insert(0, "line 1\nline 2");
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 0);
|
||||
compare(textEdit.cursorPosition, 7);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 7);
|
||||
compare(textEdit.cursorPosition, 0);
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 1);
|
||||
compare(textEdit.cursorPosition, 8);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 8);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
|
||||
testHelper.setFixedChars("1", "2");
|
||||
testHelper.setCursorFromTextItem(textEdit2, false, 0);
|
||||
compare(textEdit.cursorPosition, 8);
|
||||
testHelper.setCursorFromTextItem(textEdit2, true, 7);
|
||||
compare(textEdit.cursorPosition, 1);
|
||||
}
|
||||
|
||||
function test_mergeFormat(): void {
|
||||
textEdit.insert(0, "lots of text");
|
||||
testHelper.setCursorPosition(0);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Bold);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Italic);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Italic]), true);
|
||||
testHelper.setCursorPosition(6);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Underline);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Underline]), true);
|
||||
testHelper.setCursorPosition(9);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Strikethrough);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Strikethrough]), true);
|
||||
textEdit.clear();
|
||||
|
||||
textEdit.insert(0, "heading");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Heading1);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading1]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Heading2);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading2]), true);
|
||||
testHelper.mergeFormatOnCursor(RichFormat.Paragraph);
|
||||
compare(testHelper.checkFormatsAtCursor([]), true);
|
||||
textEdit.clear();
|
||||
|
||||
textEdit.insert(0, "text");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.UnorderedList);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.UnorderedList]), true);
|
||||
compare(testHelper.markdownText(), "- text");
|
||||
testHelper.mergeFormatOnCursor(RichFormat.OrderedList);
|
||||
compare(testHelper.checkFormatsAtCursor([RichFormat.OrderedList]), true);
|
||||
compare(testHelper.markdownText(), "1. text");
|
||||
textEdit.clear();
|
||||
}
|
||||
|
||||
function test_list(): void {
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), true);
|
||||
testHelper.indentListMoreAtCursor();
|
||||
compare(testHelper.canIndentListMoreAtCursor(), false);
|
||||
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), true);
|
||||
testHelper.indentListLessAtCursor();
|
||||
compare(testHelper.canIndentListLessAtCursor(), false);
|
||||
}
|
||||
|
||||
function test_forceActiveFocus(): void {
|
||||
textEdit2.forceActiveFocus();
|
||||
compare(textEdit.activeFocus, false);
|
||||
testHelper.forceActiveFocus();
|
||||
compare(textEdit.activeFocus, true);
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocumentFragment>
|
||||
|
||||
#include "chattextitemhelper.h"
|
||||
|
||||
class ChatTextItemHelperTestHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QML text Item the TextItemHelper is handling.
|
||||
*/
|
||||
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
public:
|
||||
explicit ChatTextItemHelperTestHelper(QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ChatTextItemHelper *textItem() const
|
||||
{
|
||||
return m_textItem;
|
||||
}
|
||||
void setTextItem(ChatTextItemHelper *textItem)
|
||||
{
|
||||
if (textItem == m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem = textItem;
|
||||
Q_EMIT textItemChanged();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setFixedChars(const QString &startChars, const QString &endChars)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setFixedChars(startChars, endChars);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return document->textDocument() == m_textItem->document();
|
||||
}
|
||||
|
||||
Q_INVOKABLE int lineCount()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return -1;
|
||||
}
|
||||
return m_textItem->lineCount();
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString firstBlockText()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return {};
|
||||
}
|
||||
return m_textItem->takeFirstBlock().toPlainText();
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFragments(const QString &before, const QString &mid, const QString &after)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasBefore = false;
|
||||
QTextDocumentFragment midFragment;
|
||||
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
|
||||
m_textItem->fillFragments(hasBefore, midFragment, afterFragment);
|
||||
|
||||
return hasBefore && m_textItem->document()->toPlainText() == before && midFragment.toPlainText() == mid && after.isEmpty()
|
||||
? !afterFragment
|
||||
: afterFragment->toPlainText() == after;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void insertFragment(const QString &text, ChatTextItemHelper::InsertPosition position = ChatTextItemHelper::Cursor, bool keepPosition = false)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
const auto fragment = QTextDocumentFragment::fromPlainText(text);
|
||||
m_textItem->insertFragment(fragment, position, keepPosition);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
const auto posSame = cursor.position() == cursorPosition;
|
||||
const auto startSame = cursor.selectionStart() == selectionStart;
|
||||
const auto endSame = cursor.selectionEnd() == selectionEnd;
|
||||
return posSame && startSame && endSame;
|
||||
}
|
||||
|
||||
Q_INVOKABLE int cursorPosition() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return -1;
|
||||
}
|
||||
return *m_textItem->cursorPosition();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorPosition(int pos)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setCursorPosition(pos);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorVisible(bool visible)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->setCursorVisible(visible);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setCursorFromTextItem(QQuickItem *item, bool infront, int cursorPos)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
const auto textItem = new ChatTextItemHelper(this);
|
||||
textItem->setTextItem(item);
|
||||
textItem->setCursorPosition(cursorPos);
|
||||
m_textItem->setCursorFromTextItem(textItem, infront);
|
||||
textItem->deleteLater();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void mergeFormatOnCursor(RichFormat::Format format)
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->mergeFormatOnCursor(format);
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool checkFormatsAtCursor(QList<RichFormat::Format> formats)
|
||||
{
|
||||
const auto cursor = m_textItem->textCursor();
|
||||
if (cursor.isNull()) {
|
||||
return false;
|
||||
}
|
||||
return RichFormat::formatsAtCursor(cursor) == formats;
|
||||
}
|
||||
|
||||
Q_INVOKABLE bool canIndentListMoreAtCursor() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return m_textItem->canIndentListMoreAtCursor();
|
||||
}
|
||||
Q_INVOKABLE bool canIndentListLessAtCursor() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return false;
|
||||
}
|
||||
return m_textItem->canIndentListLessAtCursor();
|
||||
}
|
||||
Q_INVOKABLE void indentListMoreAtCursor()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->indentListMoreAtCursor();
|
||||
}
|
||||
Q_INVOKABLE void indentListLessAtCursor()
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->indentListLessAtCursor();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void forceActiveFocus() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return;
|
||||
}
|
||||
m_textItem->forceActiveFocus();
|
||||
}
|
||||
|
||||
Q_INVOKABLE QString markdownText() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return {};
|
||||
}
|
||||
return m_textItem->markdownText();
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void textItemChanged();
|
||||
|
||||
private:
|
||||
QPointer<ChatTextItemHelper> m_textItem;
|
||||
};
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -1,307 +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 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::dateTime(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::dateTime(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::dateTime(nullptr, nullptr), QDateTime());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
|
||||
QCOMPARE(EventHandler::dateTime(room, nullptr), QDateTime());
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -1,91 +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 Q_SLOTS:
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
|
||||
void multipleLinkPreviewsMatch_data();
|
||||
void multipleLinkPreviewsMatch();
|
||||
|
||||
void linkPreviewsReject_data();
|
||||
void linkPreviewsReject();
|
||||
};
|
||||
|
||||
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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -1,621 +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 <QAbstractItemModelTester>
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QVariantList>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "contentprovider.h"
|
||||
#include "enums/powerlevel.h"
|
||||
#include "enums/roomsortparameter.h"
|
||||
#include "models/accountemoticonmodel.h"
|
||||
#include "models/actionsmodel.h"
|
||||
#include "models/commonroomsmodel.h"
|
||||
#include "models/completionmodel.h"
|
||||
#include "models/completionproxymodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "models/devicesmodel.h"
|
||||
#include "models/devicesproxymodel.h"
|
||||
#include "models/emojimodel.h"
|
||||
#include "models/emoticonfiltermodel.h"
|
||||
#include "models/eventmessagecontentmodel.h"
|
||||
#include "models/imagepacksmodel.h"
|
||||
#include "models/linemodel.h"
|
||||
#include "models/livelocationsmodel.h"
|
||||
#include "models/locationsmodel.h"
|
||||
#include "models/messagecontentfiltermodel.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "models/notificationsmodel.h"
|
||||
#include "models/permissionsmodel.h"
|
||||
#include "models/pinnedmessagemodel.h"
|
||||
#include "models/pollanswermodel.h"
|
||||
#include "models/publicroomlistmodel.h"
|
||||
#include "models/pushrulemodel.h"
|
||||
#include "models/readmarkermodel.h"
|
||||
#include "models/roomsortparametermodel.h"
|
||||
#include "models/searchmodel.h"
|
||||
#include "models/serverlistmodel.h"
|
||||
#include "models/spacechildrenmodel.h"
|
||||
#include "models/spacechildsortfiltermodel.h"
|
||||
#include "models/statefiltermodel.h"
|
||||
#include "models/statekeysmodel.h"
|
||||
#include "models/statemodel.h"
|
||||
#include "models/stickermodel.h"
|
||||
#include "models/threadmodel.h"
|
||||
#include "models/threepidmodel.h"
|
||||
#include "models/userdirectorylistmodel.h"
|
||||
#include "models/userfiltermodel.h"
|
||||
#include "models/webshortcutmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
#include "roommanager.h"
|
||||
#include "server.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
// TODO: Add data to all models as relevant.
|
||||
|
||||
// Performs basic tests on all models in NeoChat
|
||||
// When adding a new test, create the model first, then the tester, then initialize the model (e.g., setConnection and setRoom).
|
||||
// That way, the models are also tested for whether they can handle having no connection etc.
|
||||
class ModelTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
NeoChatConnection *connection = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
|
||||
QString eventId;
|
||||
|
||||
Server server;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void testRoomTreeModel();
|
||||
void testMessageContentModel();
|
||||
void testEventMessageContentModel();
|
||||
void testThreadModel();
|
||||
void testThreadFetchModel();
|
||||
void testThreadChatBarModel();
|
||||
void testReactionModel();
|
||||
void testPollAnswerModel();
|
||||
void testLineModel();
|
||||
void testSpaceChildrenModel();
|
||||
void testItineraryModel();
|
||||
void testPublicRoomListModel();
|
||||
void testMessageFilterModel();
|
||||
void testThreePIdModel();
|
||||
void testMediaMessageFilterModel();
|
||||
void testWebshortcutModel();
|
||||
void testTimelineMessageModel();
|
||||
void testReadMarkerModel();
|
||||
void testSearchModel();
|
||||
void testStateModel();
|
||||
void testTimelineModel();
|
||||
void testStateKeysModel();
|
||||
void testPinnedMessageModel();
|
||||
void testUserListModel();
|
||||
void testStickerModel();
|
||||
void testPowerLevelModel();
|
||||
void testImagePacksModel();
|
||||
void testCompletionModel();
|
||||
void testRoomListModel();
|
||||
void testCommonRoomsModel();
|
||||
void testNotificationsModel();
|
||||
void testLocationsModel();
|
||||
void testServerListModel();
|
||||
void testEmojiModel();
|
||||
void testCustomEmojiModel();
|
||||
void testPushRuleModel();
|
||||
void testActionsModel();
|
||||
void testDevicesModel();
|
||||
void testUserDirectoryListModel();
|
||||
void testAccountEmoticonModel();
|
||||
void testPermissionsModel();
|
||||
void testLiveLocationsModel();
|
||||
void testRoomSortParameterModel();
|
||||
void testSortFilterRoomTreeModel();
|
||||
void testSortFilterSpaceListModel();
|
||||
void testSortFilterRoomListModel();
|
||||
void testSpaceChildSortFilterModel();
|
||||
void testStateFilterModel();
|
||||
void testMessageContentFilterModel();
|
||||
void testUserFilterModel();
|
||||
void testEmoticonFilterModel();
|
||||
void testDevicesProxyModel();
|
||||
void testCompletionProxyModel();
|
||||
};
|
||||
|
||||
void ModelTest::initTestCase()
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
server.start();
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
auto accountManager = new AccountManager(true, this);
|
||||
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
|
||||
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||
eventId = server.sendEvent(roomId,
|
||||
u"m.room.message"_s,
|
||||
QJsonObject{
|
||||
{u"body"_s, u"foo"_s},
|
||||
{u"msgtype"_s, u"m.text"_s},
|
||||
});
|
||||
|
||||
server.sendEvent(roomId,
|
||||
u"m.room.message"_s,
|
||||
QJsonObject{
|
||||
{u"body"_s, u"asdf"_s},
|
||||
{u"m.relates_to"_s,
|
||||
QJsonObject{
|
||||
{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s},
|
||||
{u"is_falling_back"_s, true},
|
||||
{u"m.in_reply_to"_s, QJsonObject{{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s}}},
|
||||
{u"rel_type"_s, u"m.thread"_s},
|
||||
}},
|
||||
{u"msgtype"_s, u"m.text"_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 ModelTest::testRoomTreeModel()
|
||||
{
|
||||
auto roomTreeModel = new RoomTreeModel(this);
|
||||
auto tester = new QAbstractItemModelTester(roomTreeModel, roomTreeModel);
|
||||
tester->setUseFetchMore(true);
|
||||
roomTreeModel->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testMessageContentModel()
|
||||
{
|
||||
auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
|
||||
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testEventMessageContentModel()
|
||||
{
|
||||
auto model = std::make_unique<EventMessageContentModel>(room, eventId);
|
||||
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreadModel()
|
||||
{
|
||||
auto model = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreadFetchModel()
|
||||
{
|
||||
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto model = new ThreadFetchModel(threadModel.get());
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreadChatBarModel()
|
||||
{
|
||||
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
|
||||
auto model = new ThreadChatBarModel(threadModel.get(), room);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testReactionModel()
|
||||
{
|
||||
auto messageContentModel = std::make_unique<MessageContentModel>(room);
|
||||
auto model = new ReactionModel(messageContentModel.get(), eventId, room);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testPollAnswerModel()
|
||||
{
|
||||
auto handler = std::make_unique<PollHandler>(room, eventId);
|
||||
auto model = new PollAnswerModel(handler.get());
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testLineModel()
|
||||
{
|
||||
auto model = new LineModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto document = new QTextDocument(this);
|
||||
model->setDocument(document);
|
||||
document->setPlainText(u"foo\nbar\n\nbaz"_s);
|
||||
}
|
||||
|
||||
void ModelTest::testSpaceChildrenModel()
|
||||
{
|
||||
auto model = new SpaceChildrenModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setSpace(room);
|
||||
}
|
||||
|
||||
void ModelTest::testItineraryModel()
|
||||
{
|
||||
auto model = new ItineraryModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testPublicRoomListModel()
|
||||
{
|
||||
auto model = new PublicRoomListModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testMessageFilterModel()
|
||||
{
|
||||
auto timelineModel = new TimelineModel(this);
|
||||
auto model = new MessageFilterModel(this, timelineModel);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
timelineModel->setRoom(room);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testThreePIdModel()
|
||||
{
|
||||
auto model = new ThreePIdModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testMediaMessageFilterModel()
|
||||
{
|
||||
auto timelineModel = new TimelineModel(this);
|
||||
auto messageFilterModel = new MessageFilterModel(this, timelineModel);
|
||||
auto model = new MediaMessageFilterModel(this, messageFilterModel);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
timelineModel->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testWebshortcutModel()
|
||||
{
|
||||
auto model = new WebShortcutModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setSelectedText(u"Foo"_s);
|
||||
}
|
||||
|
||||
void ModelTest::testTimelineMessageModel()
|
||||
{
|
||||
auto model = new TimelineMessageModel();
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testReadMarkerModel()
|
||||
{
|
||||
auto model = std::make_unique<ReadMarkerModel>(eventId, room);
|
||||
auto tester = new QAbstractItemModelTester(model.get(), model.get());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testSearchModel()
|
||||
{
|
||||
auto model = new SearchModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setSearchText(u"foo"_s);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testStateModel()
|
||||
{
|
||||
auto model = new StateModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testTimelineModel()
|
||||
{
|
||||
auto model = new TimelineModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testStateKeysModel()
|
||||
{
|
||||
auto model = new StateKeysModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setEventType(u"m.room.member"_s);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testPinnedMessageModel()
|
||||
{
|
||||
auto model = new PinnedMessageModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testUserListModel()
|
||||
{
|
||||
auto model = new UserListModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testStickerModel()
|
||||
{
|
||||
auto model = new StickerModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setPackIndex(0);
|
||||
model->setRoom(room);
|
||||
auto imagePacksModel = new ImagePacksModel(this);
|
||||
model->setModel(imagePacksModel);
|
||||
imagePacksModel->setRoom(room);
|
||||
imagePacksModel->setShowEmoticons(true);
|
||||
imagePacksModel->setShowStickers(true);
|
||||
}
|
||||
|
||||
void ModelTest::testPowerLevelModel()
|
||||
{
|
||||
auto model = new PowerLevelModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testImagePacksModel()
|
||||
{
|
||||
auto model = new ImagePacksModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
model->setShowEmoticons(true);
|
||||
model->setShowStickers(true);
|
||||
}
|
||||
|
||||
void ModelTest::testCompletionModel()
|
||||
{
|
||||
auto model = new CompletionModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setAutoCompletionType(CompletionModel::Room);
|
||||
auto roomListModel = new RoomListModel(this);
|
||||
roomListModel->setConnection(connection);
|
||||
model->setRoomListModel(roomListModel);
|
||||
}
|
||||
|
||||
void ModelTest::testRoomListModel()
|
||||
{
|
||||
auto model = new RoomListModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testCommonRoomsModel()
|
||||
{
|
||||
auto model = new CommonRoomsModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
model->setUserId(u"@user:example.com"_s);
|
||||
}
|
||||
|
||||
void ModelTest::testNotificationsModel()
|
||||
{
|
||||
auto model = new NotificationsModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testLocationsModel()
|
||||
{
|
||||
auto model = new LocationsModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testServerListModel()
|
||||
{
|
||||
auto model = new ServerListModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testEmojiModel()
|
||||
{
|
||||
auto tester = new QAbstractItemModelTester(&EmojiModel::instance(), &EmojiModel::instance());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testCustomEmojiModel()
|
||||
{
|
||||
auto tester = new QAbstractItemModelTester(&CustomEmojiModel::instance(), &CustomEmojiModel::instance());
|
||||
tester->setUseFetchMore(true);
|
||||
CustomEmojiModel::instance().setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testPushRuleModel()
|
||||
{
|
||||
auto model = new PushRuleModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testActionsModel()
|
||||
{
|
||||
auto tester = new QAbstractItemModelTester(&ActionsModel::instance(), &ActionsModel::instance());
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testDevicesModel()
|
||||
{
|
||||
auto model = new DevicesModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testUserDirectoryListModel()
|
||||
{
|
||||
auto model = new UserDirectoryListModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
model->setSearchText(u"foo"_s);
|
||||
}
|
||||
|
||||
void ModelTest::testAccountEmoticonModel()
|
||||
{
|
||||
auto model = new AccountEmoticonModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testPermissionsModel()
|
||||
{
|
||||
auto model = new PermissionsModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testLiveLocationsModel()
|
||||
{
|
||||
auto model = new LiveLocationsModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testRoomSortParameterModel()
|
||||
{
|
||||
auto model = new RoomSortParameterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
}
|
||||
|
||||
void ModelTest::testSortFilterRoomTreeModel()
|
||||
{
|
||||
auto sourceModel = new RoomTreeModel(this);
|
||||
auto model = new SortFilterRoomTreeModel(sourceModel, sourceModel);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
sourceModel->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testSortFilterSpaceListModel()
|
||||
{
|
||||
auto sourceModel = new RoomListModel(this);
|
||||
auto model = new SortFilterSpaceListModel(sourceModel, sourceModel);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
sourceModel->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testSortFilterRoomListModel()
|
||||
{
|
||||
auto sourceModel = new RoomListModel(this);
|
||||
auto model = new SortFilterRoomListModel(sourceModel, sourceModel);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
sourceModel->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testSpaceChildSortFilterModel()
|
||||
{
|
||||
auto model = new SpaceChildSortFilterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto spaceChildrenModel = new SpaceChildrenModel(this);
|
||||
model->setSourceModel(spaceChildrenModel);
|
||||
spaceChildrenModel->setSpace(nullptr);
|
||||
}
|
||||
|
||||
void ModelTest::testStateFilterModel()
|
||||
{
|
||||
auto model = new StateFilterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto stateModel = new StateModel(this);
|
||||
model->setSourceModel(stateModel);
|
||||
stateModel->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testMessageContentFilterModel()
|
||||
{
|
||||
auto model = new MessageContentFilterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setSourceModel(ContentProvider::self().contentModelForEvent(room, eventId));
|
||||
}
|
||||
|
||||
void ModelTest::testUserFilterModel()
|
||||
{
|
||||
auto model = new UserFilterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto userListModel = new UserListModel(this);
|
||||
model->setSourceModel(userListModel);
|
||||
userListModel->setRoom(room);
|
||||
}
|
||||
|
||||
void ModelTest::testEmoticonFilterModel()
|
||||
{
|
||||
auto model = new EmoticonFilterModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto accountEmoticonModel = new AccountEmoticonModel(this);
|
||||
model->setSourceModel(accountEmoticonModel);
|
||||
model->setShowEmojis(true);
|
||||
model->setShowStickers(true);
|
||||
accountEmoticonModel->setConnection(connection);
|
||||
}
|
||||
|
||||
void ModelTest::testDevicesProxyModel()
|
||||
{
|
||||
auto model = new DevicesProxyModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
auto devicesModel = new DevicesModel(this);
|
||||
model->setSourceModel(devicesModel);
|
||||
devicesModel->setConnection(dynamic_cast<NeoChatConnection *>(connection));
|
||||
}
|
||||
|
||||
void ModelTest::testCompletionProxyModel()
|
||||
{
|
||||
auto model = new CompletionProxyModel(this);
|
||||
auto tester = new QAbstractItemModelTester(model, model);
|
||||
tester->setUseFetchMore(true);
|
||||
model->setSourceModel(&EmojiModel::instance());
|
||||
}
|
||||
|
||||
QTEST_MAIN(ModelTest)
|
||||
#include "modeltest.moc"
|
||||
@@ -1,64 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "server.h"
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class NeoChatRoomTest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
Server server;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void eventTest();
|
||||
};
|
||||
|
||||
void NeoChatRoomTest::initTestCase()
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
server.start();
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
auto accountManager = new AccountManager(true, this);
|
||||
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
|
||||
|
||||
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||
server.sendEvent(roomId,
|
||||
u"m.room.message"_s,
|
||||
QJsonObject{
|
||||
{u"body"_s, u"foo"_s},
|
||||
{u"msgtype"_s, u"m.text"_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 NeoChatRoomTest::eventTest()
|
||||
{
|
||||
QCOMPARE(room->timelineSize(), 1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(NeoChatRoomTest)
|
||||
#include "neochatroomtest.moc"
|
||||
@@ -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"
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <quicktest.h>
|
||||
|
||||
QUICK_TEST_MAIN(NeoChat)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -1,402 +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).constFirst());
|
||||
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 {
|
||||
qInfo() << "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;
|
||||
}
|
||||
|
||||
QString Server::sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content)
|
||||
{
|
||||
Changes changes;
|
||||
const auto eventId = generateEventId();
|
||||
const auto json = 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},
|
||||
{u"state_key"_s, stateKey}};
|
||||
changes.events += Changes::Event{
|
||||
.fullJson = json,
|
||||
};
|
||||
changes.stateEvents += Changes::Event{.fullJson = json};
|
||||
m_state += changes;
|
||||
return eventId;
|
||||
}
|
||||
|
||||
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
|
||||
{
|
||||
QJsonObject joinRooms;
|
||||
auto token = request.query().queryItemValue(u"since"_s).toInt();
|
||||
|
||||
const auto changes = m_state.mid(token);
|
||||
for (const auto &change : changes) {
|
||||
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 : changes) {
|
||||
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 : changes) {
|
||||
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 : changes) {
|
||||
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 : changes) {
|
||||
for (const auto &state : change.stateEvents) {
|
||||
const auto &roomId = state.fullJson[u"room_id"_s].toString();
|
||||
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
|
||||
auto stateEvents = joinRooms[roomId][u"state"_s][u"events"_s].toArray();
|
||||
stateEvents.append(state.fullJson);
|
||||
auto room = joinRooms[roomId].toObject();
|
||||
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
|
||||
joinRooms[roomId] = room;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &change : changes) {
|
||||
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;
|
||||
}
|
||||
|
||||
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
|
||||
}
|
||||
@@ -1,80 +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;
|
||||
QList<Event> stateEvents;
|
||||
};
|
||||
|
||||
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);
|
||||
QString sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content);
|
||||
|
||||
private:
|
||||
QHttpServer m_server;
|
||||
QSslServer m_sslServer;
|
||||
|
||||
void sync(const QHttpServerRequest &request, QHttpServerResponder &responder);
|
||||
|
||||
QList<Changes> m_state;
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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), , `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), "_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><p>Test <span style="font-size:50px;">some</span> code</p>\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 & b"_s << u"a & b"_s;
|
||||
QTest::newRow("quote") << u""a and b""_s << u"\"a and b\""_s;
|
||||
QTest::newRow("new line") << u"new<br>line"_s << u"new\nline"_s;
|
||||
QTest::newRow("unescape") << u"can'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"<plain text in tag bracket><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 <strong>with tags</strong></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&via=matrix.org&via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s
|
||||
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\" style=\"text-decoration: none;\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\" 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"
|
||||
@@ -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"
|
||||
@@ -1,89 +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);
|
||||
|
||||
// make sure we restore maximized state when toggling
|
||||
instance.toggleWindow();
|
||||
window.setVisibility(QWindow::Maximized);
|
||||
QCOMPARE(window.windowState(), Qt::WindowMaximized);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.isVisible(), false);
|
||||
instance.toggleWindow();
|
||||
QCOMPARE(window.windowState(), Qt::WindowMaximized);
|
||||
QCOMPARE(window.isVisible(), true);
|
||||
}
|
||||
|
||||
QTEST_MAIN(WindowControllerTest)
|
||||
#include "windowcontrollertest.moc"
|
||||
14
cmake/Flatpak.cmake
Normal file
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Include FontConfig config which uses the Emoji One font from the
|
||||
# KDE Flatpak SDK.
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
|
||||
DESTINATION
|
||||
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
|
||||
)
|
||||
|
||||
23
cmake/Flatpak/99-noto-mono-color-emoji.conf
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<alias>
|
||||
<family>serif</family>
|
||||
<prefer>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
<alias>
|
||||
<family>sans-serif</family>
|
||||
<prefer>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
<alias>
|
||||
<family>monospace</family>
|
||||
<prefer>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
</fontconfig>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
|
||||
<!ENTITY % English "INCLUDE">
|
||||
]>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
-->
|
||||
|
||||
<refentry lang="&language;">
|
||||
<refentryinfo>
|
||||
<title>NeoChat User's Manual</title>
|
||||
<author><firstname>Carl</firstname><surname>Schwan</surname>
|
||||
<contrib>NeoChat man page.</contrib>
|
||||
<email>carl@carlschwan.eu</email></author>
|
||||
<date>2022-11-01</date>
|
||||
<releaseinfo>22.09</releaseinfo>
|
||||
<productname>NeoChat</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>
|
||||
<command>neochat</command>
|
||||
</refentrytitle>
|
||||
<manvolnum>1</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>neochat</refname>
|
||||
<refpurpose>Client for interacting with the matrix messaging protocol</refpurpose>
|
||||
</refnamediv>
|
||||
<!-- body begins here -->
|
||||
<refsynopsisdiv id='synopsis'>
|
||||
<cmdsynopsis>
|
||||
<command>neochat</command>
|
||||
<arg choice="opt"><replaceable>URI</replaceable></arg>
|
||||
</cmdsynopsis>
|
||||
</refsynopsisdiv>
|
||||
|
||||
|
||||
<refsect1 id="description">
|
||||
<title>Description</title>
|
||||
<para>
|
||||
<command>neochat</command> is a chat application for the matrix protocol
|
||||
that work on both desktop and mobile.
|
||||
</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="options"><title>Options</title>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><option>URI</option></term>
|
||||
<listitem>
|
||||
<para>
|
||||
The matrix uri for an user or a room. e.g matrix:u/user:example.org and
|
||||
matrix:r/root:example.org. This will makes NeoChat try to open the given
|
||||
room or conversation.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="bug">
|
||||
<title>Reporting bugs</title>
|
||||
<para>You can report bugs and feature requests at <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General">https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General</ulink></para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<simplelist>
|
||||
<member>
|
||||
A list of frequently asked questions about Matrix <ulink url="https://matrix.org/faq/">https://matrix.org/faq/</ulink>
|
||||
</member>
|
||||
<member>kf5options(7)</member>
|
||||
<member>qt5options(7)</member>
|
||||
</simplelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="copyright"><title>Copyright</title>
|
||||
<para>Copyright © 2020-2022 Tobias Fella </para>
|
||||
<para>Copyright © 2020-2022 Carl Schwan </para>
|
||||
<para>License: GNU General Public Version 3 or later <<ulink url="https://www.gnu.org/licenses/gpl-3.0.html">https://www.gnu.org/licenses/gpl-3.0.html</ulink>></para>
|
||||
</refsect1>
|
||||
</refentry>
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 36 KiB |
197
imports/NeoChat/Component/ChatBox/AttachmentPane.qml
Normal file
@@ -0,0 +1,197 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Page 1.0
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
property var attachmentMimetype: FileType.mimeTypeForUrl(chatBoxHelper.attachmentPath)
|
||||
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
|
||||
|
||||
active: visible
|
||||
sourceComponent: Component {
|
||||
Pane {
|
||||
id: attachmentPane
|
||||
property string baseFileName: chatBoxHelper.attachmentPath.toString().substring(chatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, chatBoxHelper.attachmentPath.length)
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
contentItem: Item {
|
||||
property real spacing: attachmentPane.spacing > 0 ? attachmentPane.spacing : toolBar.spacing
|
||||
implicitWidth: Math.max(image.implicitWidth, imageBusyIndicator.implicitWidth, fileInfoLayout.implicitWidth, toolBar.implicitWidth)
|
||||
implicitHeight: Math.max(
|
||||
(hasImage ? Math.max(image.preferredHeight, imageBusyIndicator.implicitHeight) + spacing : 0)
|
||||
+ fileInfoLayout.implicitHeight,
|
||||
toolBar.implicitHeight
|
||||
)
|
||||
|
||||
Image {
|
||||
id: image
|
||||
property real preferredHeight: Math.min(implicitHeight, Kirigami.Units.gridUnit * 8)
|
||||
height: preferredHeight
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: fileInfoLayout.top
|
||||
bottomMargin: parent.spacing
|
||||
}
|
||||
width: Math.min(implicitWidth, attachmentPane.availableWidth)
|
||||
asynchronous: true
|
||||
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
|
||||
smooth: height == preferredHeight && parent.height == parent.implicitHeight // Don't smooth until height animation stops
|
||||
source: hasImage ? chatBoxHelper.attachmentPath : ""
|
||||
visible: hasImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
onSourceChanged: {
|
||||
// Reset source size height, which affect implicitHeight
|
||||
sourceSize.height = -1
|
||||
}
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if (implicitHeight > Kirigami.Units.gridUnit * 8) {
|
||||
// This can save a lot of RAM when loading large images.
|
||||
// It also improves visual quality for large images.
|
||||
sourceSize.height = Kirigami.Units.gridUnit * 8
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: imageBusyIndicator
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
bottom: fileInfoLayout.top
|
||||
bottomMargin: parent.spacing
|
||||
}
|
||||
visible: running
|
||||
running: image.visible && image.progress < 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fileInfoLayout
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: undefined
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: parent.spacing
|
||||
|
||||
Kirigami.Icon {
|
||||
id: mimetypeIcon
|
||||
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(fileLabel.implicitHeight)
|
||||
implicitWidth: implicitHeight
|
||||
source: attachmentMimetype.iconName
|
||||
}
|
||||
|
||||
Label {
|
||||
id: fileLabel
|
||||
text: baseFileName
|
||||
}
|
||||
|
||||
states: State {
|
||||
when: !hasImage
|
||||
AnchorChanges {
|
||||
target: fileInfoLayout
|
||||
anchors.bottom: undefined
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Using a toolbar to get a button spacing consistent with what the QQC2 style normally has
|
||||
// Also has some accessibility info
|
||||
ToolBar {
|
||||
id: toolBar
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
Kirigami.Theme.inherit: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: parent.spacing
|
||||
Label {
|
||||
Layout.leftMargin: -attachmentPane.leftPadding
|
||||
Layout.topMargin: -attachmentPane.topPadding
|
||||
leftPadding: cancelAttachmentButton.leftPadding + 1 + attachmentPane.leftPadding
|
||||
rightPadding: cancelAttachmentButton.rightPadding + 1
|
||||
topPadding: cancelAttachmentButton.topPadding + attachmentPane.topPadding
|
||||
bottomPadding: cancelAttachmentButton.bottomPadding
|
||||
text: i18n("Attachment:")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
property real cornerRadius: cancelAttachmentButton.background.hasOwnProperty("radius") ?
|
||||
Math.min(cancelAttachmentButton.background.radius, height/2) : 0
|
||||
corners.bottomLeftRadius: toolBar.mirrored ? cornerRadius : 0
|
||||
corners.bottomRightRadius: toolBar.mirrored ? 0 : cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.75
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Button {
|
||||
id: editImageButton
|
||||
visible: hasImage
|
||||
icon.name: "document-edit"
|
||||
text: i18n("Edit")
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
Component {
|
||||
id: imageEditorPage
|
||||
ImageEditorPage {
|
||||
imagePath: chatBoxHelper.attachmentPath
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
|
||||
imageEditor.newPathChanged.connect(function(newPath) {
|
||||
applicationWindow().pageStack.layers.pop();
|
||||
chatBoxHelper.attachmentPath = newPath;
|
||||
});
|
||||
}
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
Button {
|
||||
id: cancelAttachmentButton
|
||||
icon.name: "dialog-cancel"
|
||||
text: i18n("Cancel")
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: chatBoxHelper.clearAttachment();
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
background: null
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
422
imports/NeoChat/Component/ChatBox/ChatBar.qml
Normal file
@@ -0,0 +1,422 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Templates 2.15 as T
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import QtQuick.Window 2.15
|
||||
|
||||
import org.kde.kirigami 2.18 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
ToolBar {
|
||||
id: chatBar
|
||||
property string replyEventId: ""
|
||||
property string editEventId: ""
|
||||
property alias inputFieldText: inputField.text
|
||||
property alias textField: inputField
|
||||
property alias emojiPaneOpened: emojiButton.checked
|
||||
|
||||
// store each user we autoComplete here, this will be helpful later to generate
|
||||
// the matrix.to links.
|
||||
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
|
||||
property var userAutocompleted: ({})
|
||||
|
||||
signal closeAllTriggered()
|
||||
signal inputFieldForceActiveFocusTriggered()
|
||||
signal messageSent()
|
||||
signal pasteImageTriggered()
|
||||
signal editLastUserMessage()
|
||||
signal replyPreviousUserMessage()
|
||||
|
||||
property alias isCompleting: completionMenu.visible
|
||||
|
||||
onInputFieldForceActiveFocusTriggered: {
|
||||
inputField.forceActiveFocus();
|
||||
// set the cursor to the end of the text
|
||||
inputField.cursorPosition = inputField.length;
|
||||
}
|
||||
|
||||
position: ToolBar.Footer
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
// Using a custom background because some styles like Material
|
||||
// or Fusion might have ugly colors for a TextArea placed inside
|
||||
// of a toolbar. ToolBar is otherwise the closest QQC2 component
|
||||
// to what we want because of the padding and spacing values.
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: chatBar.spacing
|
||||
|
||||
ScrollView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: inputField.implicitHeight
|
||||
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
|
||||
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
|
||||
+ inputField.topPadding + inputField.bottomPadding
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
font: inputField.font
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: inputField
|
||||
focus: true
|
||||
/* Some QQC2 styles will have their own predefined backgrounds for TextAreas.
|
||||
* Make sure there is no background since we are using the ToolBar background.
|
||||
*
|
||||
* This could cause a problem if the QQC2 style was designed around TextArea
|
||||
* background colors being very different from the QPalette::Base color.
|
||||
* Luckily, none of the Qt QQC2 styles do that and neither do KDE's QQC2 styles.
|
||||
*/
|
||||
background: MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
z: 1
|
||||
}
|
||||
|
||||
leftPadding: mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
rightPadding: !mirrored ? 0 : Kirigami.Units.largeSpacing
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
property real progress: 0
|
||||
property bool autoAppeared: false
|
||||
//property int lineHeight: contentHeight / lineCount
|
||||
|
||||
text: inputFieldText
|
||||
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message…")
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
horizontalAlignment: TextEdit.AlignLeft
|
||||
wrapMode: Text.Wrap
|
||||
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.SpellChecking.enabled: true
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
hoverEnabled: !Kirigami.Settings.tabletMode
|
||||
|
||||
selectByMouse: !Kirigami.Settings.tabletMode
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
document: inputField.textDocument
|
||||
cursorPosition: inputField.cursorPosition
|
||||
selectionStart: inputField.selectionStart
|
||||
selectionEnd: inputField.selectionEnd
|
||||
room: currentRoom ?? null
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: repeatTimer
|
||||
interval: 5000
|
||||
}
|
||||
|
||||
function sendMessage(event) {
|
||||
if (isCompleting && completionMenu.count > 0) {
|
||||
chatBar.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
inputField.insert(cursorPosition, "\n")
|
||||
} else {
|
||||
currentRoom.sendTypingNotification(false)
|
||||
chatBar.postMessage()
|
||||
}
|
||||
isCompleting = false;
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: { sendMessage(event) }
|
||||
Keys.onEnterPressed: { sendMessage(event) }
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
closeAllTriggered()
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||
chatBar.pasteImage();
|
||||
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
|
||||
replyPreviousUserMessage();
|
||||
} else if (event.key === Qt.Key_Up && inputField.text.length === 0) {
|
||||
editLastUserMessage();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomUp();
|
||||
return;
|
||||
}
|
||||
if (!isCompleting) {
|
||||
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocusReason)
|
||||
return
|
||||
}
|
||||
if (!autoAppeared) {
|
||||
let decrementedIndex = completionMenu.currentIndex - 1
|
||||
// Wrap around to the last item
|
||||
if (decrementedIndex < 0) {
|
||||
decrementedIndex = Math.max(completionMenu.count - 1, 0) // 0 if count == 0
|
||||
}
|
||||
completionMenu.currentIndex = decrementedIndex
|
||||
} else {
|
||||
autoAppeared = false;
|
||||
}
|
||||
|
||||
chatBar.complete();
|
||||
}
|
||||
|
||||
// yes, decrement goes up and increment goes down visually.
|
||||
Keys.onUpPressed: (event) => {
|
||||
if (chatBar.isCompleting) {
|
||||
event.accepted = true
|
||||
completionMenu.listView.decrementCurrentIndex()
|
||||
autoAppeared = true;
|
||||
}
|
||||
event.accepted = false
|
||||
}
|
||||
|
||||
Keys.onDownPressed: (event) => {
|
||||
if (chatBar.isCompleting) {
|
||||
event.accepted = true
|
||||
completionMenu.listView.incrementCurrentIndex()
|
||||
autoAppeared = true;
|
||||
}
|
||||
event.accepted = false
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
switchRoomDown();
|
||||
return;
|
||||
}
|
||||
if (!isCompleting) {
|
||||
nextItemInFocusChain().forceActiveFocus(Qt.TabFocusReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO detect moved cursor
|
||||
|
||||
// ignore first time tab was clicked so that user can select
|
||||
// first emoji/user
|
||||
if (!autoAppeared) {
|
||||
let incrementedIndex = completionMenu.currentIndex + 1;
|
||||
// Wrap around to the first item
|
||||
if (incrementedIndex > completionMenu.count - 1) {
|
||||
incrementedIndex = 0
|
||||
}
|
||||
completionMenu.currentIndex = incrementedIndex;
|
||||
} else {
|
||||
autoAppeared = false;
|
||||
}
|
||||
|
||||
chatBar.complete();
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (!repeatTimer.running && Config.typingNotifications) {
|
||||
currentRoom.sendTypingNotification(true)
|
||||
}
|
||||
repeatTimer.start()
|
||||
|
||||
currentRoom.cachedInput = text
|
||||
autoAppeared = false;
|
||||
|
||||
const completionInfo = documentHandler.getAutocompletionInfo(isCompleting);
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.Ignore) {
|
||||
if (completionInfo.keyword) {
|
||||
// custom emojis
|
||||
const idx = completionMenu.currentIndex;
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
completionMenu.currentIndex = idx;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (completionInfo.type === ChatDocumentHandler.None) {
|
||||
isCompleting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
completionMenu.completionType = completionInfo.type
|
||||
if (completionInfo.type === ChatDocumentHandler.User) {
|
||||
completionMenu.model = currentRoom.getUsers(completionInfo.keyword, 10);
|
||||
} else if (completionInfo.type === ChatDocumentHandler.Command) {
|
||||
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
|
||||
} else {
|
||||
completionMenu.model = Array.from(chatBar.customEmojiModel.filterModel(completionInfo.keyword)).concat(EmojiModel.filterModel(completionInfo.keyword))
|
||||
}
|
||||
|
||||
if (completionMenu.model.length === 0) {
|
||||
isCompleting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCompleting) {
|
||||
isCompleting = true
|
||||
autoAppeared = true;
|
||||
completionMenu.endPosition = cursorPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !chatBoxHelper.isReplying && (!chatBoxHelper.hasAttachment || uploadingBusySpinner.running)
|
||||
implicitWidth: uploadButton.implicitWidth
|
||||
implicitHeight: uploadButton.implicitHeight
|
||||
ToolButton {
|
||||
id: uploadButton
|
||||
anchors.fill: parent
|
||||
// Matrix does not allow sending attachments in replies
|
||||
visible: !chatBoxHelper.isReplying && !chatBoxHelper.hasAttachment && !uploadingBusySpinner.running
|
||||
icon.name: "mail-attachment"
|
||||
text: i18n("Attach an image or file")
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
if (Clipboard.hasImage) {
|
||||
attachDialog.open()
|
||||
} else {
|
||||
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
|
||||
fileDialog.chosen.connect((path) => {
|
||||
if (!path) { return }
|
||||
chatBoxHelper.attachmentPath = path;
|
||||
})
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
BusyIndicator {
|
||||
id: uploadingBusySpinner
|
||||
anchors.fill: parent
|
||||
visible: running
|
||||
running: currentRoom && currentRoom.hasFileUploading
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: emojiButton
|
||||
icon.name: "preferences-desktop-emoticons"
|
||||
text: i18n("Add an Emoji")
|
||||
display: AbstractButton.IconOnly
|
||||
checkable: true
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: sendButton
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send message")
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
chatBar.postMessage()
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: pasteAction
|
||||
shortcut: StandardKey.Paste
|
||||
onTriggered: {
|
||||
if (Clipboard.hasImage) {
|
||||
pasteImageTriggered();
|
||||
}
|
||||
activeFocusItem.paste();
|
||||
}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
width: parent.width
|
||||
//height: 80 //Math.min(implicitHeight, delegate.implicitHeight * 6)
|
||||
height: implicitHeight
|
||||
y: -height - 1
|
||||
z: 1
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
onCompleteTriggered: {
|
||||
complete()
|
||||
isCompleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
property CustomEmojiModel customEmojiModel: CustomEmojiModel {
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
|
||||
function pasteImage() {
|
||||
let localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png";
|
||||
if (!Clipboard.saveImage(localPath)) {
|
||||
return;
|
||||
}
|
||||
chatBoxHelper.attachmentPath = localPath;
|
||||
}
|
||||
|
||||
function postMessage() {
|
||||
checkForFancyEffectsReason();
|
||||
|
||||
if (chatBoxHelper.hasAttachment) {
|
||||
// send attachment but don't reset the text
|
||||
actionsHandler.postMessage("", chatBoxHelper.attachmentPath,
|
||||
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, {}, this.customEmojiModel);
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
messageSent();
|
||||
return;
|
||||
}
|
||||
|
||||
const re = /^s\/([^\/]*)\/([^\/]*)/;
|
||||
if (Config.allowQuickEdit && re.test(inputField.text)) {
|
||||
// send edited messages
|
||||
actionsHandler.postEdit(inputField.text);
|
||||
} else {
|
||||
// send normal message
|
||||
actionsHandler.postMessage(inputField.text.trim(), chatBoxHelper.attachmentPath,
|
||||
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
|
||||
}
|
||||
currentRoom.markAllMessagesAsRead();
|
||||
inputField.clear();
|
||||
inputField.text = Qt.binding(function() {
|
||||
return currentRoom ? currentRoom.cachedInput : "";
|
||||
});
|
||||
messageSent()
|
||||
}
|
||||
|
||||
function complete() {
|
||||
documentHandler.replaceAutoComplete(completionMenu.currentDisplayText);
|
||||
if (completionMenu.completionType === ChatDocumentHandler.User
|
||||
&& completionMenu.currentDisplayText.length > 0
|
||||
&& completionMenu.currentItem.userId.length > 0) {
|
||||
userAutocompleted[completionMenu.currentDisplayText] = completionMenu.currentItem.userId;
|
||||
}
|
||||
}
|
||||
}
|
||||
283
imports/NeoChat/Component/ChatBox/ChatBox.qml
Normal file
@@ -0,0 +1,283 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import Qt.labs.platform 1.1 as Platform
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component.ChatBox 1.0
|
||||
import NeoChat.Component.Emoji 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property alias inputFieldText: chatBar.inputFieldText
|
||||
|
||||
signal fancyEffectsReasonFound(string fancyEffect)
|
||||
signal messageSent()
|
||||
signal editLastUserMessage()
|
||||
signal replyPreviousUserMessage()
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
implicitWidth: {
|
||||
let w = 0
|
||||
for(let i = 0; i < visibleChildren.length; ++i) {
|
||||
w = Math.max(w, Math.ceil(visibleChildren[i].implicitWidth))
|
||||
}
|
||||
return w
|
||||
}
|
||||
implicitHeight: {
|
||||
let h = 0
|
||||
for(let i = 0; i < visibleChildren.length; ++i) {
|
||||
h += Math.ceil(visibleChildren[i].implicitHeight)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// For some reason, this is needed to make the height animation work even though
|
||||
// it used to work and height should be directly affected by implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
id: connectionPaneSeparator
|
||||
visible: connectionPane.visible
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: connectionPane.top
|
||||
z: 1
|
||||
}
|
||||
|
||||
QQC2.Pane {
|
||||
id: connectionPane
|
||||
padding: fontMetrics.lineSpacing * 0.25
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
font: networkLabel.font
|
||||
}
|
||||
spacing: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
visible: !Controller.isOnline
|
||||
width: parent.width
|
||||
QQC2.Label {
|
||||
id: networkLabel
|
||||
text: i18n("NeoChat is offline. Please check your network connection.")
|
||||
}
|
||||
anchors.bottom: emojiPickerLoaderSeparator.top
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
id: emojiPickerLoaderSeparator
|
||||
visible: emojiPickerLoader.visible
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: emojiPickerLoader.top
|
||||
z: 1
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: emojiPickerLoader
|
||||
active: visible
|
||||
visible: chatBar.emojiPaneOpened
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: replySeparator.top
|
||||
sourceComponent: QQC2.Pane {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
rightPadding: 0
|
||||
leftPadding: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
contentItem: EmojiPicker {
|
||||
textArea: chatBar.textField
|
||||
onChosen: addText(emoji)
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
id: replySeparator
|
||||
visible: replyPane.visible
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: replyPane.top
|
||||
}
|
||||
|
||||
|
||||
ReplyPane {
|
||||
id: replyPane
|
||||
visible: chatBoxHelper.isReplying || chatBoxHelper.isEditing
|
||||
user: chatBoxHelper.replyUser
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: attachmentSeparator.top
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
onReplyCancelled: {
|
||||
root.focusInputField()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
id: attachmentSeparator
|
||||
visible: attachmentPane.visible
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: attachmentPane.top
|
||||
}
|
||||
|
||||
AttachmentPane {
|
||||
id: attachmentPane
|
||||
visible: chatBoxHelper.hasAttachment
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: chatBarSeparator.top
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
id: chatBarSeparator
|
||||
visible: chatBar.visible
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: chatBar.top
|
||||
}
|
||||
|
||||
ChatBar {
|
||||
id: chatBar
|
||||
visible: currentRoom.canSendEvent("m.room.message")
|
||||
width: parent.width
|
||||
height: visible ? implicitHeight : 0
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
onCloseAllTriggered: closeAll()
|
||||
onMessageSent: {
|
||||
closeAll()
|
||||
checkForFancyEffectsReason()
|
||||
root.messageSent();
|
||||
}
|
||||
onEditLastUserMessage: {
|
||||
root.editLastUserMessage();
|
||||
}
|
||||
onReplyPreviousUserMessage: {
|
||||
root.replyPreviousUserMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function checkForFancyEffectsReason() {
|
||||
if (!Config.showFancyEffects) {
|
||||
return
|
||||
}
|
||||
|
||||
let text = root.inputFieldText.trim()
|
||||
if (text.includes('\u{2744}')) {
|
||||
root.fancyEffectsReasonFound("snowflake")
|
||||
}
|
||||
if (text.includes('\u{1F386}')) {
|
||||
root.fancyEffectsReasonFound("fireworks")
|
||||
}
|
||||
if (text.includes('\u{1F387}')) {
|
||||
root.fancyEffectsReasonFound("fireworks")
|
||||
}
|
||||
if (text.includes('\u{1F389}')) {
|
||||
root.fancyEffectsReasonFound("confetti")
|
||||
}
|
||||
if (text.includes('\u{1F38A}')) {
|
||||
root.fancyEffectsReasonFound("confetti")
|
||||
}
|
||||
}
|
||||
|
||||
function addText(text) {
|
||||
root.inputFieldText = inputFieldText + text
|
||||
}
|
||||
|
||||
function insertText(str) {
|
||||
root.inputFieldText = inputFieldText.substr(0, inputField.cursorPosition) + str + inputFieldText.substr(inputField.cursorPosition)
|
||||
}
|
||||
|
||||
function focusInputField() {
|
||||
chatBar.inputFieldForceActiveFocusTriggered()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
chatBar.userAutocompleted = {};
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatBoxHelper
|
||||
|
||||
function onShouldClearText() {
|
||||
root.inputFieldText = "";
|
||||
}
|
||||
|
||||
function onEditing(editContent, editFormatedContent) {
|
||||
// Set the input field in edit mode
|
||||
root.inputFieldText = editContent;
|
||||
|
||||
// clean autocompletion list
|
||||
chatBar.userAutocompleted = {};
|
||||
|
||||
// Fill autocompletion list with values extracted from message.
|
||||
// We can't just iterate on every user in the list and try to
|
||||
// find matching display name since some users have display name
|
||||
// matching frequent words and this will marks too many words as
|
||||
// mentions.
|
||||
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
|
||||
chatBar.userAutocompleted[match[2]] = match[1];
|
||||
}
|
||||
chatBox.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
chatBoxHelper.clear();
|
||||
chatBar.emojiPaneOpened = false;
|
||||
}
|
||||
}
|
||||
163
imports/NeoChat/Component/ChatBox/CompletionMenu.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Popup {
|
||||
id: control
|
||||
|
||||
// Expose internal ListView properties.
|
||||
property alias model: completionListView.model
|
||||
property alias listView: completionListView
|
||||
property alias currentIndex: completionListView.currentIndex
|
||||
property alias currentItem: completionListView.currentItem
|
||||
property alias count: completionListView.count
|
||||
property alias delegate: completionListView.delegate
|
||||
|
||||
// Autocomplee text
|
||||
property string currentDisplayText: currentItem && (currentItem.displayName ?? "")
|
||||
|
||||
property int completionType: ChatDocumentHandler.Emoji
|
||||
property int beginPosition: 0
|
||||
property int endPosition: 0
|
||||
|
||||
signal completeTriggered()
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
clip: true
|
||||
|
||||
onVisibleChanged: if (!visible) {
|
||||
completionListView.currentIndex = 0;
|
||||
}
|
||||
|
||||
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
|
||||
|
||||
contentItem: ScrollView {
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ListView {
|
||||
id: completionListView
|
||||
implicitWidth: contentWidth
|
||||
delegate: {
|
||||
if (completionType === ChatDocumentHandler.Emoji) {
|
||||
emojiDelegate
|
||||
} else if (completionType === ChatDocumentHandler.Command) {
|
||||
commandDelegate
|
||||
} else if (completionType === ChatDocumentHandler.User) {
|
||||
usernameDelegate
|
||||
}
|
||||
}
|
||||
|
||||
keyNavigationWraps: true
|
||||
|
||||
//interactive: Window.window ? contentHeight + control.topPadding + control.bottomPadding > Window.window.height : false
|
||||
clip: true
|
||||
currentIndex: control.currentIndex || 0
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
|
||||
Component {
|
||||
id: usernameDelegate
|
||||
Kirigami.BasicListItem {
|
||||
id: usernameItem
|
||||
width: ListView.view.width ?? implicitWidth
|
||||
property string displayName: modelData.displayName
|
||||
property string userId: modelData.id
|
||||
leading: Kirigami.Avatar {
|
||||
implicitHeight: Kirigami.Units.gridUnit
|
||||
implicitWidth: implicitHeight
|
||||
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
||||
color: modelData.color ? Qt.darker(modelData.color, 1.1) : null
|
||||
}
|
||||
labelItem.textFormat: Text.PlainText
|
||||
text: modelData.displayName
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emojiDelegate
|
||||
Kirigami.BasicListItem {
|
||||
id: emojiItem
|
||||
width: ListView.view.width ?? implicitWidth
|
||||
property string displayName: modelData.isCustom ? modelData.shortname : modelData.unicode
|
||||
text: modelData.shortname
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
leading: Image {
|
||||
source: modelData.isCustom ? modelData.unicode : ""
|
||||
|
||||
width: height
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: ShimmerGradient { }
|
||||
}
|
||||
|
||||
Label {
|
||||
id: unicodeLabel
|
||||
|
||||
visible: !modelData.isCustom
|
||||
|
||||
font.family: 'emoji'
|
||||
font.pixelSize: height - 2
|
||||
|
||||
text: modelData.unicode
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: commandDelegate
|
||||
Kirigami.BasicListItem {
|
||||
id: commandItem
|
||||
width: ListView.view.width ?? implicitWidth
|
||||
text: "<i>" + modelData.parameter.replace("<", "<").replace(">", ">") + "</i> " + modelData.help
|
||||
property string displayName: modelData.command
|
||||
|
||||
leading: Label {
|
||||
id: commandLabel
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit
|
||||
Layout.preferredWidth: textMetrics.tightBoundingRect.width
|
||||
text: modelData.command
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: modelData.command
|
||||
font: commandLabel.font
|
||||
}
|
||||
onClicked: completeTriggered();
|
||||
}
|
||||
}
|
||||
}
|
||||
121
imports/NeoChat/Component/ChatBox/ReplyPane.qml
Normal file
@@ -0,0 +1,121 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
readonly property bool isEdit: chatBoxHelper.isEditing
|
||||
property var user: null
|
||||
property string avatarMediaUrl: user ? "image://mxc/" + user.avatarMediaId : ""
|
||||
|
||||
signal replyCancelled()
|
||||
|
||||
active: visible
|
||||
sourceComponent: Pane {
|
||||
id: replyPane
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
spacing: leftPadding
|
||||
|
||||
contentItem: RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: replyPane.spacing
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
font: textArea.font
|
||||
}
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: avatar
|
||||
Layout.alignment: textContentLayout.height > avatar.height ? Qt.AlignHCenter | Qt.AlignTop : Qt.AlignCenter
|
||||
Layout.preferredWidth: Layout.preferredHeight
|
||||
Layout.preferredHeight: fontMetrics.lineSpacing * 2 - fontMetrics.leading
|
||||
source: root.avatarMediaUrl
|
||||
name: user ? user.displayName : ""
|
||||
color: user ? user.color : "transparent"
|
||||
visible: Boolean(user)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: textContentLayout
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: fontMetrics.leading
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
textFormat: Text.StyledText
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
let heading = "<b>%1</b>"
|
||||
let userName = user ? "<font color=\""+ user.color +"\">" + currentRoom.htmlSafeMemberName(user.id) + "</font>" : ""
|
||||
if (isEdit) {
|
||||
heading = heading.arg(i18n("Editing message:")) + "<br/>"
|
||||
} else {
|
||||
heading = heading.arg(i18n("Replying to %1:", userName))
|
||||
}
|
||||
|
||||
return heading
|
||||
}
|
||||
}
|
||||
ScrollView {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
TextArea {
|
||||
id: textArea
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
text: {
|
||||
const stylesheet = "<style> a{color:"+Kirigami.Theme.linkColor+";}.user-pill{}</style>";
|
||||
const content = chatBoxHelper.isReplying ? chatBoxHelper.replyEventContent : chatBoxHelper.editContent;
|
||||
return stylesheet + content;
|
||||
}
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
readOnly: true
|
||||
wrapMode: Label.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
background: Item {}
|
||||
HoverHandler {
|
||||
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: cancelReplyButton
|
||||
Layout.alignment: avatar.Layout.alignment
|
||||
icon.name: "dialog-cancel"
|
||||
text: i18n("Cancel")
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
chatBoxHelper.clear();
|
||||
root.replyCancelled();
|
||||
}
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
7
imports/NeoChat/Component/ChatBox/qmldir
Normal file
@@ -0,0 +1,7 @@
|
||||
module NeoChat.Component.ChatBox
|
||||
ChatBox 1.0 ChatBox.qml
|
||||
ChatBar 1.0 ChatBar.qml
|
||||
ReplyPane 1.0 ReplyPane.qml
|
||||
AttachmentPane 1.0 AttachmentPane.qml
|
||||
CompletionMenu 1.0 CompletionMenu.qml
|
||||
EmojiPickerPane 1.0 EmojiPickerPane.qml
|
||||
169
imports/NeoChat/Component/Emoji/EmojiPicker.qml
Normal file
@@ -0,0 +1,169 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0 as NeoChat
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: _picker
|
||||
|
||||
property string emojiCategory: "history"
|
||||
property var textArea
|
||||
readonly property var emojiModel: NeoChat.EmojiModel
|
||||
|
||||
property NeoChat.CustomEmojiModel customModel: NeoChat.CustomEmojiModel {
|
||||
connection: NeoChat.Controller.activeConnection
|
||||
}
|
||||
|
||||
signal chosen(string emoji)
|
||||
|
||||
spacing: 0
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + 2 // for the focus line
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
clip: true
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
|
||||
model: ListModel {
|
||||
ListElement { label: "custom"; category: "custom" }
|
||||
ListElement { label: "⌛️"; category: "history" }
|
||||
ListElement { label: "😏"; category: "people" }
|
||||
ListElement { label: "🌲"; category: "nature" }
|
||||
ListElement { label: "🍛"; category: "food"}
|
||||
ListElement { label: "🚁"; category: "activity" }
|
||||
ListElement { label: "🚅"; category: "travel" }
|
||||
ListElement { label: "💡"; category: "objects" }
|
||||
ListElement { label: "🔣"; category: "symbols" }
|
||||
ListElement { label: "🏁"; category: "flags" }
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: del
|
||||
|
||||
required property string label
|
||||
required property string category
|
||||
|
||||
width: contentItem.Layout.preferredWidth
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: del.label === "custom" ? 4 : 1
|
||||
|
||||
Layout.preferredWidth: del.label === "custom" ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2
|
||||
|
||||
font.family: del.label === "custom" ? Kirigami.Theme.defaultFont.family : 'emoji'
|
||||
text: del.label === "custom" ? i18n("Custom") : del.label
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
width: parent.width
|
||||
height: 2
|
||||
|
||||
visible: emojiCategory === category
|
||||
|
||||
color: Kirigami.Theme.focusColor
|
||||
}
|
||||
|
||||
onClicked: emojiCategory = category
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
}
|
||||
|
||||
GridView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 8
|
||||
Layout.fillHeight: true
|
||||
|
||||
cellWidth: Kirigami.Units.gridUnit * 2
|
||||
cellHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
boundsBehavior: Flickable.DragOverBounds
|
||||
|
||||
clip: true
|
||||
|
||||
model: {
|
||||
switch (emojiCategory) {
|
||||
case "custom":
|
||||
return _picker.customModel
|
||||
case "history":
|
||||
return emojiModel.history
|
||||
case "people":
|
||||
return emojiModel.people
|
||||
case "nature":
|
||||
return emojiModel.nature
|
||||
case "food":
|
||||
return emojiModel.food
|
||||
case "activity":
|
||||
return emojiModel.activity
|
||||
case "travel":
|
||||
return emojiModel.travel
|
||||
case "objects":
|
||||
return emojiModel.objects
|
||||
case "symbols":
|
||||
return emojiModel.symbols
|
||||
case "flags":
|
||||
return emojiModel.flags
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: Kirigami.Units.gridUnit * 2
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
|
||||
contentItem: Kirigami.Heading {
|
||||
level: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: 'emoji'
|
||||
text: modelData.isCustom ? "" : modelData.unicode
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: modelData.isCustom
|
||||
source: visible ? modelData.unicode : ""
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: parent.status === Image.Loading
|
||||
radius: height/2
|
||||
gradient: ShimmerGradient { }
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (modelData.isCustom) {
|
||||
chosen(modelData.shortname)
|
||||
} else {
|
||||
chosen(modelData.unicode)
|
||||
}
|
||||
emojiModel.emojiUsed(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
}
|
||||
2
imports/NeoChat/Component/Emoji/qmldir
Normal file
@@ -0,0 +1,2 @@
|
||||
module NeoChat.Component.Emoji
|
||||
EmojiPicker 1.0 EmojiPicker.qml
|
||||
296
imports/NeoChat/Component/FancyEffectsContainer.qml
Normal file
@@ -0,0 +1,296 @@
|
||||
// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Particles 2.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Item {
|
||||
id: item
|
||||
property bool enabled: false
|
||||
property int effectInterval: Kirigami.Units.veryLongDuration*10;
|
||||
property color darkSnowColor: "grey"
|
||||
property bool isThemeDark: Kirigami.Theme.backgroundColor.hslLightness <= darkSnowColor.hslLightness
|
||||
|
||||
function showConfettiEffect() {
|
||||
confettiTimer.start()
|
||||
}
|
||||
|
||||
function showSnowEffect() {
|
||||
snowTimer.start()
|
||||
}
|
||||
|
||||
function showFireworksEffect() {
|
||||
fireworksTimer.start()
|
||||
}
|
||||
|
||||
// Confetti
|
||||
|
||||
Timer {
|
||||
id: confettiTimer
|
||||
interval: item.effectInterval;
|
||||
running: false;
|
||||
repeat: false;
|
||||
triggeredOnStart: true;
|
||||
onTriggered: {
|
||||
if (item.enabled) {
|
||||
confettiSystem.running = !confettiSystem.running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParticleSystem {
|
||||
id: confettiSystem
|
||||
anchors.fill: parent
|
||||
|
||||
running: false
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
opacity = 1
|
||||
} else {
|
||||
opacity = 0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration }
|
||||
}
|
||||
}
|
||||
|
||||
ImageParticle {
|
||||
source: "qrc:/imports/NeoChat/Component/confetti.png"
|
||||
entryEffect: ImageParticle.Scale
|
||||
rotationVariation: 360
|
||||
rotationVelocity: 90
|
||||
color: Qt.hsla(Math.random(), 0.5, 0.6, 1)
|
||||
colorVariation: 1
|
||||
}
|
||||
|
||||
Emitter {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
sizeVariation: Kirigami.Units.iconSizes.small/2
|
||||
lifeSpan: Kirigami.Units.veryLongDuration*10
|
||||
size: Kirigami.Units.iconSizes.small
|
||||
|
||||
velocity: AngleDirection {
|
||||
angle: 90
|
||||
angleVariation: 42
|
||||
magnitude: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snow
|
||||
|
||||
Timer {
|
||||
id: snowTimer
|
||||
interval: item.effectInterval;
|
||||
running: false;
|
||||
repeat: false;
|
||||
triggeredOnStart: true;
|
||||
onTriggered: {
|
||||
if (item.enabled) {
|
||||
snowSystem.running = !snowSystem.running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParticleSystem {
|
||||
id: snowSystem
|
||||
anchors.fill: parent
|
||||
|
||||
running: false
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
opacity = 1
|
||||
} else {
|
||||
opacity = 0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration }
|
||||
}
|
||||
}
|
||||
|
||||
ItemParticle {
|
||||
delegate: Rectangle {
|
||||
width: 10
|
||||
height: width
|
||||
radius: width
|
||||
color: item.isThemeDark ? "white" : darkSnowColor
|
||||
scale: Math.random()
|
||||
opacity: Math.random()
|
||||
}
|
||||
}
|
||||
|
||||
Emitter {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
sizeVariation: Kirigami.Units.iconSizes.medium
|
||||
lifeSpan: Kirigami.Units.veryLongDuration*10
|
||||
size: Kirigami.Units.iconSizes.large
|
||||
emitRate: 42
|
||||
|
||||
velocity: AngleDirection {
|
||||
angle: 90
|
||||
angleVariation: 10
|
||||
magnitude: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fireworks
|
||||
|
||||
Timer {
|
||||
id: fireworksTimer
|
||||
interval: item.effectInterval;
|
||||
running: false;
|
||||
repeat: false;
|
||||
triggeredOnStart: true;
|
||||
onTriggered: {
|
||||
if (item.enabled) {
|
||||
fireworksInternalTimer.running = !fireworksInternalTimer.running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fireworksInternalTimer
|
||||
interval: 300
|
||||
triggeredOnStart: true
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
var x = Math.random() * parent.width
|
||||
var y = Math.random() * parent.height
|
||||
customEmit(x, y)
|
||||
customEmit(x, y)
|
||||
customEmit(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
ParticleSystem {
|
||||
id: fireworksSystem
|
||||
anchors.fill: parent
|
||||
running: fireworksInternalTimer.running
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
opacity = 1
|
||||
} else {
|
||||
opacity = 0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageParticle {
|
||||
id: fireworksParticleA
|
||||
system: fireworksSystem
|
||||
source: "qrc:/imports/NeoChat/Component/glowdot.png"
|
||||
alphaVariation: item.isThemeDark ? 0.1 : 0.1
|
||||
alpha: item.isThemeDark ? 0.5 : 1
|
||||
groups: ["a"]
|
||||
opacity: fireworksSystem.opacity
|
||||
entryEffect: ImageParticle.Scale
|
||||
rotationVariation: 360
|
||||
}
|
||||
|
||||
ImageParticle {
|
||||
system: fireworksSystem
|
||||
source: "qrc:/imports/NeoChat/Component/glowdot.png"
|
||||
color: item.isThemeDark ? "white" : "gold"
|
||||
alphaVariation: item.isThemeDark ? 0.1 : 0.1
|
||||
alpha: item.isThemeDark ? 0.5 : 1
|
||||
groups: ["light"]
|
||||
opacity: fireworksSystem.opacity
|
||||
entryEffect: ImageParticle.Scale
|
||||
rotationVariation: 360
|
||||
}
|
||||
|
||||
ImageParticle {
|
||||
id: fireworksParticleB
|
||||
system: fireworksSystem
|
||||
source: "qrc:/imports/NeoChat/Component/glowdot.png"
|
||||
alphaVariation: item.isThemeDark ? 0.1 : 0.1
|
||||
alpha: item.isThemeDark ? 0.5 : 1
|
||||
groups: ["b"]
|
||||
opacity: fireworksSystem.opacity
|
||||
entryEffect: ImageParticle.Scale
|
||||
rotationVariation: 360
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emitterComp
|
||||
Emitter {
|
||||
id: container
|
||||
property int life: 23
|
||||
property real targetX: 0
|
||||
property real targetY: 0
|
||||
width: 1
|
||||
height: 1
|
||||
system: fireworksSystem
|
||||
size: 16
|
||||
endSize: 8
|
||||
sizeVariation: 5
|
||||
Timer {
|
||||
interval: life
|
||||
running: true
|
||||
onTriggered: {
|
||||
container.destroy();
|
||||
var randomHue = Math.random()
|
||||
var lightness = item.isThemeDark ? 0.8 : 0.7
|
||||
fireworksParticleA.color = Qt.hsla(randomHue, 0.8, lightness, 1)
|
||||
fireworksParticleB.color = Qt.hsla(1-randomHue, 0.8, lightness, 1)
|
||||
}
|
||||
}
|
||||
velocity: AngleDirection {angleVariation:360; magnitude: 200}
|
||||
}
|
||||
}
|
||||
|
||||
function customEmit(x,y) {
|
||||
var currentSize = Math.round(Math.random() * 200) + 40
|
||||
var currentLifeSpan = Math.round(Math.random() * 1000) + 100
|
||||
for (var i=0; i<8; i++) {
|
||||
var obj = emitterComp.createObject(parent);
|
||||
obj.x = x
|
||||
obj.y = y
|
||||
obj.targetX = Math.random() * currentSize - currentSize/2 + obj.x
|
||||
obj.targetY = Math.random() * currentSize - currentSize/2 + obj.y
|
||||
obj.life = Math.round(Math.random() * 23) + 150
|
||||
obj.emitRate = Math.round(Math.random() * 32) + 5
|
||||
obj.lifeSpan = currentLifeSpan
|
||||
const group = Math.round(Math.random() * 3);
|
||||
switch (group) {
|
||||
case 0:
|
||||
obj.group = "light";
|
||||
break;
|
||||
case 1:
|
||||
obj.group = "a";
|
||||
break;
|
||||
case 2:
|
||||
obj.group = "b";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
308
imports/NeoChat/Component/FullScreenImage.qml
Normal file
@@ -0,0 +1,308 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property alias source: image.source
|
||||
property string filename
|
||||
property string blurhash: ""
|
||||
property int imageWidth: -1
|
||||
property int imageHeight: -1
|
||||
property var modelData
|
||||
|
||||
parent: Overlay.overlay
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
modal: true
|
||||
padding: 0
|
||||
background: null
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Control {
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Avatar {
|
||||
id: avatar
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
|
||||
name: modelData.author.name ?? modelData.author.displayName
|
||||
source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
|
||||
color: modelData.author.color
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
id: nameLabel
|
||||
|
||||
text: modelData.author.displayName
|
||||
textFormat: Text.PlainText
|
||||
font.weight: Font.Bold
|
||||
color: author.color
|
||||
}
|
||||
Label {
|
||||
id: timeLabel
|
||||
|
||||
text: time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
}
|
||||
Label {
|
||||
id: imageLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
text: modelData.display
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Zoom in")
|
||||
Accessible.name: text
|
||||
icon.name: "zoom-in"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
image.scaleFactor = image.scaleFactor + 0.25
|
||||
if (image.scaleFactor > 3) {
|
||||
image.scaleFactor = 3
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Zoom out")
|
||||
Accessible.name: text
|
||||
icon.name: "zoom-out"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
image.scaleFactor = image.scaleFactor - 0.25
|
||||
if (image.scaleFactor < 0.25) {
|
||||
image.scaleFactor = 0.25
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Rotate left")
|
||||
Accessible.name: text
|
||||
icon.name: "image-rotate-left-symbolic"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: image.rotationAngle = image.rotationAngle - 90
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Rotate right")
|
||||
Accessible.name: text
|
||||
icon.name: "image-rotate-right-symbolic"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: image.rotationAngle = image.rotationAngle + 90
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Save as")
|
||||
Accessible.name: text
|
||||
icon.name: "document-save"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
var dialog = saveAsDialog.createObject(ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
ToolButton {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
|
||||
text: i18n("Close")
|
||||
Accessible.name: text
|
||||
icon.name: "dialog-close"
|
||||
display: AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
Layout.fillWidth: true
|
||||
visible: image.status !== Image.Ready && root.blurhash === ""
|
||||
running: visible
|
||||
}
|
||||
// Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area.
|
||||
Item {
|
||||
id: imageContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: image
|
||||
|
||||
property var scaleFactor: 1
|
||||
property int rotationAngle: 0
|
||||
property var rotationInsensitiveWidth: Math.min(root.imageWidth > 0 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2)
|
||||
property var rotationInsensitiveHeight: Math.min(root.imageHeight > 0 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2)
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
|
||||
height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
|
||||
fillMode: Image.PreserveAspectFit
|
||||
clip: true
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: image.width
|
||||
height: image.height
|
||||
source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : ""
|
||||
visible: root.blurhash !== "" && parent.status !== Image.Ready
|
||||
}
|
||||
|
||||
transform: [
|
||||
Rotation {
|
||||
origin.x: image.width / 2
|
||||
origin.y: image.height / 2
|
||||
angle: image.rotationAngle
|
||||
|
||||
Behavior on angle {
|
||||
RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
},
|
||||
Scale {
|
||||
origin.x: image.width / 2
|
||||
origin.y: image.height / 2
|
||||
xScale: image.scaleFactor
|
||||
yScale: image.scaleFactor
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
Behavior on yScale {
|
||||
NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
const contextMenu = fileDelegateContextMenu.createObject(parent, {
|
||||
author: modelData.author,
|
||||
message: modelData.message,
|
||||
eventId: modelData.eventId,
|
||||
source: modelData.source,
|
||||
file: root.parent,
|
||||
mimeType: modelData.mimeType,
|
||||
progressInfo: modelData.progressInfo,
|
||||
plainMessage: modelData.message,
|
||||
});
|
||||
contextMenu.closeFullscreen.connect(root.close)
|
||||
contextMenu.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: saveAsDialog
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
if (!currentFile) {
|
||||
return;
|
||||
}
|
||||
currentRoom.downloadFile(eventId, currentFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
image.scaleFactor = 1
|
||||
image.rotationAngle = 0
|
||||
}
|
||||
}
|
||||
61
imports/NeoChat/Component/Login/Homeserver.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
LoginStep {
|
||||
id: root
|
||||
|
||||
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
|
||||
property bool loading: false
|
||||
|
||||
title: i18nc("@title", "Select a Homeserver")
|
||||
|
||||
action: Kirigami.Action {
|
||||
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
|
||||
onTriggered: {
|
||||
// TODO
|
||||
console.log("register todo")
|
||||
}
|
||||
}
|
||||
|
||||
onHomeserverChanged: {
|
||||
LoginHelper.testHomeserver("@user:" + homeserver)
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Component.onCompleted: Controller.testHomeserver(homeserver)
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: serverCombo
|
||||
|
||||
Kirigami.FormData.label: i18n("Homeserver:")
|
||||
model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")]
|
||||
}
|
||||
|
||||
QQC2.TextField {
|
||||
id: customHomeserver
|
||||
|
||||
Kirigami.FormData.label: i18n("Url:")
|
||||
visible: serverCombo.currentIndex === 3
|
||||
onTextChanged: {
|
||||
Controller.testHomeserver(text)
|
||||
}
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: continueButton
|
||||
text: i18nc("@action:button", "Continue")
|
||||
action: root.action
|
||||
}
|
||||
}
|
||||
}
|
||||
28
imports/NeoChat/Component/Login/Loading.qml
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
property var showContinueButton: false
|
||||
property var showBackButton: false
|
||||
property string title: i18n("Loading…")
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
QQC2.Label {
|
||||
text: i18n("Please wait. This might take a little while.")
|
||||
}
|
||||
|
||||
QQC2.BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: false
|
||||
}
|
||||
}
|
||||
65
imports/NeoChat/Component/Login/Login.qml
Normal file
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
LoginStep {
|
||||
id: login
|
||||
|
||||
showContinueButton: true
|
||||
showBackButton: false
|
||||
|
||||
title: i18nc("@title", "Login")
|
||||
message: i18n("Enter your Matrix ID")
|
||||
|
||||
Component.onCompleted: {
|
||||
LoginHelper.matrixId = ""
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
QQC2.TextField {
|
||||
id: matrixIdField
|
||||
Kirigami.FormData.label: i18n("Matrix ID:")
|
||||
placeholderText: "@user:matrix.org"
|
||||
onTextChanged: {
|
||||
if(acceptableInput) {
|
||||
LoginHelper.matrixId = text
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
matrixIdField.forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
login.action.trigger()
|
||||
}
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(\:[0-9]+)?$/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading…") : i18nc("@action:button", "Continue")
|
||||
onTriggered: {
|
||||
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
|
||||
} else if (LoginHelper.supportsPassword) {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/Password.qml");
|
||||
} else {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/Sso.qml");
|
||||
}
|
||||
}
|
||||
enabled: LoginHelper.homeserverReachable
|
||||
}
|
||||
}
|
||||
32
imports/NeoChat/Component/Login/LoginMethod.qml
Normal file
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component.Login 1.0
|
||||
|
||||
LoginStep {
|
||||
id: loginMethod
|
||||
|
||||
title: i18n("Login Methods")
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: i18n("Login with password")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
|
||||
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml")
|
||||
}
|
||||
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: i18n("Login with single sign-on")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
|
||||
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml")
|
||||
}
|
||||
}
|
||||
31
imports/NeoChat/Component/Login/LoginRegister.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component.Login 1.0
|
||||
|
||||
LoginStep {
|
||||
id: loginRegister
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: i18n("Login")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
|
||||
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml")
|
||||
}
|
||||
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: i18n("Register")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
|
||||
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml")
|
||||
}
|
||||
}
|
||||
27
imports/NeoChat/Component/Login/LoginStep.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
/// Step for the login/registration flow
|
||||
ColumnLayout {
|
||||
|
||||
property string title: i18n("Welcome")
|
||||
property string message: i18n("Welcome")
|
||||
property bool showContinueButton: false
|
||||
property bool showBackButton: false
|
||||
property bool acceptable: false
|
||||
property string previousUrl: ""
|
||||
|
||||
/// Process this module, this is called by the continue button.
|
||||
/// Should call \sa processed when it finish successfully.
|
||||
property Action action: null
|
||||
|
||||
/// Called when switching to the next step.
|
||||
signal processed(url nextUrl)
|
||||
|
||||
signal showMessage(string message)
|
||||
|
||||
}
|
||||
52
imports/NeoChat/Component/Login/Password.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
LoginStep {
|
||||
id: password
|
||||
|
||||
title: i18nc("@title", "Password")
|
||||
message: i18n("Enter your password")
|
||||
showContinueButton: true
|
||||
showBackButton: true
|
||||
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml"
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Login")
|
||||
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
|
||||
onTriggered: {
|
||||
LoginHelper.login();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onConnected() {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Kirigami.PasswordField {
|
||||
id: passwordField
|
||||
onTextChanged: LoginHelper.password = text
|
||||
enabled: !LoginHelper.isLoggingIn
|
||||
|
||||
Component.onCompleted: {
|
||||
passwordField.forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
password.action.trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
imports/NeoChat/Component/Login/Sso.qml
Normal file
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
|
||||
LoginStep {
|
||||
id: root
|
||||
|
||||
title: i18nc("@title", "Login")
|
||||
message: i18n("Login with single sign-on")
|
||||
|
||||
Kirigami.FormLayout {
|
||||
Connections {
|
||||
target: LoginHelper
|
||||
function onSsoUrlChanged() {
|
||||
UrlHelper.openUrl(LoginHelper.ssoUrl)
|
||||
}
|
||||
function onConnected() {
|
||||
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button", "Back")
|
||||
|
||||
onClicked: {
|
||||
module.source = "qrc:/imports/NeoChat/Component/Login/Login.qml"
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
text: i18n("Login")
|
||||
onClicked: {
|
||||
LoginHelper.loginWithSso()
|
||||
root.showMessage(i18n("Complete the authentication steps in your browser"))
|
||||
}
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
Keys.onReturnPressed: clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
imports/NeoChat/Component/Login/qmldir
Normal file
@@ -0,0 +1,7 @@
|
||||
module NeoChat.Component.Login
|
||||
Login 1.0 Login.qml
|
||||
Password 1.0 Password.qml
|
||||
LoginRegister 1.0 LoginRegister.qml
|
||||
Loading 1.0 Loading.qml
|
||||
LoginMethod 1.0 LoginMethod.qml
|
||||
LoginStep 1.0 LoginStep.qml
|
||||
108
imports/NeoChat/Component/QuickSwitcher.qml
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.10
|
||||
import QtQuick.Controls 2.12 as QQC2
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
import org.kde.kitemmodels 1.0
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
QQC2.Popup {
|
||||
id: _popup
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
enabled: !Kirigami.Settings.hasPlatformMenuBar
|
||||
onActivated: _popup.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
quickSearch.forceActiveFocus()
|
||||
quickSearch.text = ""
|
||||
}
|
||||
|
||||
anchors.centerIn: QQC2.Overlay.overlay
|
||||
background: Kirigami.Card {}
|
||||
height: 2 * Math.round(implicitHeight / 2)
|
||||
padding: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
Kirigami.SearchField {
|
||||
id: quickSearch
|
||||
|
||||
// TODO: get this broken property removed/disabled by default in Kirigami,
|
||||
// we used to be able to expect that the text field wouldn't attempt to
|
||||
// perform a mini-DDOS attack using signals.
|
||||
autoAccept: false
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 21 // 3 * 7 = 21, roughly 7 avatars on screen
|
||||
Keys.onLeftPressed: cView.decrementCurrentIndex()
|
||||
Keys.onRightPressed: cView.incrementCurrentIndex()
|
||||
onAccepted: {
|
||||
const item = cView.itemAtIndex(cView.currentIndex)
|
||||
|
||||
RoomManager.enterRoom(item.currentRoom)
|
||||
|
||||
_popup.close()
|
||||
}
|
||||
}
|
||||
ListView {
|
||||
id: cView
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
model: SortFilterRoomListModel {
|
||||
id: sortFilterRoomListModel
|
||||
sourceModel: RoomListModel {
|
||||
id: roomListModel
|
||||
connection: Controller.activeConnection
|
||||
}
|
||||
filterText: quickSearch.text
|
||||
roomSortOrder: SortFilterRoomListModel.LastActivity
|
||||
}
|
||||
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: Kirigami.Avatar {
|
||||
id: del
|
||||
|
||||
implicitHeight: Kirigami.Units.gridUnit * 3
|
||||
implicitWidth: Kirigami.Units.gridUnit * 3
|
||||
|
||||
required property string avatar
|
||||
required property var currentRoom
|
||||
required property int index
|
||||
|
||||
// When an item is hovered set the currentIndex of listview to it so that it is highlighted
|
||||
onHoveredChanged: {
|
||||
if (!hovered) {
|
||||
return
|
||||
}
|
||||
cView.currentIndex = index
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
id: enterRoomAction
|
||||
onTriggered: {
|
||||
RoomManager.enterRoom(currentRoom);
|
||||
|
||||
_popup.close()
|
||||
}
|
||||
}
|
||||
|
||||
source: avatar != "" ? "image://mxc/" + avatar : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
}
|
||||
39
imports/NeoChat/Component/ShimmerGradient.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
// Not to be confused with the Shimmer project.
|
||||
// I like their gradiented GTK themes though.
|
||||
|
||||
import QtQuick 2.15
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
Gradient {
|
||||
id: gradient
|
||||
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
property color color: Kirigami.Theme.textColor
|
||||
property color translucent: Qt.rgba(color.r, color.g, color.b, 0.2)
|
||||
property color bright: Qt.rgba(color.r, color.g, color.b, 0.3)
|
||||
property real pos: 0.5
|
||||
property real offset: 0.6
|
||||
|
||||
property SequentialAnimation ani: SequentialAnimation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: -2.0
|
||||
to: 2.0
|
||||
duration: 700
|
||||
target: gradient
|
||||
properties: "pos"
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
GradientStop { position: gradient.pos-gradient.offset; color: gradient.translucent }
|
||||
GradientStop { position: gradient.pos; color: gradient.bright }
|
||||
GradientStop { position: gradient.pos+gradient.offset; color: gradient.translucent }
|
||||
}
|
||||
122
imports/NeoChat/Component/Timeline/AudioDelegate.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtMultimedia 5.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
TimelineContainer {
|
||||
id: audioDelegate
|
||||
|
||||
onReplyClicked: ListView.view.goToEvent(eventID)
|
||||
|
||||
onOpenContextMenu: openFileContext(model, audioDelegate)
|
||||
|
||||
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
|
||||
onDownloadedChanged: audio.play()
|
||||
|
||||
hoverComponent: hoverActions
|
||||
innerObject: Control {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: audioDelegate.contentMaxWidth
|
||||
|
||||
Audio {
|
||||
id: audio
|
||||
source: model.progressInfo.localPath
|
||||
autoLoad: false
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !model.progressInfo.completed && !model.progressInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: currentRoom.downloadFile(model.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: model.progressInfo.active && !model.progressInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-stop"
|
||||
onClicked: {
|
||||
currentRoom.cancelFileTransfer(model.eventId)
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: model.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
audio.play()
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: model.progressInfo.completed && audio.playbackState === Audio.PlayingState
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
|
||||
icon.name: "media-playback-pause"
|
||||
|
||||
onClicked: audio.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
RowLayout {
|
||||
ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
Label {
|
||||
text: model.display
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
ProgressBar {
|
||||
id: downloadBar
|
||||
visible: false
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: model.content.info.size
|
||||
value: model.progressInfo.progress
|
||||
}
|
||||
RowLayout {
|
||||
visible: audio.hasAudio
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Slider {
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
onMoved: audio.seek(value)
|
||||
}
|
||||
|
||||
Label {
|
||||
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
imports/NeoChat/Component/Timeline/EncryptedDelegate.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
TimelineContainer {
|
||||
id: encryptedDelegate
|
||||
|
||||
innerObject: TextEdit {
|
||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
Layout.maximumWidth: encryptedDelegate.contentMaxWidth
|
||||
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
|
||||
}
|
||||
}
|
||||
77
imports/NeoChat/Component/Timeline/EventDelegate.qml
Normal file
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
|
||||
DelegateChooser {
|
||||
role: "eventType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "state"
|
||||
delegate: StateDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "emote"
|
||||
delegate: MessageDelegate {
|
||||
isEmote: true
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "message"
|
||||
delegate: MessageDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "notice"
|
||||
delegate: MessageDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "image"
|
||||
delegate: ImageDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "sticker"
|
||||
delegate: ImageDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "audio"
|
||||
delegate: AudioDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "video"
|
||||
delegate: VideoDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "file"
|
||||
delegate: FileDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "encrypted"
|
||||
delegate: EncryptedDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "readMarker"
|
||||
delegate: ReadMarkerDelegate {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: "other"
|
||||
delegate: Item {}
|
||||
}
|
||||
}
|
||||
137
imports/NeoChat/Component/Timeline/FileDelegate.qml
Normal file
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1
|
||||
|
||||
import org.kde.kirigami 2.15 as Kirigami
|
||||
|
||||
import org.kde.neochat 1.0
|
||||
import NeoChat.Component 1.0
|
||||
import NeoChat.Dialog 1.0
|
||||
import NeoChat.Menu.Timeline 1.0
|
||||
|
||||
TimelineContainer {
|
||||
id: fileDelegate
|
||||
|
||||
onReplyClicked: ListView.view.goToEvent(eventID)
|
||||
hoverComponent: hoverActions
|
||||
|
||||
onOpenContextMenu: openFileContext(model, fileDelegate)
|
||||
|
||||
readonly property bool downloaded: progressInfo && progressInfo.completed
|
||||
|
||||
function saveFileAs() {
|
||||
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
||||
dialog.open()
|
||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (UrlHelper.openUrl(progressInfo.localPath)) return;
|
||||
if (UrlHelper.openUrl(progressInfo.localDir)) return;
|
||||
}
|
||||
|
||||
innerObject: RowLayout {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: fileDelegate.contentMaxWidth
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: progressInfo.completed
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
|
||||
icon.name: "document-open"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: progressInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: sizeLabel
|
||||
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
|
||||
}
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "media-playback-stop"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
onClicked: currentRoom.cancelFileTransfer(eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "raw"
|
||||
when: true
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
|
||||
onClicked: fileDelegate.saveFileAs()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Kirigami.Icon {
|
||||
id: ikon
|
||||
source: model.fileMimetypeIcon
|
||||
fallback: "unknown"
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
text: model.display
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
id: sizeLabel
|
||||
|
||||
text: Controller.formatByteSize(content.info ? content.info.size : 0)
|
||||
opacity: 0.7
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: downloadButton
|
||||
icon.name: "download"
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
FileDialog {
|
||||
fileMode: FileDialog.SaveFile
|
||||
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
currentRoom.downloadFile(eventId, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||