Compare commits

..

102 Commits

Author SHA1 Message Date
l10n daemon script
2ea6e0425a GIT_SILENT Sync po/docbooks with svn 2025-11-03 03:13:11 +00:00
Heiko Becker
fe55ff19d2 Fix version in appstream file
GIT_SILENT

(cherry picked from commit 40694f502a)
2025-11-01 20:02:22 +01:00
Heiko Becker
c35bdc0592 GIT_SILENT Upgrade release service version to 25.08.3. 2025-10-31 01:57:04 +01:00
Heiko Becker
c6fa5a10dd GIT_SILENT Update Appstream for new release 2025-10-31 01:23:14 +01:00
l10n daemon script
f956c33b82 GIT_SILENT Sync po/docbooks with svn 2025-10-29 03:19:25 +00:00
Joshua Goins
0b9295e67e Show less scary icon for neutral reasons when cancelling verification
For example, accepting a verification on another device shows a giant
red security icon which isn't really suitable. I chucked the
dialog-information icon in for some of the neutral-sounding messages so
this dialog can be a little less intimidating.

BUG: 510421
FIXED-IN: 24.08.3


(cherry picked from commit be92e56c3a)

Co-authored-by: Joshua Goins <josh@redstrate.com>
2025-10-28 09:16:51 -04:00
Joshua Goins
0fdade83e0 Fix jump to previous/next unread room shortcuts
These were dependent on a renamed property and ended up breaking, oops.

(cherry picked from commit c778ba8b24)
2025-10-27 18:09:42 -04:00
l10n daemon script
6a25945131 GIT_SILENT Sync po/docbooks with svn 2025-10-25 03:20:51 +00:00
l10n daemon script
218222bc58 GIT_SILENT made messages (after extraction) 2025-10-22 02:35:06 +00:00
l10n daemon script
4300bba804 GIT_SILENT Sync po/docbooks with svn 2025-10-21 03:15:16 +00:00
l10n daemon script
13f766d166 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-21 03:11:53 +00:00
l10n daemon script
fd4e701c51 GIT_SILENT Sync po/docbooks with svn 2025-10-18 04:10:39 +00:00
l10n daemon script
d8ff639374 GIT_SILENT Sync po/docbooks with svn 2025-10-17 04:09:02 +00:00
l10n daemon script
4a50281152 GIT_SILENT Sync po/docbooks with svn 2025-10-16 03:17:41 +00:00
l10n daemon script
acc9289d06 GIT_SILENT Sync po/docbooks with svn 2025-10-14 03:18:05 +00:00
l10n daemon script
235bd21eaf GIT_SILENT Sync po/docbooks with svn 2025-10-09 03:23:35 +00:00
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
308 changed files with 58945 additions and 93748 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

@@ -14,6 +14,7 @@ Dependencies:
'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'
@@ -28,15 +29,12 @@ 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', 'Android']
'require':
'libraries/kunifiedpush': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'frameworks/kdbusaddons': '@latest-kf6' 'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6' 'frameworks/purpose': '@latest-kf6'
'libraries/kunifiedpush': '@latest-kf6'
- 'on': ['Linux'] - 'on': ['Linux']
'require': 'require':
@@ -45,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 "3")
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) 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"
@@ -148,7 +149,7 @@ set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
option(WITH_UNIFIEDPUSH "Build with KUnifiedPush support" ON) option(WITH_UNIFIEDPUSH "Build with KUnifiedPush support" ON)
if (APPLE OR WIN32 OR HAIKU) if (ANDROID OR APPLE OR WIN32 OR HAIKU)
set(WITH_UNIFIEDPUSH OFF) set(WITH_UNIFIEDPUSH OFF)
endif() endif()

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

@@ -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();

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

@@ -306,8 +306,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 +320,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>
@@ -487,7 +488,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.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/> <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"/>

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

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

@@ -48,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
@@ -103,11 +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
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
@@ -119,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
@@ -203,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
@@ -213,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)
@@ -323,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}
) )
@@ -342,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})
@@ -352,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,9 +33,13 @@
#include <KWindowSystem> #include <KWindowSystem>
#endif #endif
#include <KLocalizedQmlContext> #if __has_include("KCrash")
#include <KCrash>
#endif
#include <KIconTheme>
#include <KLocalizedContext>
#include <KLocalizedString> #include <KLocalizedString>
#include <KirigamiApp>
#include "neochat-version.h" #include "neochat-version.h"
@@ -100,6 +104,7 @@ Q_DECL_EXPORT
#endif #endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true); QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW #ifdef HAVE_WEBVIEW
@@ -108,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);
@@ -158,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;
@@ -256,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)) {
@@ -271,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

@@ -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,7 +25,11 @@ class CommonRoomsModel : public QAbstractListModel
public: public:
enum Roles { enum Roles {
RoomIdRole = Qt::DisplayRole, TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
}; };
Q_ENUM(Roles) Q_ENUM(Roles)

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

@@ -259,7 +259,7 @@ 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]=有新的加入聊天室邀請

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()));
}); });
@@ -430,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

@@ -92,3 +92,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,50 +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 Rooms")
enabled: root.connection
onTriggered: {
let dialog = root.appWindow.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");
});
}
}
Labs.MenuItem {
text: i18nc("menu", "Configure NeoChat…") text: i18nc("menu", "Configure NeoChat…")
shortcut: StandardKey.Preferences shortcut: StandardKey.Preferences
@@ -71,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 {
@@ -87,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 {
@@ -97,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
} }

View File

@@ -3,54 +3,29 @@
// 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 org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
RowLayout { QQC2.Control {
id: root id: root
property string text property string text
onTextChanged: { visible: !root.text.startsWith("https://matrix.to/") && root.text.length > 0
// This is done so the text doesn't disappear for a split second while in the opacity transition Kirigami.Theme.colorSet: Kirigami.Theme.View
if (root.text.length > 0) {
urlLabel.text = root.text z: 20
}
Accessible.ignored: true
contentItem: QQC2.Label {
text: root.text.startsWith("https://matrix.to/") ? "" : root.text
elide: Text.ElideRight
Accessible.description: i18nc("@info screenreader", "The currently selected link")
} }
z: 99 background: Rectangle {
spacing: 0 color: Kirigami.Theme.backgroundColor
opacity: (!root.text.startsWith("https://matrix.to/") && root.text.length > 0) ? 1 : 0
visible: opacity > 0
Behavior on opacity {
OpacityAnimator {
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
}
QQC2.Control {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Accessible.ignored: true
contentItem: QQC2.Label {
id: urlLabel
elide: Text.ElideRight
}
background: Kirigami.ShadowedRectangle {
corners.topRightRadius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
width: 1
}
}
} }
} }

View File

@@ -2,8 +2,6 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.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.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
@@ -59,16 +57,6 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
Kirigami.SelectableLabel {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
textFormat: TextEdit.PlainText
visible: root.currentRoom && root.currentRoom.canonicalAlias
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor
horizontalAlignment: Text.AlignHCenter
}
} }
} }
@@ -82,14 +70,7 @@ ColumnLayout {
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
Kirigami.Heading { Kirigami.Heading {
text: root.invitingMember.displayName text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
}
QQC2.Label {
text: root.invitingMember.id
color: Kirigami.Theme.disabledTextColor
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@@ -178,7 +159,7 @@ ColumnLayout {
QQC2.Label { QQC2.Label {
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
text: xi18nc("@info:label Ensure you are referring to the same translation used for that settings page", "You can reject invitations from unknown users under the <interface>Security & Safety</interface> settings.") text: i18nc("@info:label", "You can reject invitations from unknown users under Security settings.")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
// + 5 to prevent it from wrapping unnecessarily // + 5 to prevent it from wrapping unnecessarily

View File

@@ -8,6 +8,7 @@ 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.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.prison
import org.kde.neochat import org.kde.neochat
@@ -24,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", "Join Room") title: i18nc("@title:dialog", "Join Room")
contentItem: ColumnLayout { contentItem: ColumnLayout {
@@ -66,7 +67,7 @@ Kirigami.Dialog {
text: i18nc("@action:button", "Join room") text: i18nc("@action:button", "Join room")
icon.name: "irc-join-channel" icon.name: "irc-join-channel"
onClicked: { onClicked: {
RoomManager.resolveResource(root.room, "join_confirmed"); RoomManager.resolveResource(root.room, "join");
root.close(); root.close();
} }
} }

View File

@@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import QtQml import QtQml
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -51,22 +50,6 @@ Kirigami.Page {
sourceComponent: message sourceComponent: message
} }
}, },
State {
name: "waitingForKey"
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
PropertyChanges {
target: stateLoader
sourceComponent: message
}
},
State {
name: "waitingForAccept"
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
PropertyChanges {
target: stateLoader
sourceComponent: message
}
},
State { State {
name: "waitingForMac" name: "waitingForMac"
when: root.session.state === KeyVerificationSession.WAITINGFORMAC when: root.session.state === KeyVerificationSession.WAITINGFORMAC
@@ -144,9 +127,7 @@ Kirigami.Page {
case KeyVerificationSession.WAITINGFORREADY: case KeyVerificationSession.WAITINGFORREADY:
case KeyVerificationSession.INCOMING: case KeyVerificationSession.INCOMING:
case KeyVerificationSession.WAITINGFORMAC: case KeyVerificationSession.WAITINGFORMAC:
case KeyVerificationSession.WAITINGFORKEY: return "security-medium-symbolic";
case KeyVerificationSession.WAITINGFORACCEPT:
return "security-medium-symbolic";
case KeyVerificationSession.DONE: case KeyVerificationSession.DONE:
return "security-high"; return "security-high";
default: default:
@@ -160,19 +141,13 @@ Kirigami.Page {
case KeyVerificationSession.INCOMING: case KeyVerificationSession.INCOMING:
return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId); return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId);
case KeyVerificationSession.WAITINGFORMAC: case KeyVerificationSession.WAITINGFORMAC:
return i18n("Waiting for other party to send us keys.");
case KeyVerificationSession.WAITINGFORKEY:
return i18n("Waiting for other party to confirm our keys.");
case KeyVerificationSession.WAITINGFORACCEPT:
return i18n("Waiting for other party to verify."); return i18n("Waiting for other party to verify.");
case KeyVerificationSession.DONE: case KeyVerificationSession.DONE:
return i18n("Successfully verified device **%1**", root.session.remoteDeviceId); return i18n("Successfully verified device **%1**", root.session.remoteDeviceId)
default: default:
return ""; return "";
} }
} }
isDone: root.session.state === KeyVerificationSession.DONE
onDone: root.closeDialog()
} }
} }

View File

@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.config as KConfig import org.kde.config as KConfig
@@ -19,11 +20,6 @@ Kirigami.ApplicationWindow {
property bool initialized: false property bool initialized: false
readonly property QuickSwitcher quickSwitcher: QuickSwitcher {
connection: root.connection
window: root
}
title: { title: {
if (NeoChatConfig.windowTitleFocus) { if (NeoChatConfig.windowTitleFocus) {
return activeFocusItem + " " + (activeFocusItem ? activeFocusItem.Accessible.name : ""); return activeFocusItem + " " + (activeFocusItem ? activeFocusItem.Accessible.name : "");
@@ -87,7 +83,6 @@ Kirigami.ApplicationWindow {
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
sourceComponent: GlobalMenu { sourceComponent: GlobalMenu {
connection: root.connection connection: root.connection
appWindow: root
} }
} }
@@ -95,13 +90,19 @@ Kirigami.ApplicationWindow {
configGroupName: "MainWindow" configGroupName: "MainWindow"
} }
QuickSwitcher {
id: quickSwitcher
connection: root.connection
}
Connections { Connections {
target: RoomManager target: RoomManager
function onCurrentRoomChanged() { function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) { if (RoomManager.currentRoom && pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage')); let roomPage = pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'), {
roomPage.forceActiveFocus(); connection: root.connection
});
roomPage.backRequested.connect(event => { roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom(); RoomManager.clearCurrentRoom();
}); });
@@ -109,26 +110,33 @@ Kirigami.ApplicationWindow {
} }
function onAskJoinRoom(room) { function onAskJoinRoom(room) {
(Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, { Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
room: room, room: room,
connection: root.connection connection: root.connection
}) as JoinRoomDialog).open(); }).open();
} }
function onShowUserDetail(user, room) { function onShowUserDetail(user, room) {
root.showUserDetail(user, room); root.showUserDetail(user, room);
} }
function goToEvent(event) {
if (event.length > 0) {
roomItem.goToEvent(event);
}
roomItem.forceActiveFocus();
}
function onAskDirectChatConfirmation(user) { function onAskDirectChatConfirmation(user) {
(Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, { Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
user: user user: user
}) as AskDirectChatConfirmation).open(); }).open();
} }
function onExternalUrl(url) { function onExternalUrl(url) {
(Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this, { let dialog = Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this);
link: url dialog.link = url;
}) as ConfirmUrlDialog).open(); dialog.open();
} }
} }
@@ -192,7 +200,7 @@ Kirigami.ApplicationWindow {
dim = false; dim = false;
} }
} }
enabled: RoomManager.hasOpenRoom && root.pageStack.layers.depth < 2 && root.pageStack.depth < 3 && (root.pageStack.visibleItems.length > 1 || root.pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 && (pageStack.visibleItems.length > 1 || pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
handleVisible: enabled handleVisible: enabled
} }
@@ -214,10 +222,10 @@ Kirigami.ApplicationWindow {
Connections { Connections {
target: NeoChatConfig target: NeoChatConfig
function onBlurChanged() { function onBlurChanged() {
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout); WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
} }
function onCompactLayoutChanged() { function onCompactLayoutChanged() {
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout); WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
} }
} }
@@ -234,7 +242,7 @@ Kirigami.ApplicationWindow {
RoomListPage { RoomListPage {
id: roomList id: roomList
onSearch: root.quickSwitcher.open() onSearch: quickSwitcher.open()
connection: root.connection connection: root.connection
@@ -268,23 +276,12 @@ Kirigami.ApplicationWindow {
} }
} }
Connections {
target: root.connection
function onLoggedOut(): void {
root.pageStack.clear();
let page = root.pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {
showExisting: true,
}) as WelcomePage;
page.connectionChosen.connect(() => root.load())
}
}
Connections { Connections {
target: AccountRegistry target: AccountRegistry
function onRowsRemoved() { function onRowsRemoved() {
if (AccountRegistry.rowCount() === 0) { if (AccountRegistry.rowCount() === 0) {
root.pageStack.clear(); pageStack.clear();
root.pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage')); pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
} }
} }
} }
@@ -293,7 +290,7 @@ Kirigami.ApplicationWindow {
target: Controller target: Controller
function onErrorOccured(error) { function onErrorOccured(error) {
root.showPassiveNotification(error, "short"); showPassiveNotification(error, "short");
} }
} }
@@ -308,9 +305,9 @@ Kirigami.ApplicationWindow {
}); });
} }
function onUserConsentRequired(url) { function onUserConsentRequired(url) {
(Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, { Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
url: url url: url
}) as ConsentDialog).open(); }).open();
} }
} }
@@ -341,7 +338,7 @@ Kirigami.ApplicationWindow {
} }
} }
function handleShare(): void { function handleShare(): void {
const dialog = root.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), { const dialog = applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ChooseRoomDialog'), {
connection: root.connection connection: root.connection
}, { }, {
title: i18nc("@title", "Share"), title: i18nc("@title", "Share"),
@@ -358,12 +355,8 @@ Kirigami.ApplicationWindow {
room: room, room: room,
user: user, user: user,
connection: root.connection, connection: root.connection,
}) as UserDetailDialog; });
// FIXME: The reason why we don't want the focusedWindowItem for the room null case (aka QR codes) is because it will parent it to the soon-to-be-destroyed window item. dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
// But this won't be a problem if we turn it into a Kirigami.Dialog or some other in-scene item, which it really should be.
if (room != null) {
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
}
dialog.open(); dialog.open();
} }
@@ -372,7 +365,9 @@ Kirigami.ApplicationWindow {
RoomManager.loadInitialRoom(); RoomManager.loadInitialRoom();
if (!Kirigami.Settings.isMobile) { if (!Kirigami.Settings.isMobile) {
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage')); let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'), {
connection: root.connection
});
roomPage.forceActiveFocus(); roomPage.forceActiveFocus();
} }

View File

@@ -21,7 +21,7 @@ Kirigami.Dialog {
/** /**
* @brief Thrown when a user is selected. * @brief Thrown when a user is selected.
*/ */
signal userSelected(string userId) signal userSelected
title: i18nc("@title", "User ID") title: i18nc("@title", "User ID")
@@ -38,7 +38,7 @@ Kirigami.Dialog {
text: i18n("OK") text: i18n("OK")
icon.name: "dialog-ok" icon.name: "dialog-ok"
onTriggered: { onTriggered: {
root.userSelected(userIdText.text) root.connection.requestDirectChat(userIdText.text);
root.accept(); root.accept();
} }
} }

View File

@@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import org.kde.kirigami as Kirigami
Kirigami.PromptDialog {
id: root
required property bool hasExistingMeeting
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.")
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: Kirigami.Action {
icon.name: "camera-video-symbolic"
text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
onTriggered: root.accept()
}
}

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