Compare commits

..

86 Commits

Author SHA1 Message Date
l10n daemon script
c7c1c8fd5c GIT_SILENT Sync po/docbooks with svn 2025-10-06 03:29:02 +00:00
l10n daemon script
fdbee5a508 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-10-05 03:06:37 +00:00
Ron El
fb58003451 Bump the min required KF version to 6.16
This became required by de97275a38
because Kirigami.ColumnView.preferredWidth did not exist before then.

Reverting that commit enables this to still build with 6.13.
2025-10-03 10:29:32 +09:30
Heiko Becker
debbe8e478 GIT_SILENT Update Appstream for new release 2025-10-01 00:40:00 +02:00
Heiko Becker
0ce86e5a08 GIT_SILENT Upgrade release service version to 25.08.2. 2025-09-30 23:22:50 +02:00
l10n daemon script
6935d887c4 GIT_SILENT Sync po/docbooks with svn 2025-09-29 03:19:32 +00:00
l10n daemon script
963346e0f4 GIT_SILENT Sync po/docbooks with svn 2025-09-28 03:33:33 +00:00
l10n daemon script
e37dd88c43 GIT_SILENT Sync po/docbooks with svn 2025-09-27 03:18:49 +00:00
Joshua Goins
2aacb640c8 Fix some miscellaneous weirdness around push notifications
For some reason, I *stopped* the timer if we get a push message - which
makes *no* sense. It meant that NeoChat kept running in the background
for basically no reason.

We can also make the NotificationsManager::postPushNotification function
static and avoid creating some singletons.

(cherry picked from commit 94ea1305b2)
2025-09-26 15:49:45 -04:00
Joshua Goins
2d63a92702 Replace KDBusService usage with QDBusConnection when D-Bus activated
This is lighter and more reliable when it comes to being activated by
KUnifiedPush. We also don't need all of the features of KDBusService
here.

(cherry picked from commit 960377838d)
2025-09-26 15:49:45 -04:00
Joshua Goins
de6731cfda Remove useless translated string from dbus-activated CLI option
It never gets shown, so let's not waste our translators time.

(cherry picked from commit a48e8662d6)
2025-09-26 15:49:45 -04:00
l10n daemon script
7b3c40757c GIT_SILENT Sync po/docbooks with svn 2025-09-25 03:21:49 +00:00
l10n daemon script
87f243ba8b GIT_SILENT Sync po/docbooks with svn 2025-09-24 03:16:10 +00:00
l10n daemon script
192cdc1ff3 GIT_SILENT Sync po/docbooks with svn 2025-09-22 03:19:05 +00:00
l10n daemon script
c72f77f7b6 GIT_SILENT Sync po/docbooks with svn 2025-09-20 03:20:16 +00:00
Vlad Zahorodnii
d978f8de50 Fix inserting UserListModel items without beginInsertRows()
If an item is added, the corresponding code should be wrapped with
beginInsertRows() and endInsertRows(), otherwise proxy or filter models
can end up with corrupted internal state.

m_members.insert() in refreshMember() should be unnecessary because
if pos is not the same as m_members.size(), then it means there's already
a member.id() item in the member list.


(cherry picked from commit 161815acff)

Co-authored-by: Vlad Zahorodnii <vlad.zahorodnii@kde.org>
2025-09-15 18:15:56 +00:00
l10n daemon script
cf216268ab GIT_SILENT Sync po/docbooks with svn 2025-09-15 03:15:28 +00:00
l10n daemon script
f9741a66c4 GIT_SILENT Sync po/docbooks with svn 2025-09-14 03:12:48 +00:00
l10n daemon script
aac3bfda88 GIT_SILENT Sync po/docbooks with svn 2025-09-12 03:13:06 +00:00
l10n daemon script
2722a6f2f0 GIT_SILENT Sync po/docbooks with svn 2025-09-10 03:14:06 +00:00
Joshua Goins
ecf4b85f00 Send thumbnails when uploading videos
NeoChat will now generate thumbnails from the first frame of the video,
since we are loading it anyway. This makes the experience of exchanging
videos in NeoChat much nicer!


(cherry picked from commit a0b3e484f5)

Co-authored-by: Joshua Goins <josh@redstrate.com>
2025-09-09 08:51:41 -04:00
Tobias Fella
ec1413d1ce Set object ownership for NeoChatRoomMembers
(cherry picked from commit 4c638a740e)

Co-authored-by: Tobias Fella <tobias.fella@kde.org>
2025-09-09 08:15:50 +00:00
l10n daemon script
0b7a6df0a3 GIT_SILENT Sync po/docbooks with svn 2025-09-09 04:29:46 +00:00
l10n daemon script
040efa46f9 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-09-09 04:19:53 +00:00
l10n daemon script
ef4b41e6f8 GIT_SILENT made messages (after extraction) 2025-09-09 03:28:55 +00:00
l10n daemon script
80f81847f4 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-09-08 04:03:30 +00:00
Heiko Becker
681a3c4036 GIT_SILENT Update Appstream for new release 2025-09-04 01:31:12 +02:00
Heiko Becker
fd27c70b85 GIT_SILENT Upgrade release service version to 25.08.1. 2025-09-04 00:09:03 +02:00
l10n daemon script
435124ffe8 GIT_SILENT Sync po/docbooks with svn 2025-09-03 03:26:05 +00:00
l10n daemon script
e2ca698389 GIT_SILENT Sync po/docbooks with svn 2025-09-02 03:42:16 +00:00
l10n daemon script
7e9cfbedc9 GIT_SILENT Sync po/docbooks with svn 2025-09-01 03:24:20 +00:00
l10n daemon script
eeed8a7277 GIT_SILENT Sync po/docbooks with svn 2025-08-27 03:27:03 +00:00
l10n daemon script
aa8c515432 GIT_SILENT Sync po/docbooks with svn 2025-08-26 03:17:13 +00:00
l10n daemon script
ba30014d40 GIT_SILENT Sync po/docbooks with svn 2025-08-23 03:21:47 +00:00
l10n daemon script
d131030d47 GIT_SILENT made messages (after extraction) 2025-08-22 02:36:59 +00:00
l10n daemon script
23a0d91627 GIT_SILENT Sync po/docbooks with svn 2025-08-21 03:29:16 +00:00
l10n daemon script
11d5a37ffe GIT_SILENT Sync po/docbooks with svn 2025-08-20 03:17:17 +00:00
l10n daemon script
256a8e5a1e GIT_SILENT Sync po/docbooks with svn 2025-08-16 03:21:53 +00:00
l10n daemon script
b608921d43 GIT_SILENT Sync po/docbooks with svn 2025-08-15 03:47:04 +00:00
l10n daemon script
45a3984bf9 GIT_SILENT Sync po/docbooks with svn 2025-08-14 03:34:47 +00:00
l10n daemon script
570e0425e9 GIT_SILENT Sync po/docbooks with svn 2025-08-13 03:19:34 +00:00
Tobias Fella
09ed1bd616 Fix copying images
(cherry picked from commit ef4f11546f)
2025-08-12 16:36:03 +02:00
Tobias Fella
3201426886 Update room versions in security settings
We need to come up with a better way of testing the versions here, but that's for different patch

(cherry picked from commit 35b363fdce)
2025-08-11 23:16:43 +02:00
l10n daemon script
96dc83d807 GIT_SILENT made messages (after extraction) 2025-08-11 02:40:56 +00:00
l10n daemon script
ab1fb8ae97 GIT_SILENT Sync po/docbooks with svn 2025-08-10 03:27:35 +00:00
l10n daemon script
c5a4b2a50a GIT_SILENT Sync po/docbooks with svn 2025-08-07 21:08:05 +00:00
l10n daemon script
17fdad3afd GIT_SILENT made messages (after extraction) 2025-08-07 20:25:41 +00:00
l10n daemon script
64bc2691cb GIT_SILENT Sync po/docbooks with svn 2025-08-07 03:23:41 +00:00
l10n daemon script
c7df34a9c8 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-08-07 03:19:05 +00:00
l10n daemon script
1525c74b10 GIT_SILENT made messages (after extraction) 2025-08-07 02:40:41 +00:00
l10n daemon script
58b85622c5 GIT_SILENT Sync po/docbooks with svn 2025-08-06 03:35:41 +00:00
l10n daemon script
796470d0e0 GIT_SILENT Sync po/docbooks with svn 2025-08-05 03:55:46 +00:00
l10n daemon script
3a43d99575 GIT_SILENT Sync po/docbooks with svn 2025-08-04 03:40:01 +00:00
l10n daemon script
a62798ef1e GIT_SILENT Sync po/docbooks with svn 2025-08-03 03:19:30 +00:00
l10n daemon script
349d0c5f5f GIT_SILENT Sync po/docbooks with svn 2025-08-02 03:15:57 +00:00
Tobias Fella
c917fc0166 Fix account switching on logout
(cherry picked from commit 501f14fead)
2025-08-01 12:28:35 +02:00
Tobias Fella
a4f2d8fca1 Fix loading
(cherry picked from commit d14466451d)
2025-08-01 12:13:38 +02:00
Tobias Fella
fd18f88adf Fix common crash during login
(cherry picked from commit 7742c6d4b0)
2025-08-01 11:28:43 +02:00
l10n daemon script
546694b08e GIT_SILENT Sync po/docbooks with svn 2025-08-01 03:27:30 +00:00
Heiko Becker
699026fc2f GIT_SILENT Update Appstream for new release 2025-08-01 00:54:04 +02:00
Heiko Becker
c61c2d7437 GIT_SILENT Upgrade release service version to 25.08.0. 2025-07-31 23:43:12 +02:00
l10n daemon script
56babbc1c5 GIT_SILENT Sync po/docbooks with svn 2025-07-31 03:14:23 +00:00
Joshua Goins
74b3e703c1 Replace duplicate beginResetModel with endResetModel
The initialize method was calling beginResetModel twice without
a corresponding endResetModel call. This could cause model state
inconsistencies.


(cherry picked from commit 4e0b295f66)

Co-authored-by: Wang Yu <wangyu@uniontech.com>
2025-07-30 21:13:01 -04:00
l10n daemon script
f620221113 GIT_SILENT Sync po/docbooks with svn 2025-07-30 03:15:18 +00:00
Tobias Fella
896c001430 Fix test
(cherry picked from commit 24e43d063a)
2025-07-29 17:04:21 +02:00
l10n daemon script
defee77c96 GIT_SILENT Sync po/docbooks with svn 2025-07-29 03:17:38 +00:00
l10n daemon script
4328ab8e89 GIT_SILENT Sync po/docbooks with svn 2025-07-28 03:13:37 +00:00
Tobias Fella
54b081abba Prepare for new RoomId format
See MSC4291

(cherry picked from commit edf5d55da4)
2025-07-27 21:54:13 +02:00
l10n daemon script
29686608e1 GIT_SILENT Sync po/docbooks with svn 2025-07-25 03:20:27 +00:00
Heiko Becker
b720ecf29d GIT_SILENT Upgrade release service version to 25.07.90. 2025-07-23 22:24:21 +02:00
l10n daemon script
fc859d679a GIT_SILENT Sync po/docbooks with svn 2025-07-23 03:18:09 +00:00
l10n daemon script
3595ad9293 GIT_SILENT Sync po/docbooks with svn 2025-07-22 03:21:18 +00:00
l10n daemon script
73f8ebc54e GIT_SILENT Sync po/docbooks with svn 2025-07-21 03:18:12 +00:00
l10n daemon script
19cf534acd GIT_SILENT Sync po/docbooks with svn 2025-07-17 03:14:25 +00:00
l10n daemon script
9b86088e26 GIT_SILENT Sync po/docbooks with svn 2025-07-16 04:06:03 +00:00
l10n daemon script
a93117fcd6 GIT_SILENT Sync po/docbooks with svn 2025-07-15 03:24:36 +00:00
l10n daemon script
ee20c90498 GIT_SILENT Sync po/docbooks with svn 2025-07-14 03:37:26 +00:00
l10n daemon script
860a2267d5 GIT_SILENT Sync po/docbooks with svn 2025-07-13 03:15:50 +00:00
l10n daemon script
cb9b2648ca GIT_SILENT Sync po/docbooks with svn 2025-07-12 03:21:34 +00:00
l10n daemon script
1e798b6c15 GIT_SILENT Sync po/docbooks with svn 2025-07-11 03:17:16 +00:00
l10n daemon script
124ffba5e0 GIT_SILENT Sync po/docbooks with svn 2025-07-10 03:16:20 +00:00
l10n daemon script
3aaaa610df GIT_SILENT Sync po/docbooks with svn 2025-07-09 03:22:43 +00:00
l10n daemon script
18e883834c GIT_SILENT Sync po/docbooks with svn 2025-07-08 03:24:01 +00:00
l10n daemon script
6acbd2dffd GIT_SILENT Sync po/docbooks with svn 2025-07-07 03:14:00 +00:00
l10n daemon script
dc5c27aa2d GIT_SILENT Sync po/docbooks with svn 2025-07-06 03:22:47 +00:00
Albert Astals Cid
26774bbe56 GIT_SILENT Upgrade release service version to 25.07.80. 2025-07-05 11:30:42 +02:00
329 changed files with 67598 additions and 127720 deletions

View File

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

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat", "id": "org.kde.neochat",
"branch": "master", "branch": "master",
"runtime": "org.kde.Platform", "runtime": "org.kde.Platform",
"runtime-version": "6.10", "runtime-version": "6.9",
"sdk": "org.kde.Sdk", "sdk": "org.kde.Sdk",
"command": "neochat", "command": "neochat",
"tags": [ "tags": [
@@ -20,32 +20,21 @@
"--talk-name=org.kde.kwalletd5", "--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher", "--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets", "--talk-name=org.freedesktop.secrets",
"--talk-name=org.kde.kuiserver",
"--own-name=org.kde.StatusNotifierItem-2-2" "--own-name=org.kde.StatusNotifierItem-2-2"
], ],
"cleanup": [
"/include",
"/lib/*.a",
"/lib/cmake",
"/lib/pkgconfig",
"/share/ndk-modules"
],
"modules": [ "modules": [
{ {
"name": "opencv", "name": "kirigamiaddons",
"config-opts": [ "config-opts": [
"-DBUILD_TESTS=OFF", "-DBUILD_TESTING=OFF"
"-DWITH_GTK=OFF",
"-DBUILD_LIST=core,imgproc"
], ],
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"sources": [ "sources": [
{ {
"type": "git", "type": "git",
"url": "https://github.com/opencv/opencv" "url": "https://invent.kde.org/libraries/kirigami-addons.git"
} }
], ]
"builddir": true
}, },
{ {
"name": "kquickimageeditor", "name": "kquickimageeditor",
@@ -65,7 +54,6 @@
"name": "olm", "name": "olm",
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"config-opts": [ "config-opts": [
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DOLM_TESTS=OFF" "-DOLM_TESTS=OFF"
], ],
"sources": [ "sources": [
@@ -94,8 +82,8 @@
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://download.gnome.org/sources/libsecret/0.21/libsecret-0.21.7.tar.xz", "url": "https://download.gnome.org/sources/libsecret/0.21/libsecret-0.21.6.tar.xz",
"sha256": "6b452e4750590a2b5617adc40026f28d2f4903de15f1250e1d1c40bfd68ed55e", "sha256": "747b8c175be108c880d3adfb9c3537ea66e520e4ad2dccf5dce58003aeeca090",
"x-checker-data": { "x-checker-data": {
"type": "gnome", "type": "gnome",
"name": "libsecret", "name": "libsecret",
@@ -165,20 +153,16 @@
"name": "kunifiedpush", "name": "kunifiedpush",
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"builddir": true, "builddir": true,
"config-opts": [
"-DENABLE_TESTING=OFF",
"-DKUNIFIEDPUSH_CLIENT_ONLY=ON"
],
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.3/src/kunifiedpush-25.08.3.tar.xz", "url": "https://download.kde.org/stable/kunifiedpush/kunifiedpush-1.0.0.tar.xz",
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a", "sha256": "2ddeba21306d0307114ec50a2c38159ec62359f9fc6cdd58da30a369fbd550cf",
"x-checker-data": { "x-checker-data": {
"type": "anitya", "type": "anitya",
"project-id": 8763, "project-id": 375055,
"stable-only": true, "stable-only": true,
"url-template": "https://download.kde.org/stable/release-service/$version/src/kunifiedpush-$version.tar.xz" "url-template": "https://download.kde.org/stable/kunifiedpush/kunifiedpush-$version.tar.xz"
} }
} }
] ]

View File

@@ -10,11 +10,11 @@ Dependencies:
'frameworks/ki18n': '@latest-kf6' 'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6' 'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6' 'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6' 'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6' 'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6' 'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6' 'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6' 'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6' 'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6' 'frameworks/prison': '@latest-kf6'
@@ -29,6 +29,7 @@ Dependencies:
'frameworks/kio': '@latest-kf6' 'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6' 'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6' 'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'frameworks/kdbusaddons': '@latest-kf6' 'frameworks/kdbusaddons': '@latest-kf6'
@@ -42,4 +43,3 @@ Dependencies:
Options: Options:
per-test-timeout: 90 per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows'] require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True

View File

@@ -7,15 +7,15 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "26") set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "03") set(RELEASE_SERVICE_VERSION_MINOR "08")
set(RELEASE_SERVICE_VERSION_MICRO "70") set(RELEASE_SERVICE_VERSION_MICRO "2")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.17") set(KF_MIN_VERSION "6.16")
set(QT_MIN_VERSION "6.9") set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -24,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.17) set(KDE_COMPILERSETTINGS_LEVEL 6.0)
include(FeatureSummary) include(FeatureSummary)
include(ECMSetupVersion) include(ECMSetupVersion)
@@ -39,16 +39,17 @@ include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory) include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk) include(ECMAddAndroidApk)
include(ECMQmlModule) include(ECMQmlModule)
include(ECMDeprecationSettings)
include(GenerateExportHeader) include(GenerateExportHeader)
include(ECMGenerateHeaders) include(ECMGenerateHeaders)
if (NOT ANDROID) if (NOT ANDROID)
include(KDEClangFormat) include(KDEClangFormat)
endif() endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE) if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0) set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_setup_version(${PROJECT_VERSION} ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT VARIABLE_PREFIX NEOCHAT
@@ -65,7 +66,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW) qt_policy(SET QTP0004 NEW)
endif () endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme IconThemes) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme)
set_package_properties(KF6 PROPERTIES set_package_properties(KF6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Basic application components" PURPOSE "Basic application components"
@@ -74,7 +75,7 @@ set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Kirigami application UI framework" PURPOSE "Kirigami application UI framework"
) )
find_package(KF6KirigamiAddons 1.10.0 REQUIRED) find_package(KF6KirigamiAddons 1.6.0 REQUIRED)
if (UNIX AND NOT APPLE AND NOT ANDROID AND NOT NEOCHAT_FLATPAK AND NOT NEOCHAT_APPIMAGE) 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) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
@@ -88,7 +89,7 @@ if(ANDROID)
) )
else() else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets) find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem Crash)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED) find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME TYPE RUNTIME
@@ -106,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED) find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif() endif()
find_package(QuotientQt6 0.9.3) find_package(QuotientQt6 0.9.1)
set_package_properties(QuotientQt6 PROPERTIES set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API" DESCRIPTION "Qt wrapper around Matrix API"

View File

@@ -88,9 +88,3 @@ path = "memorytests/memtest-sync.json"
precedence = "aggregate" precedence = "aggregate"
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>" SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
SPDX-License-Identifier = "BSD-2-Clause" SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".contextProperties.ini"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"

View File

@@ -92,15 +92,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME actionstest TEST_NAME actionstest
) )
ecm_add_test(
servernoticestest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME servernoticestest
)
ecm_add_test(
roommanagertest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)

View File

@@ -63,7 +63,7 @@ void ActionsTest::testActions_data()
QTest::addColumn<std::optional<QString>>("resultText"); QTest::addColumn<std::optional<QString>>("resultText");
QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type"); QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type");
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\\\_(ツ)\\_/¯ Hello"_s) QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\_(ツ)_/¯ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text); << 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("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) QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s)

View File

@@ -6,7 +6,6 @@
#include <QObject> #include <QObject>
#include <QTest> #include <QTest>
#include <QSignalSpy>
#include <Quotient/roommember.h> #include <Quotient/roommember.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include <qtestcase.h> #include <qtestcase.h>
@@ -33,7 +32,6 @@ private Q_SLOTS:
void noRoom(); void noRoom();
void badParent(); void badParent();
void reply(); void reply();
void replyMissingUser();
void edit(); void edit();
void attachment(); void attachment();
}; };
@@ -104,33 +102,6 @@ void ChatBarCacheTest::reply()
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s)); QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s); QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
}
void ChatBarCacheTest::replyMissingUser()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room
room->syncNewEvents(u"test-min-sync-extra-sync.json"_s);
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
} }
void ChatBarCacheTest::edit() void ChatBarCacheTest::edit()

View File

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

View File

@@ -10,7 +10,7 @@
#include <Quotient/roommember.h> #include <Quotient/roommember.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include "models/eventmessagecontentmodel.h" #include "models/messagecontentmodel.h"
#include "neochatconnection.h" #include "neochatconnection.h"
#include "testutils.h" #include "testutils.h"
@@ -39,17 +39,17 @@ void MessageContentModelTest::initTestCase()
void MessageContentModelTest::missingEvent() void MessageContentModelTest::missingEvent()
{ {
auto room = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s); auto room = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
auto model1 = EventMessageContentModel(room, u"$153456789:example.org"_s); auto model1 = MessageContentModel(room, u"$153456789:example.org"_s);
QCOMPARE(model1.rowCount(), 1); QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s); QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s);
auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true); auto model2 = MessageContentModel(room, u"$153456789:example.org"_s, true);
QCOMPARE(model2.rowCount(), 1); QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading); QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s); QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s);
room->syncNewEvents(u"test-min-sync.json"_s); room->syncNewEvents(u"test-min-sync.json"_s);
QCOMPARE(model1.rowCount(), 2); QCOMPARE(model1.rowCount(), 2);

View File

@@ -9,7 +9,7 @@
#include <Quotient/events/roommessageevent.h> #include <Quotient/events/roommessageevent.h>
#include "models/eventmessagecontentmodel.h" #include "models/messagecontentmodel.h"
#include "testutils.h" #include "testutils.h"
using namespace Quotient; using namespace Quotient;
@@ -21,7 +21,7 @@ class ReactionModelTest : public QObject
private: private:
Connection *connection = nullptr; Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr; TestUtils::TestRoom *room = nullptr;
EventMessageContentModel *parentModel; MessageContentModel *parentModel;
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
@@ -34,7 +34,7 @@ void ReactionModelTest::initTestCase()
{ {
connection = Connection::makeMockConnection(u"@bob:kde.org"_s); connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s); room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s);
parentModel = new EventMessageContentModel(room, "123456"_L1); parentModel = new MessageContentModel(room, "123456"_L1);
} }
void ReactionModelTest::basicReaction() void ReactionModelTest::basicReaction()

View File

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

View File

@@ -4,12 +4,15 @@
#include "server.h" #include "server.h"
#include <QFile> #include <QFile>
#include <QHttpServer>
#include <QHttpServerResponder> #include <QHttpServerResponder>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QSslCertificate> #include <QSslCertificate>
#include <QSslKey> #include <QSslKey>
#include <QSslServer>
#include <QUuid> #include <QUuid>
#include <Quotient/networkaccessmanager.h> #include <Quotient/networkaccessmanager.h>
@@ -106,20 +109,98 @@ void Server::start()
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s, m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
QHttpServerRequest::Method::Post, QHttpServerRequest::Method::Post,
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) { [this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
Changes changes; m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString();
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); responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
}); });
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync); m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
QMap<QString, QJsonArray> stateEvents;
for (const auto &[roomId, matrixId] : m_roomsToCreate) {
stateEvents[roomId] += 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, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
stateEvents[roomId] += 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, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, matrixId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
m_roomsToCreate.clear();
for (const auto &roomId : m_invitedUsers.keys()) {
const auto &values = m_invitedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += 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, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_invitedUsers.clear();
for (const auto &roomId : m_bannedUsers.keys()) {
const auto &values = m_bannedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += 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, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_bannedUsers.clear();
for (const auto &roomId : m_joinedUsers.keys()) {
const auto &values = m_joinedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += 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, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_joinedUsers.clear();
QJsonObject rooms;
for (const auto &roomId : stateEvents.keys()) {
rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}};
}
responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok);
});
QSslConfiguration config; QSslConfiguration config;
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s); QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly)); key.open(QFile::ReadOnly);
config.setPrivateKey(QSslKey(&key, QSsl::Rsa)); config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front()); config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config); m_sslServer.setSslConfiguration(config);
@@ -133,239 +214,22 @@ void Server::start()
QString Server::createRoom(const QString &matrixId) QString Server::createRoom(const QString &matrixId)
{ {
const auto roomId = generateRoomId(); auto roomId = generateRoomId();
Changes changes; m_roomsToCreate += {roomId, matrixId};
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {},
};
m_state += changes;
return roomId; return roomId;
} }
void Server::inviteUser(const QString &roomId, const QString &matrixId) void Server::inviteUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_invitedUsers[roomId] += matrixId;
changes.invitations += Changes::InviteUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
} }
void Server::banUser(const QString &roomId, const QString &matrixId) void Server::banUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_bannedUsers[roomId] += matrixId;
changes.bans += Changes::BanUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
} }
void Server::joinUser(const QString &roomId, const QString &matrixId) void Server::joinUser(const QString &roomId, const QString &matrixId)
{ {
Changes changes; m_joinedUsers[roomId] += matrixId;
changes.joins += Changes::JoinUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
}
QString Server::createServerNoticesRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {u"m.server_notice"_s},
};
m_state += changes;
return roomId;
}
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
changes.events += Changes::Event{
.fullJson = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId}},
};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
for (const auto &change : m_state.mid(token)) {
for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents;
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, newRoom.initialMembers[0]},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : newRoom.initialMembers) {
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
auto room = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents}}}};
QJsonArray roomAccountData;
QJsonObject tags;
for (const auto &tag : newRoom.tags) {
tags[tag] = QJsonObject();
}
if (!tags.empty()) {
roomAccountData += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
}
if (roomAccountData.size() > 0) {
room[u"account_data"] = QJsonObject{{u"events"_s, roomAccountData}};
}
joinRooms[newRoom.roomId] = room;
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &invitation : change.invitations) {
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, invitation.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, invitation.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(invitation.roomId)) {
auto room = joinRooms[invitation.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[invitation.roomId] = room;
} else {
joinRooms[invitation.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &ban : change.bans) {
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, ban.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, ban.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(ban.roomId)) {
auto room = joinRooms[ban.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[ban.roomId] = room;
} else {
joinRooms[ban.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &join : change.joins) {
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, join.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, join.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(join.roomId)) {
auto room = joinRooms[join.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[join.roomId] = room;
} else {
joinRooms[join.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
timeline += event.fullJson;
if (joinRooms.contains(event.fullJson[u"room_id"_s].toString())) {
auto room = joinRooms[event.fullJson[u"room_id"_s].toString()].toObject();
room[u"timeline"_s] = QJsonObject{{u"events"_s, timeline}};
joinRooms[event.fullJson[u"room_id"_s].toString()] = room;
} else {
joinRooms[event.fullJson[u"room_id"_s].toString()] = QJsonObject{
{u"timeline"_s, QJsonObject{{u"events"_s, timeline}}},
};
}
}
}
QJsonObject syncData = {
// {u"account_data"_s, QJsonObject {}},
// {u"presence"_s, QJsonObject {}},
{u"next_batch"_s, QString::number(m_state.size())},
};
QJsonObject rooms;
if (!joinRooms.isEmpty()) {
rooms[u"join"_s] = joinRooms;
}
if (!rooms.empty()) {
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
} }

View File

@@ -2,51 +2,10 @@
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
#include <QHttpServer> #include <QHttpServer>
#include <QJsonObject>
#include <QSslServer> #include <QSslServer>
struct Changes { class Server
struct NewRoom {
QStringList initialMembers;
QString roomId;
QStringList tags;
};
QList<NewRoom> newRooms;
struct InviteUser {
QString userId;
QString roomId;
};
QList<InviteUser> invitations;
struct BanUser {
QString userId;
QString roomId;
};
QList<BanUser> bans;
struct JoinUser {
QString userId;
QString roomId;
};
QList<JoinUser> joins;
struct Event {
QJsonObject fullJson;
};
QList<Event> events;
};
struct RoomData {
QStringList members;
QString id;
QStringList tags;
};
class Server : public QObject
{ {
Q_OBJECT
public: public:
Server(); Server();
@@ -62,17 +21,13 @@ public:
void banUser(const QString &roomId, const QString &matrixId); void banUser(const QString &roomId, const QString &matrixId);
void joinUser(const QString &roomId, const QString &matrixId); void joinUser(const QString &roomId, const QString &matrixId);
/**
* Create a server notices room.
*/
QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
private: private:
QHttpServer m_server; QHttpServer m_server;
QSslServer m_sslServer; QSslServer m_sslServer;
void sync(const QHttpServerRequest &request, QHttpServerResponder &responder); QHash<QString, QList<QString>> m_invitedUsers;
QHash<QString, QList<QString>> m_bannedUsers;
QHash<QString, QList<QString>> m_joinedUsers;
QList<Changes> m_state; QList<std::pair<QString, QString>> m_roomsToCreate;
}; };

View File

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

View File

@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com> // 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 // 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/events/event.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
@@ -33,7 +32,7 @@ public:
if (!syncFileName.isEmpty()) { if (!syncFileName.isEmpty()) {
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
Q_UNUSED(testSyncFile.open(QIODevice::ReadOnly)); testSyncFile.open(QIODevice::ReadOnly);
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()); const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object()); Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
update(std::move(roomData)); update(std::move(roomData));
@@ -47,7 +46,7 @@ inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFile
if (!eventFileName.isEmpty()) { if (!eventFileName.isEmpty()) {
QFile testEventFile; QFile testEventFile;
testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName); testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName);
Q_UNUSED(testEventFile.open(QIODevice::ReadOnly)); testEventFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object(); auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson); return Quotient::loadEvent<EventT>(testSyncJson);
} }

View File

@@ -34,10 +34,6 @@ private Q_SLOTS:
void stripDisallowedTags(); void stripDisallowedTags();
void stripDisallowedAttributes(); void stripDisallowedAttributes();
void emptyCodeTags(); void emptyCodeTags();
void addStyle_data();
void addStyle();
void dontAddStyle_data();
void dontAddStyle();
void sendSimpleStringCase(); void sendSimpleStringCase();
void sendSingleParaMarkup(); void sendSingleParaMarkup();
@@ -75,9 +71,6 @@ private Q_SLOTS:
void componentOutput_data(); void componentOutput_data();
void componentOutput(); void componentOutput();
void updateSpoiler_data();
void updateSpoiler();
}; };
void TextHandlerTest::initTestCase() void TextHandlerTest::initTestCase()
@@ -96,26 +89,21 @@ void TextHandlerTest::initTestCase()
void TextHandlerTest::allowedAttributes() 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 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 testOutputString1 = 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. // 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 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 testOutputString2 = 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; TextHandler testTextHandler;
testTextHandler.setData(testInputString1); testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S); QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
testTextHandler.setData(testInputString2); testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S); QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
} }
void TextHandlerTest::stripDisallowedTags() void TextHandlerTest::stripDisallowedTags()
@@ -158,56 +146,6 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), 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() void TextHandlerTest::sendSimpleStringCase()
{ {
const QString testInputString = u"This data should just be left alone."_s; const QString testInputString = u"This data should just be left alone."_s;
@@ -400,8 +338,7 @@ void TextHandlerTest::receiveRichInPlainOut()
void TextHandlerTest::receivePlainTextIn() void TextHandlerTest::receivePlainTextIn()
{ {
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s; const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
const QString testOutputStringRich = const QString testOutputStringRich = u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>."_s;
u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s; QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
// Make sure quotes are maintained in a plain string. // Make sure quotes are maintained in a plain string.
@@ -471,7 +408,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill() void TextHandlerTest::receiveRichUserPill()
{ {
const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s; 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; const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>"_s;
TextHandler testTextHandler; TextHandler testTextHandler;
testTextHandler.setData(testInputString); testTextHandler.setData(testInputString);
@@ -523,23 +460,21 @@ void TextHandlerTest::receiveRichPlainUrl_data()
// so we can confirm consistent behaviour for complex urls. // so we can confirm consistent behaviour for complex urls.
QTest::addRow("link 1") QTest::addRow("link 1")
<< u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s << u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">Link already rich</a>"_s; << u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s;
// Another real case. The linkification wasn't handling it when a single link // 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 // contains what looks like and email. It was broken into 3 but needs to
// be just single link. // be just single link.
QTest::addRow("link 2") QTest::addRow("link 2")
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s << 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; << u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
QTest::addRow("email") QTest::addRow("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s << uR"(<a href="mailto:email@example.com">email@example.com</a> <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") QTest::addRow("mxid")
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s << 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; << u"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>"_s;
QTest::addRow("mxid with prefix") << u"a @user:kde.org 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\">@user:kde.org</a></b> b"_s;
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;
} }
/** /**
@@ -661,35 +596,5 @@ void TextHandlerTest::componentOutput()
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents); 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) QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc" #include "texthandlertest.moc"

View File

@@ -161,7 +161,7 @@ void TimelineMessageModelTest::pendingEvent()
// different every time. // different every time.
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s);
QVERIFY(testSyncFile.open(QIODevice::ReadOnly)); testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()); auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
auto root = testSyncJson.object(); auto root = testSyncJson.object();
auto timeline = root["timeline"_L1].toObject(); auto timeline = root["timeline"_L1].toObject();
@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s); auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room); model->setRoom(room);
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0); QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
} }
void TimelineMessageModelTest::cleanup() void TimelineMessageModelTest::cleanup()

14
cmake/Flatpak.cmake Normal file
View 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
)

View 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>

View File

@@ -3,12 +3,12 @@
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" ) add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline_memtest qt_add_executable(timeline-memtest
main.cpp main.cpp
) )
target_link_libraries(timeline_memtest PRIVATE neochatplugin Timelineplugin) target_link_libraries(timeline-memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline_memtest PUBLIC target_link_libraries(timeline-memtest PUBLIC
Qt::Core Qt::Core
Qt::Quick Qt::Quick
Qt::Qml Qt::Qml
@@ -16,13 +16,14 @@ target_link_libraries(timeline_memtest PUBLIC
Qt::QuickControls2 Qt::QuickControls2
Qt::Widgets Qt::Widgets
KF6::I18nQml KF6::I18nQml
KF6::Kirigami
QuotientQt6 QuotientQt6
LibNeoChat LibNeoChat
Timeline Timeline
) )
ecm_add_qml_module(timeline_memtest URI org.kde.neochat.timeline_memtest GENERATE_PLUGIN_SOURCE ecm_add_qml_module(timeline-memtest URI org.kde.neochat.timeline-memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline_memtest OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline-memtest
QML_FILES QML_FILES
Main.qml Main.qml
SOURCES SOURCES

View File

@@ -28,7 +28,7 @@ int main(int argc, char **argv)
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel); engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel); engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
engine.loadFromModule("org.kde.neochat.timeline_memtest", "Main"); engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
return app.exec(); return app.exec();
} }

View File

@@ -37,11 +37,7 @@ public:
if (!syncFileName.isEmpty()) { if (!syncFileName.isEmpty()) {
QFile testSyncFile; QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
auto ok = testSyncFile.open(QIODevice::ReadOnly); testSyncFile.open(QIODevice::ReadOnly);
if (!ok) {
qWarning() << "Failed to open" << testSyncFile.fileName() << testSyncFile.errorString();
}
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object(); auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject(); auto timelineJson = testSyncJson["timeline"_L1].toObject();
timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100); timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100);

View File

@@ -41,7 +41,6 @@
<name xml:lang="pl">NeoChat</name> <name xml:lang="pl">NeoChat</name>
<name xml:lang="pt">NeoChat</name> <name xml:lang="pt">NeoChat</name>
<name xml:lang="pt-BR">NeoChat</name> <name xml:lang="pt-BR">NeoChat</name>
<name xml:lang="ro">NeoChat</name>
<name xml:lang="ru">NeoChat</name> <name xml:lang="ru">NeoChat</name>
<name xml:lang="sa">नवचैट्</name> <name xml:lang="sa">नवचैट्</name>
<name xml:lang="sk">NeoChat</name> <name xml:lang="sk">NeoChat</name>
@@ -76,7 +75,6 @@
<summary xml:lang="nn">Prat med via Matrix</summary> <summary xml:lang="nn">Prat med via Matrix</summary>
<summary xml:lang="pl">Rozmawiaj na Matriksie</summary> <summary xml:lang="pl">Rozmawiaj na Matriksie</summary>
<summary xml:lang="pt-BR">Bate-papo na Matrix</summary> <summary xml:lang="pt-BR">Bate-papo na Matrix</summary>
<summary xml:lang="ro">Discutați pe Matrix</summary>
<summary xml:lang="ru">Общение в Matrix</summary> <summary xml:lang="ru">Общение в Matrix</summary>
<summary xml:lang="sa">Matrix इत्यत्र गपशपं कुर्वन्तु</summary> <summary xml:lang="sa">Matrix इत्यत्र गपशपं कुर्वन्तु</summary>
<summary xml:lang="sl">Klepet na Matrixu</summary> <summary xml:lang="sl">Klepet na Matrixu</summary>
@@ -111,7 +109,6 @@
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p> <p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p> <p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="pt-BR">O NeoChat é um aplicativo de bate-papo que permite que você aproveite ao máximo a rede Matrix. Ele oferece uma maneira segura de enviar mensagens de texto, vídeos e arquivos de áudio para sua família, colegas e amigos.</p> <p xml:lang="pt-BR">O NeoChat é um aplicativo de bate-papo que permite que você aproveite ao máximo a rede Matrix. Ele oferece uma maneira segura de enviar mensagens de texto, vídeos e arquivos de áudio para sua família, colegas e amigos.</p>
<p xml:lang="ro">NeoChat e o aplicație de discuții ce vă ajută să profitați din plin de rețeaua Matrix. Aceasta oferă o modalitate sigură de a trimite mesaje textuale, videoclipuri și fișiere audio familiei, colegilor și prietenilor.</p>
<p xml:lang="ru">NeoChat — приложение для общения, предоставляющее все преимущества сети Matrix. С его помощью можно безопасно отправлять текстовые сообщения, видеозаписи и звуковые файлы родственникам, коллегам и друзьям.</p> <p xml:lang="ru">NeoChat — приложение для общения, предоставляющее все преимущества сети Matrix. С его помощью можно безопасно отправлять текстовые сообщения, видеозаписи и звуковые файлы родственникам, коллегам и друзьям.</p>
<p xml:lang="sa">NeoChat इति एकं गपशप-अनुप्रयोगं यत् भवान् Matrix-जालस्य पूर्णं लाभं ग्रहीतुं शक्नोति । एतत् भवन्तं भवतः परिवाराय, सहकारिभ्यः, मित्रेभ्यः च पाठसन्देशान्, भिडियो, श्रव्यसञ्चिकाः च प्रेषयितुं सुरक्षितं मार्गं प्रदाति ।</p> <p xml:lang="sa">NeoChat इति एकं गपशप-अनुप्रयोगं यत् भवान् Matrix-जालस्य पूर्णं लाभं ग्रहीतुं शक्नोति । एतत् भवन्तं भवतः परिवाराय, सहकारिभ्यः, मित्रेभ्यः च पाठसन्देशान्, भिडियो, श्रव्यसञ्चिकाः च प्रेषयितुं सुरक्षितं मार्गं प्रदाति ।</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p> <p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
@@ -145,7 +142,6 @@
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p> <p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p> <p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="pt-BR">O NeoChat pretende ser um aplicativo completo para a especificação Matrix. Dessa forma, tudo na especificação estável atual, com as notáveis exceções de VoIP, tópicos e alguns aspectos da criptografia de ponta a ponta, é suportado. Há algumas outras pequenas omissões devido ao fato de a especificação Matrix estar em constante evolução, mas o objetivo continua sendo fornecer suporte eventual para toda a especificação.</p> <p xml:lang="pt-BR">O NeoChat pretende ser um aplicativo completo para a especificação Matrix. Dessa forma, tudo na especificação estável atual, com as notáveis exceções de VoIP, tópicos e alguns aspectos da criptografia de ponta a ponta, é suportado. Há algumas outras pequenas omissões devido ao fato de a especificação Matrix estar em constante evolução, mas o objetivo continua sendo fornecer suporte eventual para toda a especificação.</p>
<p xml:lang="ro">NeoChat vrea să fie o aplicație completă pentru specificațiile Matrix. Astfel, susține tot ce se găsește acum în specificațiile stabile cu excepția VoIP, a firelor de discuții, și a unor părți din criptarea punct-la-punct. Sunt și câteva omisiuni minore din cauza faptului că specificația Matrix evoluează continuu, dar scopul rămâne acela de a implementa întreaga specificație.</p>
<p xml:lang="ru">Целью создания NeoChat является полноценная реализация программы для спецификации Matrix. Как следствие, реализовано всё в текущей стабильной спецификации (за исключением голосовой интернет-связи, потоков и некоторых аспектов сквозного шифрования). Есть также несколько других незначительных пробелов, обусловленных постоянными изменениями спецификации Matrix. Тем не менее, стоит задача в итоге предоставить полную поддержку спецификации.</p> <p xml:lang="ru">Целью создания NeoChat является полноценная реализация программы для спецификации Matrix. Как следствие, реализовано всё в текущей стабильной спецификации (за исключением голосовой интернет-связи, потоков и некоторых аспектов сквозного шифрования). Есть также несколько других незначительных пробелов, обусловленных постоянными изменениями спецификации Matrix. Тем не менее, стоит задача в итоге предоставить полную поддержку спецификации.</p>
<p xml:lang="sa">NeoChat इत्यस्य उद्देश्यं Matrix विनिर्देशस्य कृते पूर्णतया विशेषतायुक्तः अनुप्रयोगः भवितुम् अस्ति । यथा तथा वर्तमानस्थिरविनिर्देशे सर्वं VoIP इत्यस्य उल्लेखनीयअपवादैः सह, थ्रेड्स तथा च End-to-End Encryption इत्यस्य केचन पक्षाः समर्थिताः सन्ति । अन्ये कतिचन लघु लोपाः सन्ति यतोहि Matrix spec निरन्तरं विकसितः अस्ति परन्तु उद्देश्यं सम्पूर्ण spec कृते अन्ततः समर्थनं प्रदातुं अवशिष्टम् अस्ति</p> <p xml:lang="sa">NeoChat इत्यस्य उद्देश्यं Matrix विनिर्देशस्य कृते पूर्णतया विशेषतायुक्तः अनुप्रयोगः भवितुम् अस्ति । यथा तथा वर्तमानस्थिरविनिर्देशे सर्वं VoIP इत्यस्य उल्लेखनीयअपवादैः सह, थ्रेड्स तथा च End-to-End Encryption इत्यस्य केचन पक्षाः समर्थिताः सन्ति । अन्ये कतिचन लघु लोपाः सन्ति यतोहि Matrix spec निरन्तरं विकसितः अस्ति परन्तु उद्देश्यं सम्पूर्ण spec कृते अन्ततः समर्थनं प्रदातुं अवशिष्टम् अस्ति</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p> <p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
@@ -179,7 +175,6 @@
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p> <p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p> <p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
<p xml:lang="pt-BR">Devido à natureza do desenvolvimento da especificação Matrix, o NeoChat também suporta diversos recursos instáveis. Atualmente, são eles:</p> <p xml:lang="pt-BR">Devido à natureza do desenvolvimento da especificação Matrix, o NeoChat também suporta diversos recursos instáveis. Atualmente, são eles:</p>
<p xml:lang="ro">Datorită modului de dezvoltare a specificațiilor Matrix, NeoChat susține și numeroase caracteristici nestabile. Acum, acestea sunt:</p>
<p xml:lang="ru">В силу природы разработки спецификации Matrix в NeoChat тоже предусмотрена поддержка многочисленных нестабильных возможностей. В текущей версии это следующие возможности:</p> <p xml:lang="ru">В силу природы разработки спецификации Matrix в NeoChat тоже предусмотрена поддержка многочисленных нестабильных возможностей. В текущей версии это следующие возможности:</p>
<p xml:lang="sa">Matrix विनिर्देशविकासस्य प्रकृतेः कारणात् NeoChat अपि अनेकानाम् अस्थिरविशेषतानां समर्थनं करोति । सम्प्रति एते सन्ति :</p> <p xml:lang="sa">Matrix विनिर्देशविकासस्य प्रकृतेः कारणात् NeoChat अपि अनेकानाम् अस्थिरविशेषतानां समर्थनं करोति । सम्प्रति एते सन्ति :</p>
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p> <p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
@@ -214,7 +209,6 @@
<li xml:lang="pl">Ankiety - MSC3381</li> <li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li> <li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="pt-BR">Enquetes - MSC3381</li> <li xml:lang="pt-BR">Enquetes - MSC3381</li>
<li xml:lang="ro">Sondaje - MSC3381</li>
<li xml:lang="ru">Голосования — MSC3381</li> <li xml:lang="ru">Голосования — MSC3381</li>
<li xml:lang="sa">मतदान - MSC3381</li> <li xml:lang="sa">मतदान - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li> <li xml:lang="sl">Polls - MSC3381</li>
@@ -248,7 +242,6 @@
<li xml:lang="pl">Paczki naklejek - MSC2545</li> <li xml:lang="pl">Paczki naklejek - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li> <li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="pt-BR">Pacotes de Stickers - MSC2545</li> <li xml:lang="pt-BR">Pacotes de Stickers - MSC2545</li>
<li xml:lang="ro">Colecții de abțibilduri - MSC2545</li>
<li xml:lang="ru">Наборы стикеров — MSC2545</li> <li xml:lang="ru">Наборы стикеров — MSC2545</li>
<li xml:lang="sa">स्टिकर पैक - MSC2545</li> <li xml:lang="sa">स्टिकर पैक - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li> <li xml:lang="sl">Sticker Packs - MSC2545</li>
@@ -282,7 +275,6 @@
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li> <li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li> <li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="pt-BR">Localização de eventos - MSC3488</li> <li xml:lang="pt-BR">Localização de eventos - MSC3488</li>
<li xml:lang="ro">Evenimente de amplasare - MSC3488</li>
<li xml:lang="ru">События местоположения — MSC3488</li> <li xml:lang="ru">События местоположения — MSC3488</li>
<li xml:lang="sa">स्थान घटनाएँ - MSC3488</li> <li xml:lang="sa">स्थान घटनाएँ - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li> <li xml:lang="sl">Location Events - MSC3488</li>
@@ -306,8 +298,8 @@
<keyword>Matrix</keyword> <keyword>Matrix</keyword>
<keyword>Kirigami</keyword> <keyword>Kirigami</keyword>
</keywords> </keywords>
<developer id="org.kde"> <developer id="kde.org">
<name translate="no">KDE</name> <name>The KDE Community</name>
<url>https://kde.org</url> <url>https://kde.org</url>
</developer> </developer>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
@@ -320,6 +312,7 @@
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value> <value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value> <value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value> <value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::supporters">Tanguy Fardet;[dabe](https://freeradical.zone/@dabe);[lengau](https://mastodon.world/@lengau);Joshua Strobl;Stuart Turton</value>
</custom> </custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable> <launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots> <screenshots>
@@ -351,7 +344,6 @@
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption> <caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption> <caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption> <caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption>
<caption xml:lang="ro">Vederea principală cu lista de camere, discuție, și informații despre cameră</caption>
<caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption> <caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption>
<caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption> <caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption> <caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
@@ -388,7 +380,6 @@
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption> <caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption> <caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="pt-BR">Descubra novas comunidades com os Espaços Matrix</caption> <caption xml:lang="pt-BR">Descubra novas comunidades com os Espaços Matrix</caption>
<caption xml:lang="ro">Descoperiți comunități noi cu Spații Matrix</caption>
<caption xml:lang="ru">Поиск новых сообществ с помощью Matrix Spaces</caption> <caption xml:lang="ru">Поиск новых сообществ с помощью Matrix Spaces</caption>
<caption xml:lang="sa">Matrix Spaces इत्यनेन सह नूतनानां समुदायानाम् अन्वेषणं कुर्वन्तु</caption> <caption xml:lang="sa">Matrix Spaces इत्यनेन सह नूतनानां समुदायानाम् अन्वेषणं कुर्वन्तु</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption> <caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
@@ -433,7 +424,6 @@
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption> <caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption> <caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption> <caption xml:lang="pt-BR">Visão principal com lista de salas, bate-papo e informações sobre as salas</caption>
<caption xml:lang="ro">Vederea principală cu lista de camere, discuție, și informații despre cameră</caption>
<caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption> <caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption>
<caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption> <caption xml:lang="sa">कक्षसूची, गपशपः, कक्षसूचना च सह मुख्यदृश्यम्</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption> <caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
@@ -472,7 +462,6 @@
<caption xml:lang="pl">Ekran logowania</caption> <caption xml:lang="pl">Ekran logowania</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption> <caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="pt-BR">Tela de login</caption> <caption xml:lang="pt-BR">Tela de login</caption>
<caption xml:lang="ro">Ecran de autentificare</caption>
<caption xml:lang="ru">Окно входа</caption> <caption xml:lang="ru">Окно входа</caption>
<caption xml:lang="sa">लॉगिन् स्क्रीन</caption> <caption xml:lang="sa">लॉगिन् स्क्रीन</caption>
<caption xml:lang="sl">Prijavni zaslon</caption> <caption xml:lang="sl">Prijavni zaslon</caption>
@@ -487,9 +476,6 @@
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
</content_rating> </content_rating>
<releases> <releases>
<release version="25.12.1" date="2026-01-08"/>
<release version="25.12.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/>
<release version="25.08.2" date="2025-10-09"/> <release version="25.08.2" date="2025-10-09"/>
<release version="25.08.1" date="2025-09-11"/> <release version="25.08.1" date="2025-09-11"/>
<release version="25.08.0" date="2025-08-14"/> <release version="25.08.0" date="2025-08-14"/>

View File

@@ -108,12 +108,10 @@ Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე Comment[ka]=ჩატი Matrix-ზე
Comment[ko]=Matrix에서 대화하기 Comment[ko]=Matrix에서 대화하기
Comment[lt]=Pokalbiai per Matrix
Comment[lv]=Tērzējiet „Matrix“ tīklā Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie Comment[pl]=Rozmawiaj na Matriksie
Comment[pt_BR]=Bate papo na Matrix Comment[pt_BR]=Bate papo na Matrix
Comment[ro]=Discutați pe Matrix
Comment[ru]=Общение в Matrix Comment[ru]=Общение в Matrix
Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु
Comment[sl]=Klepet na Matrixu Comment[sl]=Klepet na Matrixu

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,122 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Brazilian-Portuguese "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Manual do Usuário do NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat man page.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01/11/2022</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Cliente para interação com o protocolo de mensagens Matrix.</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
>Descrição</title>
<para
>O <command
>neochat</command
> é um aplicativo de bate-papo para o protocolo Matrix. Ele funciona tanto em computadores quanto em dispositivos móveis. </para>
</refsect1>
<refsect1 id="options"
><title
>Opções</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>O URI da matriz para um usuário ou uma sala. Por exemplo, matrix:u/usuário:exemplo.org e matrix:r/root:exemplo.org. Isso fará com que o NeoChat tente abrir a sala ou conversa especificada. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Relatar bugs</title>
<para
>Você pode reportar erros e solicitar novas funcionalidades em <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Veja também</title>
<simplelist>
<member
>Lista de perguntas frequentes sobre o 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
>Direitos autorais</title>
<para
>Direitos autorais &copy; 2020-2022 Tobias Fella </para>
<para
>Direitos autorais &copy; 2020-2022 Carl Schwan </para>
<para
>Licença: GNU General Public Versão 3 ou posterior <ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>carl@carlschwan.eu</email >carl@carlschwan.eu</email
></author> ></author>
<date <date
>20221101</date> >2022-11-01</date>
<releaseinfo <releaseinfo
>22.09</releaseinfo> >22.09</releaseinfo>
<productname <productname
@@ -111,9 +111,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
><title ><title
>Telif Hakkı</title> >Telif Hakkı</title>
<para <para
>Telif hakkı &copy; 20202022 Tobias Fella </para> >Telif hakkı &copy; 2020-2022 Tobias Fella </para>
<para <para
>Telif hakkı &copy; 20202022 Carl Schwan </para> >Telif hakkı &copy; 2020-2022 Carl Schwan </para>
<para <para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html" >Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink >https://www.gnu.org/licenses/gpl-3.0.html</ulink

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org> # SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause # SPDX-License-Identifier: BSD-2-Clause
qt_add_library(neochat STATIC add_library(neochat STATIC
controller.cpp controller.cpp
controller.h controller.h
roommanager.cpp roommanager.cpp
@@ -35,8 +35,6 @@ qt_add_library(neochat STATIC
models/commonroomsmodel.h models/commonroomsmodel.h
texttospeechhelper.h texttospeechhelper.h
texttospeechhelper.cpp texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
) )
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -50,8 +48,6 @@ if(ANDROID OR WIN32)
set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES
QT_QML_SOURCE_TYPENAME GlobalMenu QT_QML_SOURCE_TYPENAME GlobalMenu
) )
else()
set(EXTRA_IMPORTS org.kde.purpose)
endif() endif()
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
@@ -105,12 +101,9 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ReasonDialog.qml qml/ReasonDialog.qml
qml/NewPollDialog.qml qml/NewPollDialog.qml
qml/UserMenu.qml qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
DEPENDENCIES DEPENDENCIES
QtCore QtCore
QtQuick QtQuick
io.github.quotient_im.libquotient
IMPORTS IMPORTS
org.kde.neochat.libneochat org.kde.neochat.libneochat
org.kde.neochat.rooms org.kde.neochat.rooms
@@ -122,15 +115,13 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.devtools org.kde.neochat.devtools
org.kde.neochat.login org.kde.neochat.login
org.kde.neochat.chatbar org.kde.neochat.chatbar
org.kde.config
org.kde.syntaxhighlighting
${EXTRA_IMPORTS}
) )
if(NOT ANDROID AND NOT WIN32) if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES qt_target_qml_sources(neochat QML_FILES
qml/ShareAction.qml qml/ShareAction.qml
qml/GlobalMenu.qml qml/GlobalMenu.qml
qml/EditMenu.qml
) )
else() else()
qt_target_qml_sources(neochat QML_FILES qt_target_qml_sources(neochat QML_FILES
@@ -143,17 +134,10 @@ if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib") set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif() endif()
qt_add_executable(neochat-app add_executable(neochat-app
main.cpp main.cpp
) )
if(ANDROID)
set_target_properties(neochat-app PROPERTIES
OUTPUT_NAME "neochat-app"
PREFIX "lib"
)
endif()
if(TARGET Qt::WebView) if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView) target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW) target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
@@ -163,7 +147,6 @@ target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE target_link_libraries(neochat-app PRIVATE
neochat neochat
KF6::IconThemes
) )
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png) ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -194,7 +177,7 @@ else()
endif() endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models) target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_link_libraries(neochat PRIVATE neochatplugin Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin) target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PUBLIC target_link_libraries(neochat PUBLIC
LibNeoChat LibNeoChat
Timeline Timeline
@@ -214,9 +197,8 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui KF6::ConfigGui
KF6::CoreAddons KF6::CoreAddons
KF6::SonnetCore KF6::SonnetCore
KF6::IconThemes
KF6::ItemModels KF6::ItemModels
KF6::I18nQml
KirigamiApp
QuotientQt6 QuotientQt6
Login Login
Rooms Rooms
@@ -224,6 +206,10 @@ target_link_libraries(neochat PUBLIC
Spaces Spaces
) )
if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION) kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
if(NEOCHAT_FLATPAK) if(NEOCHAT_FLATPAK)
@@ -334,7 +320,6 @@ if(ANDROID)
"kt-restore-defaults-symbolic" "kt-restore-defaults-symbolic"
"user-symbolic" "user-symbolic"
"mark-location-symbolic" "mark-location-symbolic"
"amarok_playcount"
${KIRIGAMI_ADDONS_ICONS} ${KIRIGAMI_ADDONS_ICONS}
) )
@@ -353,7 +338,14 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS) target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif() endif()
if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
if (TARGET KUnifiedPush) if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID) if (NOT ANDROID)
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service) configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
@@ -363,8 +355,7 @@ endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE) if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
# krunner plugin must be the same as the app id for flatpak to export it install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins RENAME org.kde.neochat.desktop)
endif() endif()
if (APPLE) if (APPLE)

View File

@@ -5,6 +5,7 @@
#include "controller.h" #include "controller.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include <qt6keychain/keychain.h>
#include <KLocalizedString> #include <KLocalizedString>
@@ -13,19 +14,23 @@
#include <signal.h> #include <signal.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/events/roommemberevent.h> #include <Quotient/events/roommemberevent.h>
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h> #include <Quotient/settings.h>
#include "accountmanager.h"
#include "enums/roomsortparameter.h" #include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h" #include "mediasizehelper.h"
#include "models/actionsmodel.h" #include "models/actionsmodel.h"
#include "models/messagemodel.h" #include "models/messagemodel.h"
#include "models/pushrulemodel.h"
#include "models/roomlistmodel.h" #include "models/roomlistmodel.h"
#include "models/roomtreemodel.h" #include "models/roomtreemodel.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h" #include "proxycontroller.h"
#include "roommanager.h" #include "roommanager.h"
@@ -35,6 +40,14 @@
#include "trayicon_sni.h" #include "trayicon_sni.h"
#endif #endif
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#endif
#endif
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h> #include <kunifiedpush/connector.h>
#endif #endif
@@ -123,10 +136,8 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed); connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif #endif
connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] { QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
#ifndef Q_OS_ANDROID
delete m_trayIcon; delete m_trayIcon;
#endif
NeoChatConfig::self()->save(); NeoChatConfig::self()->save();
}); });
@@ -192,15 +203,13 @@ void Controller::setAccountManager(AccountManager *manager)
m_accountManager = manager; m_accountManager = manager;
if (!m_accountManager) { if (m_accountManager) {
return; connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
} }
connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
} }
void Controller::initConnection(NeoChatConnection *connection) void Controller::initConnection(NeoChatConnection *connection)
@@ -246,10 +255,7 @@ void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatC
if (newConnection) { if (newConnection) {
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured); connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount); connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
// Refresh and update manually, in case we init too late for the badge count to actually change.
newConnection->refreshBadgeNotificationCount(); newConnection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(newConnection->badgeNotificationCount());
} }
Q_EMIT activeConnectionChanged(newConnection); Q_EMIT activeConnectionChanged(newConnection);
} }
@@ -259,8 +265,8 @@ bool Controller::supportSystemTray() const
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
return false; return false;
#else #else
QStringList unsupportedPlatforms{u"GNOME"_s, u"Pantheon"_s}; auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
return !unsupportedPlatforms.contains(QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"))); return de != u"GNOME"_s && de != u"Pantheon"_s;
#endif #endif
} }
@@ -270,8 +276,11 @@ void Controller::setQuitOnLastWindowClosed()
if (supportSystemTray() && NeoChatConfig::self()->systemTray()) { if (supportSystemTray() && NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this); m_trayIcon = new TrayIcon(this);
m_trayIcon->show(); m_trayIcon->show();
} else if (m_trayIcon) { } else {
delete m_trayIcon; if (m_trayIcon) {
delete m_trayIcon;
m_trayIcon = nullptr;
}
} }
#endif #endif
} }
@@ -327,7 +336,30 @@ void Controller::clearInvitationNotification(const QString &roomId)
void Controller::updateBadgeNotificationCount(int count) void Controller::updateBadgeNotificationCount(int count)
{ {
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop"_L1;
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(count, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"_L1] = counterSlice;
dbusUnityProperties["count-visible"_L1] = true;
} else {
dbusUnityProperties["count-visible"_L1] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_L1, "com.canonical.Unity.LauncherEntry"_L1, "Update"_L1);
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
#endif // Q_OS_ANDROID
#else
qGuiApp->setBadgeNumber(count); qGuiApp->setBadgeNumber(count);
#endif // QT_VERSION_CHECK(6, 6, 0)
} }
bool Controller::isFlatpak() const bool Controller::isFlatpak() const
@@ -348,10 +380,7 @@ QString Controller::loadFileContent(const QString &path) const
{ {
QUrl url(path); QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString()); QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
if (!file.open(QFile::ReadOnly)) { file.open(QFile::ReadOnly);
qCWarning(GENERAL) << "Failed to open file" << path;
return {};
}
return QString::fromLatin1(file.readAll()); return QString::fromLatin1(file.readAll());
} }

View File

@@ -110,9 +110,8 @@ private:
void initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection); void initActiveConnection(NeoChatConnection *oldConnection, NeoChatConnection *newConnection);
QPointer<NeoChatConnection> m_connection; QPointer<NeoChatConnection> m_connection;
#ifndef Q_OS_ANDROID TrayIcon *m_trayIcon = nullptr;
QPointer<TrayIcon> m_trayIcon;
#endif
QString m_endpoint; QString m_endpoint;
QStringList m_shownImages; QStringList m_shownImages;

View File

@@ -33,10 +33,13 @@
#include <KWindowSystem> #include <KWindowSystem>
#endif #endif
#if __has_include("KCrash")
#include <KCrash>
#endif
#include <KIconTheme> #include <KIconTheme>
#include <KLocalizedQmlContext> #include <KLocalizedContext>
#include <KLocalizedString> #include <KLocalizedString>
#include <KirigamiApp>
#include "neochat-version.h" #include "neochat-version.h"
@@ -101,11 +104,8 @@ Q_DECL_EXPORT
#endif #endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QNetworkProxyFactory::setUseSystemConfiguration(true);
// We currently need to do this ourselves,
// KirigamiApp currently called this after constructing the app which breaks icons on Windows.
KIconTheme::initTheme(); KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW #ifdef HAVE_WEBVIEW
QtWebView::initialize(); QtWebView::initialize();
@@ -113,10 +113,24 @@ int main(int argc, char *argv[])
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi); QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif #endif
KirigamiApp::App app(argc, argv); #ifdef Q_OS_ANDROID
KirigamiApp kirigamiApp; QGuiApplication app(argc, argv);
QQuickStyle::setStyle(u"org.kde.breeze"_s);
#else
QIcon::setFallbackThemeName("breeze"_L1);
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(u"org.kde.desktop"_s);
}
#endif
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
QApplication::setStyle(u"breeze"_s); QApplication::setStyle(u"breeze"_s);
QFont font(u"Segoe UI Emoji"_s); QFont font(u"Segoe UI Emoji"_s);
font.setPointSize(10); font.setPointSize(10);
@@ -163,9 +177,19 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about); KAboutData::setApplicationData(about);
QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s)); QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s));
#if __has_include("KCrash")
KCrash::initialize();
#endif
Connection::setEncryptionDefault(true); Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true); Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy(u"/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"_s, u"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"_s);
#endif
ColorSchemer colorScheme; ColorSchemer colorScheme;
QCommandLineParser parser; QCommandLineParser parser;
@@ -261,7 +285,7 @@ int main(int argc, char *argv[])
}); });
#endif #endif
KLocalization::setupLocalizedContext(&engine); engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory()); engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
if (parser.isSet("ignore-ssl-errors"_L1)) { if (parser.isSet("ignore-ssl-errors"_L1)) {
@@ -276,9 +300,7 @@ int main(int argc, char *argv[])
engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider); engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider);
if (!kirigamiApp.start("org.kde.neochat", "Main", &engine)) { engine.loadFromModule("org.kde.neochat", "Main");
return -1;
}
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) { if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]); RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);

View File

@@ -5,7 +5,6 @@
#include "jobs/neochatgetcommonroomsjob.h" #include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication> #include <QGuiApplication>
#include <Quotient/room.h>
using namespace Quotient; using namespace Quotient;
@@ -40,22 +39,8 @@ void CommonRoomsModel::setUserId(const QString &userId)
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{ {
auto roomId = m_commonRooms[index.row()]; Q_UNUSED(index)
auto room = connection()->room(roomId); Q_UNUSED(roleName)
if (!room) {
return {};
}
switch (roleName) {
case Qt::DisplayRole:
case RoomNameRole:
return room->displayName();
case RoomAvatarRole:
return room->avatarUrl();
case RoomIdRole:
return roomId;
}
return {}; return {};
} }
@@ -65,15 +50,6 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const
return m_commonRooms.size(); return m_commonRooms.size();
} }
QHash<int, QByteArray> CommonRoomsModel::roleNames() const
{
return {
{RoomIdRole, "roomId"},
{RoomNameRole, "roomName"},
{RoomAvatarRole, "roomAvatar"},
};
}
void CommonRoomsModel::reload() void CommonRoomsModel::reload()
{ {
if (!m_connection || m_userId.isEmpty()) { if (!m_connection || m_userId.isEmpty()) {

View File

@@ -7,6 +7,7 @@
#include <QQmlEngine> #include <QQmlEngine>
#include "neochatconnection.h" #include "neochatconnection.h"
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h> #include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h> #include <Quotient/roommember.h>
@@ -24,9 +25,11 @@ class CommonRoomsModel : public QAbstractListModel
public: public:
enum Roles { enum Roles {
RoomIdRole = Qt::UserRole, TextRole = Qt::DisplayRole,
RoomNameRole, LongitudeRole,
RoomAvatarRole, LatitudeRole,
AssetRole,
AuthorRole,
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
@@ -41,8 +44,6 @@ public:
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override; [[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void userIdChanged(); void userIdChanged();

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "models/limitermodel.h"
LimiterModel::LimiterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged);
}
int LimiterModel::maximumCount() const
{
return m_maximumCount;
}
void LimiterModel::setMaximumCount(int maximumCount)
{
if (m_maximumCount != maximumCount) {
m_maximumCount = maximumCount;
Q_EMIT maximumCountChanged();
}
}
int LimiterModel::extraCount() const
{
if (sourceModel()) {
return std::max(sourceModel()->rowCount() - maximumCount(), 0);
}
return 0;
}
bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent)
return source_row < maximumCount();
}
#include "moc_limitermodel.cpp"

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class LimiterModel
*
* @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount.
*
* Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch.
*/
class LimiterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged)
Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged)
public:
explicit LimiterModel(QObject *parent = nullptr);
[[nodiscard]] int maximumCount() const;
void setMaximumCount(int maximumCount);
[[nodiscard]] int extraCount() const;
Q_SIGNALS:
void maximumCountChanged();
void extraCountChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
int m_maximumCount = 0;
};

View File

@@ -58,7 +58,7 @@ QVariant NotificationsModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> NotificationsModel::roleNames() const QHash<int, QByteArray> NotificationsModel::roleNames() const
{ {
return { return {
{TextRole, "notificationText"}, {TextRole, "text"},
{RoomIdRole, "roomId"}, {RoomIdRole, "roomId"},
{AuthorName, "authorName"}, {AuthorName, "authorName"},
{AuthorAvatar, "authorAvatar"}, {AuthorAvatar, "authorAvatar"},

View File

@@ -211,7 +211,6 @@ Name[pa]=ਨਵਾਂ ਸੱਦਾ
Name[pl]=Nowe zaproszenie Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite Name[pt]=Novo Convite
Name[pt_BR]=Novo convite Name[pt_BR]=Novo convite
Name[ro]=Invitație nouă
Name[ru]=Новое приглашение Name[ru]=Новое приглашение
Name[sa]=नवीन आमन्त्रणम् Name[sa]=नवीन आमन्त्रणम्
Name[sl]=Novo povabilo Name[sl]=Novo povabilo
@@ -253,13 +252,12 @@ Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala Comment[pt]=Existe um novo convite para uma sala
Comment[pt_BR]=Existe um novo convite para uma sala Comment[pt_BR]=Existe um novo convite para uma sala
Comment[ro]=E o nouă invitație la o cameră
Comment[ru]=Доступно новое приглашение в комнату Comment[ru]=Доступно новое приглашение в комнату
Comment[sa]=कक्षस्य नूतनं निमन्त्रणम् अस्ति Comment[sa]=कक्षस्य नूतनं निमन्त्रणम् अस्ति
Comment[sl]=Tam je novo povabilo v sobo Comment[sl]=Tam je novo povabilo v sobo
Comment[sv]=Det finns en ny inbjudan till ett rum Comment[sv]=Det finns en ny inbjudan till ett rum
Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது
Comment[tr]=Bir odaya yeni bir davet var Comment[tr]=Bir odaya yeni bir davetiye var
Comment[uk]=У кімнаті нове запрошення Comment[uk]=У кімнаті нове запрошення
Comment[zh_CN]=有新的聊天室邀请 Comment[zh_CN]=有新的聊天室邀请
Comment[zh_TW]=有新的加入聊天室邀請 Comment[zh_TW]=有新的加入聊天室邀請
@@ -287,13 +285,11 @@ Name[ia]=Comparti
Name[it]=Condivisione Name[it]=Condivisione
Name[ka]=გაზიარება Name[ka]=გაზიარება
Name[ko]=공유 Name[ko]=공유
Name[lt]=Bendrinti
Name[lv]=Kopīgot Name[lv]=Kopīgot
Name[nl]=Gedeelde Name[nl]=Gedeelde
Name[nn]=Del Name[nn]=Del
Name[pl]=Udostępnij Name[pl]=Udostępnij
Name[pt_BR]=Compartilhar Name[pt_BR]=Compartilhar
Name[ro]=Partajare
Name[ru]=Публикация Name[ru]=Публикация
Name[sa]=संविभागः Name[sa]=संविभागः
Name[sl]=Deli Name[sl]=Deli
@@ -323,13 +319,11 @@ Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[ko]=콘텐츠 공유 결과 Comment[ko]=콘텐츠 공유 결과
Comment[lt]=Turinio dalies bendrinimo rezultatas
Comment[lv]=Satura kopīgošanas rezultāts Comment[lv]=Satura kopīgošanas rezultāts
Comment[nl]=Het resultaat van het delen van een stukje inhoud Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald Comment[nn]=Resultatet av deling av innhald
Comment[pl]=Wynik udostępniania kawałka treści Comment[pl]=Wynik udostępniania kawałka treści
Comment[pt_BR]=O resultado de compartilhar um conteúdo Comment[pt_BR]=O resultado de compartilhar um conteúdo
Comment[ro]=Rezultatul partajării unei bucăți de conținut
Comment[ru]=Результат публикации данных Comment[ru]=Результат публикации данных
Comment[sa]=सामग्रीखण्डस्य साझाकरणस्य परिणामः Comment[sa]=सामग्रीखण्डस्य साझाकरणस्य परिणामः
Comment[sl]=Rezultat deljenega kosa vsebine Comment[sl]=Rezultat deljenega kosa vsebine

View File

@@ -66,10 +66,6 @@
</entry> </entry>
</group> </group>
<group name="Timeline"> <group name="Timeline">
<entry name="FontScale" type="double">
<label>Scaling factor for font sizes</label>
<default>1.0</default>
</entry>
<entry name="ShowAvatarInTimeline" type="bool"> <entry name="ShowAvatarInTimeline" type="bool">
<label>Show avatar in the timeline</label> <label>Show avatar in the timeline</label>
<default>true</default> <default>true</default>
@@ -193,10 +189,6 @@
<label>Don't hide any events in the timeline</label> <label>Don't hide any events in the timeline</label>
<default>false</default> <default>false</default>
</entry> </entry>
<entry name="RelateAnyEvent" type="bool">
<label>Send relations to any event, including state events and events normally hidden.</label>
<default>false</default>
</entry>
<entry name="AlwaysVerifyDevice" type="bool"> <entry name="AlwaysVerifyDevice" type="bool">
<label>Always allow device verification</label> <label>Always allow device verification</label>
<default>false</default> <default>false</default>
@@ -211,6 +203,10 @@
<label>Enable threads</label> <label>Enable threads</label>
<default>false</default> <default>false</default>
</entry> </entry>
<entry name="SecretBackup" type="bool">
<label>Enable secret backup</label>
<default>false</default>
</entry>
<entry name="Phone3PId" type="bool"> <entry name="Phone3PId" type="bool">
<label>Enable add phone numbers as 3PIDs</label> <label>Enable add phone numbers as 3PIDs</label>
<default>false</default> <default>false</default>

View File

@@ -60,8 +60,9 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
} }
if (!m_connActiveJob.contains(connection->user()->id())) { if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
m_connActiveJob.append(connection->user()->id()); m_connActiveJob.append(connection->user()->id());
connection->callApi<GetNotificationsJob>().onResult([this, connection](const auto &job) { connect(job, &BaseJob::success, this, [this, job, connection]() {
m_connActiveJob.removeAll(connection->user()->id()); m_connActiveJob.removeAll(connection->user()->id());
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id())); processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
}); });
@@ -216,12 +217,12 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
} }
}); });
notification->setTitle(room->displayName());
QString entry; QString entry;
if (room->isDirectChat()) { if (sender == room->displayName()) {
notification->setTitle(sender);
entry = text.toHtmlEscaped(); entry = text.toHtmlEscaped();
} else { } else {
notification->setTitle(room->displayName());
entry = i18n("%1: %2", sender, text.toHtmlEscaped()); entry = i18n("%1: %2", sender, text.toHtmlEscaped());
} }
@@ -253,9 +254,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setReplyAction(std::move(replyAction)); notification->setReplyAction(std::move(replyAction));
} }
if (Controller::instance().accounts()->rowCount() > 1) { notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent(); notification->sendEvent();
} }
@@ -349,9 +348,7 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
m_invitations.remove(room->id()); m_invitations.remove(room->id());
}); });
if (Controller::instance().accounts()->rowCount() > 1) { notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent(); notification->sendEvent();
} }
@@ -434,7 +431,7 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
if (room != nullptr) { if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height()); const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) { if (icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2}; const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white); painter.setBrush(Qt::white);

View File

@@ -75,7 +75,6 @@ Comment[nn]=Finn rom i NeoChat
Comment[pl]=Znajdź pokoje w NeoChat Comment[pl]=Znajdź pokoje w NeoChat
Comment[pt]=Procurar salas no NeoChat Comment[pt]=Procurar salas no NeoChat
Comment[pt_BR]=Encontrar salas no NeoChat Comment[pt_BR]=Encontrar salas no NeoChat
Comment[ro]=Găsește camere în NeoChat
Comment[ru]=Поиск комнат NeoChat Comment[ru]=Поиск комнат NeoChat
Comment[sa]=NeoChat इत्यत्र कक्ष्याः अन्वेषणं कुर्वन्तु Comment[sa]=NeoChat इत्यत्र कक्ष्याः अन्वेषणं कुर्वन्तु
Comment[sl]=Najdi sobe v NeoChatu Comment[sl]=Najdi sobe v NeoChatu
@@ -92,3 +91,4 @@ X-Plasma-API=DBus
X-Plasma-DBusRunner-Service=org.kde.neochat X-Plasma-DBusRunner-Service=org.kde.neochat
X-Plasma-DBusRunner-Path=/RoomRunner X-Plasma-DBusRunner-Path=/RoomRunner
X-Plasma-Request-Actions-Once=true X-Plasma-Request-Actions-Once=true
X-Plasma-Runner-Min-Letter-Count=3

View File

@@ -1,16 +1,16 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com> // SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.neochat import org.kde.neochat
import org.kde.neochat.settings import org.kde.neochat.settings
import org.kde.neochat.devtools
KirigamiComponents.ConvergentContextMenu { KirigamiComponents.ConvergentContextMenu {
id: root id: root
@@ -18,17 +18,21 @@ KirigamiComponents.ConvergentContextMenu {
required property NeoChatConnection connection required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window required property Kirigami.ApplicationWindow window
Kirigami.Action { QQC2.Action {
text: i18nc("@action:button", "Show QR Code") text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic" icon.name: "view-barcode-qr-symbolic"
onTriggered: { onTriggered: {
(Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, { let qrMax = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: "https://matrix.to/#/" + root.connection.localUser.id, text: "https://matrix.to/#/" + root.connection.localUser.id,
title: root.connection.localUser.displayName, title: root.connection.localUser.displayName,
subtitle: root.connection.localUser.id, subtitle: root.connection.localUser.id,
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl. // Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : "" avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : ""
}) as QrCodeMaximizeComponent).open(); });
if (typeof root.closeDialog === "function") {
root.closeDialog();
}
qrMax.open();
} }
} }
@@ -36,27 +40,26 @@ KirigamiComponents.ConvergentContextMenu {
text: i18nc("@action:inmenu", "Switch Account") text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user" icon.name: "system-switch-user"
shortcut: "Ctrl+U" shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, { onTriggered: accountSwitchDialog.createObject(QQC2.Overlay.overlay, {
connection: root.connection connection: root.connection
}) as Kirigami.Dialog).open(); }).open();
} }
QQC2.Action {
Kirigami.Action { text: i18n("Edit This Account")
text: i18nc("@action:inmenu", "Edit This Account")
icon.name: "document-edit" icon.name: "document-edit"
onTriggered: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection}); onTriggered: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection});
} }
Kirigami.Action { QQC2.Action {
text: i18nc("@action:inmenu", "Notification Settings") text: i18n("Notification Settings")
icon.name: "notifications" icon.name: "notifications"
onTriggered: { onTriggered: {
NeoChatSettingsView.open('notifications'); NeoChatSettingsView.open('notifications');
} }
} }
Kirigami.Action { QQC2.Action {
text: i18nc("@action:inmenu", "Devices") text: i18n("Devices")
icon.name: "computer-symbolic" icon.name: "computer-symbolic"
onTriggered: { onTriggered: {
NeoChatSettingsView.open('devices'); NeoChatSettingsView.open('devices');
@@ -64,10 +67,10 @@ KirigamiComponents.ConvergentContextMenu {
} }
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu", "Open Developer Tools") text: i18n("Open Developer Tools")
icon.name: "tools" icon.name: "tools"
visible: NeoChatConfig.developerTools visible: NeoChatConfig.developerTools
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'DevtoolsPage'), { onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'DevtoolsPage'), {
connection: root.connection connection: root.connection
}, { }, {
title: i18nc("@title:window", "Developer Tools"), title: i18nc("@title:window", "Developer Tools"),
@@ -79,15 +82,15 @@ KirigamiComponents.ConvergentContextMenu {
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup") text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock" icon.name: "unlock"
visible: NeoChatConfig.secretBackup
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, { onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup") title: i18nc("@title:window", "Open Key Backup")
}) })
} }
Kirigami.Action { QQC2.Action {
text: i18nc("@action:inmenu", "Verify This Device") text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low" icon.name: "security-low"
visible: !root.connection.isVerifiedSession
onTriggered: { onTriggered: {
root.connection.startSelfVerification(); root.connection.startSelfVerification();
const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, { const dialog = Qt.createComponent("org.kde.kirigami", "PromptDialog").createObject(QQC2.Overlay.overlay, {
@@ -96,17 +99,19 @@ KirigamiComponents.ConvergentContextMenu {
standardButtons: Kirigami.Dialog.Ok standardButtons: Kirigami.Dialog.Ok
}) })
dialog.open(); dialog.open();
root.connection.newKeyVerificationSession.connect(() => { root.connection.onNewKeyVerificationSession.connect(() => {
dialog.close(); dialog.close();
}); });
} }
} }
Kirigami.Action { QQC2.Action {
text: i18nc("@action:inmenu", "Logout") text: i18n("Logout")
icon.name: "im-kick-user" icon.name: "im-kick-user"
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, { onTriggered: confirmLogoutDialogComponent.createObject(root).open()
connection: root.connection }
}) as Kirigami.Dialog).open()
readonly property Component confirmLogoutDialogComponent: ConfirmLogoutDialog {
connection: root.connection
} }
} }

View File

@@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com> // SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -19,6 +16,8 @@ Kirigami.Dialog {
required property NeoChatConnection connection required property NeoChatConnection connection
parent: applicationWindow().overlay
leftPadding: 0 leftPadding: 0
rightPadding: 0 rightPadding: 0
topPadding: 0 topPadding: 0
@@ -26,7 +25,7 @@ Kirigami.Dialog {
standardButtons: Kirigami.Dialog.NoButton standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24) width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title: dialog to switch between logged in accounts", "Switch Account") title: i18nc("@title: dialog to switch between logged in accounts", "Switch Account")
onVisibleChanged: if (visible) { onVisibleChanged: if (visible) {
@@ -54,14 +53,14 @@ Kirigami.Dialog {
} }
text: i18nc("@button: login to or register a new account.", "Add Account") text: i18nc("@button: login to or register a new account.", "Add Account")
contentItem: Delegates.SubtitleContentItem { contentItem: Delegates.SubtitleContentItem {
itemDelegate: addDelegate itemDelegate: parent
subtitle: i18nc("@info", "Log in or create a new account") subtitle: i18n("Log in or create a new account")
labelItem.textFormat: Text.PlainText labelItem.textFormat: Text.PlainText
subtitleItem.textFormat: Text.PlainText subtitleItem.textFormat: Text.PlainText
} }
onClicked: { onClicked: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, { pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
title: i18nc("@title:window", "Login") title: i18nc("@title:window", "Login")
}); });
root.close(); root.close();
@@ -95,8 +94,8 @@ Kirigami.Dialog {
accountView.decrementCurrentIndex(); accountView.decrementCurrentIndex();
} }
} }
Keys.onEnterPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked() Keys.onEnterPressed: accountView.currentItem.clicked()
Keys.onReturnPressed: ((accountView.currentItem ?? accountView.footerItem) as Delegates.RoundedItemDelegate).clicked() Keys.onReturnPressed: accountView.currentItem.clicked()
onVisibleChanged: { onVisibleChanged: {
for (let i = 0; i < accountView.count; i++) { for (let i = 0; i < accountView.count; i++) {

View File

@@ -6,6 +6,8 @@ import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Dialog { Kirigami.Dialog {
id: root id: root
@@ -18,7 +20,7 @@ Kirigami.Dialog {
title: i18nc("@title:dialog", "Start a chat") title: i18nc("@title:dialog", "Start a chat")
contentItem: QQC2.Label { contentItem: QQC2.Label {
text: i18nc("@info", "Do you want to start a chat with %1?", root.user.displayName) text: i18n("Do you want to start a chat with %1?", root.user.displayName)
textFormat: Text.PlainText textFormat: Text.PlainText
wrapMode: Text.Wrap wrapMode: Text.Wrap
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter

View File

@@ -33,7 +33,7 @@ ColumnLayout {
} }
QQC2.ToolButton { QQC2.ToolButton {
id: editImageButton id: editImageButton
visible: root.hasImage visible: hasImage
icon.name: "document-edit" icon.name: "document-edit"
text: i18n("Edit") text: i18n("Edit")
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
@@ -46,9 +46,9 @@ ColumnLayout {
} }
onClicked: { onClicked: {
let imageEditor = (Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(imageEditorPage); let imageEditor = applicationWindow().pageStack.pushDialogLayer(imageEditorPage);
imageEditor.newPathChanged.connect(function (newPath) { imageEditor.newPathChanged.connect(function (newPath) {
imageEditor.closeDialog(); applicationWindow().pageStack.layers.pop();
root.attachmentPath = newPath; root.attachmentPath = newPath;
}); });
} }
@@ -58,18 +58,14 @@ ColumnLayout {
QQC2.ToolButton { QQC2.ToolButton {
id: cancelAttachmentButton id: cancelAttachmentButton
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel sending attachment") action: Kirigami.Action {
icon.name: "dialog-close" text: i18n("Cancel sending attachment")
onClicked: root.attachmentCancelled() icon.name: "dialog-close"
onTriggered: attachmentCancelled()
shortcut: "Escape"
}
QQC2.ToolTip.text: text QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Kirigami.Action {
shortcut: "Escape"
onTriggered: cancelAttachmentButton.clicked()
}
} }
} }
@@ -79,8 +75,8 @@ ColumnLayout {
asynchronous: true asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly. cache: false // Cache is not needed. Images will rarely be shown repeatedly.
source: root.hasImage ? root.attachmentPath : "" source: hasImage ? root.attachmentPath : ""
visible: root.hasImage visible: hasImage
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
onSourceChanged: { onSourceChanged: {
@@ -118,11 +114,11 @@ ColumnLayout {
id: mimetypeIcon id: mimetypeIcon
implicitWidth: Kirigami.Units.iconSizes.smallMedium implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium implicitHeight: Kirigami.Units.iconSizes.smallMedium
source: root.attachmentMimetype.iconName source: attachmentMimetype.iconName
} }
QQC2.Label { QQC2.Label {
id: fileLabel id: fileLabel
text: root.baseFileName text: baseFileName
} }
} }
} }

View File

@@ -3,9 +3,12 @@
// SPDX-License-Identifier: LGPL-2.0-or-later // SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Templates as T
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
Delegates.RoundedItemDelegate { Delegates.RoundedItemDelegate {
id: root id: root

View File

@@ -48,7 +48,7 @@ Components.AbstractMaximizeComponent {
implicitWidth: Kirigami.Units.iconSizes.medium implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium implicitHeight: Kirigami.Units.iconSizes.medium
name: root.author.displayName name: root.author.name ?? root.author.displayName
source: root.author.avatarUrl source: root.author.avatarUrl
color: root.author.color color: root.author.color
} }
@@ -57,7 +57,7 @@ Components.AbstractMaximizeComponent {
QQC2.Label { QQC2.Label {
id: userLabel id: userLabel
text: root.author.displayName text: root.author.name ?? root.author.displayName
color: root.author.color color: root.author.color
font.weight: Font.Bold font.weight: Font.Bold
elide: Text.ElideRight elide: Text.ElideRight
@@ -91,7 +91,6 @@ Components.AbstractMaximizeComponent {
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
font.family: "monospace" font.family: "monospace"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
Kirigami.SpellCheck.enabled: false Kirigami.SpellCheck.enabled: false

View File

@@ -8,6 +8,7 @@ import QtQml.Models
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.kitemmodels
import org.kde.neochat import org.kde.neochat

86
src/app/qml/EditMenu.qml Normal file
View File

@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
import Qt.labs.platform as Labs
import QtQuick
import QtQuick.Layouts
Labs.Menu {
id: root
required property Item field
Labs.MenuItem {
enabled: root.field !== null && root.field.canUndo
text: i18nc("text editing menu action", "Undo")
shortcut: StandardKey.Undo
onTriggered: {
root.field.undo();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.canRedo
text: i18nc("text editing menu action", "Redo")
shortcut: StandardKey.Redo
onTriggered: {
root.field.undo();
root.close();
}
}
Labs.MenuSeparator {}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText
text: i18nc("text editing menu action", "Cut")
shortcut: StandardKey.Cut
onTriggered: {
root.field.cut();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText
text: i18nc("text editing menu action", "Copy")
shortcut: StandardKey.Copy
onTriggered: {
root.field.copy();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.canPaste
text: i18nc("text editing menu action", "Paste")
shortcut: StandardKey.Paste
onTriggered: {
root.field.paste();
root.close();
}
}
Labs.MenuItem {
enabled: root.field !== null && root.field.selectedText !== ""
text: i18nc("text editing menu action", "Delete")
shortcut: ""
onTriggered: {
root.field.remove(root.field.selectionStart, root.field.selectionEnd);
root.close();
}
}
Labs.MenuSeparator {}
Labs.MenuItem {
enabled: root.field !== null
text: i18nc("text editing menu action", "Select All")
shortcut: StandardKey.SelectAll
onTriggered: {
root.field.selectAll();
root.close();
}
}
}

View File

@@ -6,15 +6,16 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.neochat
ColumnLayout { ColumnLayout {
id: root id: root
required property string emoji property alias emoji: emojiLabel.text
required property string description property alias description: descriptionLabel.text
QQC2.Label { QQC2.Label {
text: root.emoji id: emojiLabel
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.iconSizes.huge Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge Layout.preferredHeight: Kirigami.Units.iconSizes.huge
@@ -24,7 +25,7 @@ ColumnLayout {
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4 font.pointSize: Kirigami.Theme.defaultFont.pointSize * 4
} }
QQC2.Label { QQC2.Label {
text: root.description id: descriptionLabel
Layout.fillWidth: true Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter

View File

@@ -17,6 +17,9 @@ RowLayout {
Repeater { Repeater {
id: repeater id: repeater
delegate: EmojiItem {} delegate: EmojiItem {
emoji: modelData.emoji
description: modelData.description
}
} }
} }

View File

@@ -17,7 +17,7 @@ ApplicationWindow {
property real longitude: NaN property real longitude: NaN
property string asset property string asset
property var author property var author
property LiveLocationsModel liveLocationModel: null property QtObject liveLocationModel: null
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen visibility: Qt.WindowFullScreen
@@ -59,7 +59,7 @@ ApplicationWindow {
Connections { Connections {
target: mapView.map target: mapView.map
function onCopyrightLinkActivated(link: string) { function onCopyrightLinkActivated() {
Qt.openUrlExternally(link); Qt.openUrlExternally(link);
} }
} }

View File

@@ -5,6 +5,7 @@ import Qt.labs.platform as Labs
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -15,48 +16,12 @@ Labs.MenuBar {
id: root id: root
required property NeoChatConnection connection required property NeoChatConnection connection
required property Kirigami.ApplicationWindow appWindow
Labs.Menu { Labs.Menu {
title: i18nc("menu", "File") title: i18nc("menu", "NeoChat")
Labs.MenuItem { Labs.MenuItem {
icon.name: "list-add-user" enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat…")
text: i18nc("@action:inmenu", "Find User")
enabled: root.connection
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find User")
})
}
Labs.MenuItem {
icon.name: "system-users-symbolic"
text: i18nc("@action:inmenu", "Create a Room…")
enabled: root.connection
shortcut: StandardKey.New
onTriggered: {
Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root.appWindow, {
connection: root.connection
}, {
title: i18nc("@title", "Create a Room")
}).open();
}
}
Labs.MenuItem {
icon.name: "compass-symbolic"
text: i18nc("@action:inmenu Explore public rooms and spaces", "Explore")
enabled: root.connection
onTriggered: {
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
}
}
Labs.MenuItem {
text: i18nc("menu", "Configure NeoChat…") text: i18nc("menu", "Configure NeoChat…")
shortcut: StandardKey.Preferences shortcut: StandardKey.Preferences
@@ -69,15 +34,58 @@ Labs.MenuBar {
onTriggered: Qt.quit() onTriggered: Qt.quit()
} }
} }
Labs.Menu {
title: i18nc("menu", "File")
Labs.MenuItem {
icon.name: "list-add-user"
text: i18nc("@action:inmenu", "Find your Friends")
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
Labs.MenuItem {
icon.name: "system-users-symbolic"
text: i18nc("@action:inmenu", "Create a Room…")
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
shortcut: StandardKey.New
onTriggered: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Create a Room")
});
}
}
Labs.MenuItem {
icon.name: "compass-symbolic"
text: i18nc("@action:inmenu", "Explore Rooms")
onTriggered: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
}
}
}
EditMenu {
title: i18nc("menu", "Edit")
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
}
Labs.Menu { Labs.Menu {
title: i18nc("menu", "View") title: i18nc("menu", "View")
Labs.MenuItem { Labs.MenuItem {
icon.name: "search-symbolic" icon.name: "search-symbolic"
enabled: root.connection
text: i18nc("@action:inmenu opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Search Rooms") text: i18nc("@action:inmenu opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Search Rooms")
onTriggered: (root.appWindow as Main).quickSwitcher.open() onTriggered: quickSwitcher.open()
} }
} }
Labs.Menu { Labs.Menu {
@@ -85,8 +93,8 @@ Labs.MenuBar {
Labs.MenuItem { Labs.MenuItem {
icon.name: "view-fullscreen-symbolic" icon.name: "view-fullscreen-symbolic"
text: root.appWindow.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen") text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
onTriggered: root.appWindow.visibility === Window.FullScreen ? root.appWindow.showNormal() : root.appWindow.showFullScreen() onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
} }
} }
Labs.Menu { Labs.Menu {
@@ -95,12 +103,12 @@ Labs.MenuBar {
Labs.MenuItem { Labs.MenuItem {
icon.name: "help-about-symbolic" icon.name: "help-about-symbolic"
text: i18nc("menu", "About NeoChat") text: i18nc("menu", "About NeoChat")
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage")) onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
} }
Labs.MenuItem { Labs.MenuItem {
icon.name: "kde-symbolic" icon.name: "kde-symbolic"
text: i18nc("menu", "About KDE") text: i18nc("menu", "About KDE")
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage")) onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
} }
} }
} }

View File

@@ -3,10 +3,8 @@
import QtQuick import QtQuick
import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat
Item { Item {
required property NeoChatConnection connection required property NeoChatConnection connection
required property Kirigami.ApplicationWindow appWindow
} }

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