Compare commits

..

193 Commits

Author SHA1 Message Date
Ulterno Ω*
d26a691647 Fix saving images
Fixed variable names and scope resolution for saveAsDialog in NeochatMaximizeComponent 

Hence, correcting the File Dialog type and making the downloadFile function work.

BUG: 479053
2024-02-11 12:17:39 +00:00
Heiko Becker
0cd0a6a672 GIT_SILENT Update Appstream for new release 2024-02-10 00:24:40 +01:00
Heiko Becker
4892e12918 GIT_SILENT Upgrade release service version to 23.08.5. 2024-02-09 22:59:27 +01:00
l10n daemon script
6baa374a1a GIT_SILENT Sync po/docbooks with svn 2024-02-06 04:46:26 +00:00
l10n daemon script
d2f1fc7175 GIT_SILENT Sync po/docbooks with svn 2024-02-05 04:43:50 +00:00
Ingo Klöcker
8e998ac72b Revert "Test the unified Windows signer and disable all other jobs"
This reverts commit eabc0e3a65.
2024-01-31 17:12:50 +01:00
Ingo Klöcker
eabc0e3a65 Test the unified Windows signer and disable all other jobs 2024-01-31 14:58:49 +01:00
l10n daemon script
ba7b6da025 GIT_SILENT Sync po/docbooks with svn 2024-01-30 03:53:17 +00:00
l10n daemon script
cb7c33075c GIT_SILENT Sync po/docbooks with svn 2024-01-26 03:55:12 +00:00
l10n daemon script
3b94847b17 GIT_SILENT Sync po/docbooks with svn 2024-01-22 04:44:31 +00:00
l10n daemon script
0e9512b9ea GIT_SILENT Sync po/docbooks with svn 2024-01-17 03:53:37 +00:00
l10n daemon script
33f8b5eb55 GIT_SILENT Sync po/docbooks with svn 2024-01-14 04:51:05 +00:00
l10n daemon script
028d8c0584 GIT_SILENT Sync po/docbooks with svn 2024-01-11 03:22:26 +00:00
l10n daemon script
bf4f1a34dc GIT_SILENT Sync po/docbooks with svn 2024-01-07 03:59:53 +00:00
l10n daemon script
5ea0f5d378 GIT_SILENT Sync po/docbooks with svn 2024-01-02 03:04:04 +00:00
l10n daemon script
5b76e8cda8 GIT_SILENT Sync po/docbooks with svn 2024-01-01 03:10:26 +00:00
l10n daemon script
4c62cf4b0a GIT_SILENT Sync po/docbooks with svn 2023-12-31 04:01:16 +00:00
l10n daemon script
2dfa11a0aa 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"
2023-12-31 03:45:22 +00:00
l10n daemon script
367d106d14 GIT_SILENT Sync po/docbooks with svn 2023-12-24 04:05:37 +00:00
l10n daemon script
6404e66686 GIT_SILENT Sync po/docbooks with svn 2023-12-23 03:37:43 +00:00
l10n daemon script
1e3b5c5d13 GIT_SILENT Sync po/docbooks with svn 2023-12-21 04:05:21 +00:00
l10n daemon script
ba26d998c0 GIT_SILENT Sync po/docbooks with svn 2023-12-19 03:07:22 +00:00
l10n daemon script
e624f34710 GIT_SILENT Sync po/docbooks with svn 2023-12-18 04:06:01 +00:00
Ingo Klöcker
36c3850cb3 Add GitLab jobs to sign and optionally publish Microsoft APPX packages 2023-12-16 15:05:16 +00:00
l10n daemon script
10e570b429 GIT_SILENT made messages (after extraction) 2023-12-16 02:30:52 +00:00
l10n daemon script
5cd0872904 GIT_SILENT Sync po/docbooks with svn 2023-12-15 03:05:59 +00:00
l10n daemon script
c1ce40ce99 GIT_SILENT made messages (after extraction) 2023-12-13 02:28:15 +00:00
l10n daemon script
0da2be9ad0 GIT_SILENT Sync po/docbooks with svn 2023-12-12 03:12:59 +00:00
l10n daemon script
be24122eb8 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"
2023-12-12 03:06:24 +00:00
l10n daemon script
e6803f4e28 GIT_SILENT Sync po/docbooks with svn 2023-12-10 04:03:02 +00:00
Ingo Klöcker
3b367b7c9d Build Windows installers on invent 2023-12-04 08:43:36 +01:00
l10n daemon script
71cda237b1 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"
2023-12-03 03:01:48 +00:00
l10n daemon script
bb6f7ce985 GIT_SILENT Sync po/docbooks with svn 2023-12-01 03:08:22 +00:00
Heiko Becker
17b14aa556 GIT_SILENT Update Appstream for new release 2023-11-30 18:34:56 +01:00
Heiko Becker
7341e5824a GIT_SILENT Upgrade release service version to 23.08.4. 2023-11-30 17:50:36 +01:00
l10n daemon script
b9b5264fcf GIT_SILENT made messages (after extraction) 2023-11-29 14:32:26 +00:00
l10n daemon script
3388f8a60d GIT_SILENT Sync po/docbooks with svn 2023-11-29 03:19:07 +00:00
l10n daemon script
d878ef1804 GIT_SILENT Sync po/docbooks with svn 2023-11-26 04:08:29 +00:00
Tobias Fella
7e8a8615b1 Fix crash for forbidden non-mxc media
BUG: 476698
2023-11-24 18:47:26 +01:00
l10n daemon script
c0f9b6ba93 GIT_SILENT Sync po/docbooks with svn 2023-11-24 03:05:25 +00:00
l10n daemon script
4c0eb363fa GIT_SILENT Sync po/docbooks with svn 2023-11-20 04:00:29 +00:00
l10n daemon script
2ee3e4d81d GIT_SILENT made messages (after extraction) 2023-11-19 02:41:07 +00:00
l10n daemon script
2cf3070e9f GIT_SILENT Sync po/docbooks with svn 2023-11-18 03:04:23 +00:00
l10n daemon script
207b554c78 GIT_SILENT Sync po/docbooks with svn 2023-11-15 03:10:40 +00:00
l10n daemon script
2a54dbc108 GIT_SILENT Sync po/docbooks with svn 2023-11-13 03:08:15 +00:00
l10n daemon script
0aea54a119 GIT_SILENT Sync po/docbooks with svn 2023-11-12 03:38:37 +00:00
Heiko Becker
753e879556 GIT_SILENT Update Appstream for new release 2023-11-04 12:33:12 +01:00
Heiko Becker
6e6636e384 GIT_SILENT Upgrade release service version to 23.08.3. 2023-11-04 11:43:54 +01:00
l10n daemon script
3574c2e077 GIT_SILENT Sync po/docbooks with svn 2023-11-04 03:06:08 +00:00
Scarlett Moore
f947320191 snapcraft: remove dbus slots rejected by store. 2023-11-03 10:45:12 +00:00
Ingo Klöcker
0f8ed0bfc7 Add CI template for building APKs 2023-11-02 15:06:57 +01:00
l10n daemon script
ccd0bd5c6d GIT_SILENT Sync po/docbooks with svn 2023-11-02 03:08:48 +00:00
Scarlett Moore
a4020a7def snapcraft: Fix libsecret support! BUndle gnomekeychain. Set XDG variables so it finds keychain. Update qtkeychain. 2023-11-01 16:26:13 +00:00
l10n daemon script
1c9487df7e GIT_SILENT Sync po/docbooks with svn 2023-11-01 03:12:16 +00:00
l10n daemon script
526db7b73a GIT_SILENT Sync po/docbooks with svn 2023-10-26 03:29:34 +00:00
l10n daemon script
d9024e98c9 GIT_SILENT made messages (after extraction) 2023-10-26 02:47:31 +00:00
l10n daemon script
8697dde08d GIT_SILENT Sync po/docbooks with svn 2023-10-25 03:25:44 +00:00
l10n daemon script
9607424389 GIT_SILENT Sync po/docbooks with svn 2023-10-24 03:10:54 +00:00
l10n daemon script
9e22b0b61a GIT_SILENT Sync po/docbooks with svn 2023-10-22 04:04:00 +00:00
Scarlett Moore
148ac2f91a snapcraft: initial import snapcraft files. 2023-10-18 17:20:14 +00:00
Tobias Fella
9c4a4bb77f Use KF5 version of kirigami-addons in flatpak builds 2023-10-18 18:27:11 +02:00
l10n daemon script
00f880f7ff GIT_SILENT Sync po/docbooks with svn 2023-10-18 03:12:02 +00:00
l10n daemon script
703678ccea GIT_SILENT Sync po/docbooks with svn 2023-10-17 03:04:30 +00:00
l10n daemon script
d2ac959a1c GIT_SILENT Sync po/docbooks with svn 2023-10-14 03:21:31 +00:00
l10n daemon script
da0e84eacb GIT_SILENT Sync po/docbooks with svn 2023-10-13 03:11:05 +00:00
l10n daemon script
463172eca6 GIT_SILENT Sync po/docbooks with svn 2023-10-11 04:05:47 +00:00
Heiko Becker
ffbd92317e GIT_SILENT Update Appstream for new release 2023-10-07 18:01:35 +02:00
Heiko Becker
7c9173eb65 GIT_SILENT Upgrade release service version to 23.08.2. 2023-10-07 17:05:08 +02:00
l10n daemon script
c6a5a119e8 GIT_SILENT Sync po/docbooks with svn 2023-10-01 03:38:21 +00:00
l10n daemon script
edf2da1413 GIT_SILENT Sync po/docbooks with svn 2023-09-30 03:10:36 +00:00
l10n daemon script
3cacb10b9c GIT_SILENT Sync po/docbooks with svn 2023-09-26 03:21:35 +00:00
l10n daemon script
ce19275d8d GIT_SILENT Sync po/docbooks with svn 2023-09-20 03:07:35 +00:00
l10n daemon script
1d050c5264 GIT_SILENT Sync po/docbooks with svn 2023-09-19 03:12:29 +00:00
l10n daemon script
5f3ab888d0 GIT_SILENT Sync po/docbooks with svn 2023-09-17 04:02:52 +00:00
l10n daemon script
a7993d7b59 GIT_SILENT Sync po/docbooks with svn 2023-09-15 03:06:42 +00:00
l10n daemon script
eb837d2435 GIT_SILENT Sync po/docbooks with svn 2023-09-13 03:19:15 +00:00
l10n daemon script
c57354f974 GIT_SILENT Sync po/docbooks with svn 2023-09-11 03:12:35 +00:00
l10n daemon script
81a3ab95c5 GIT_SILENT made messages (after extraction) 2023-09-11 02:29:47 +00:00
q234 rty
0408956364 Use shortcode as a fallback for body when sending stickers
(cherry picked from commit 50551541ed)
2023-09-10 12:32:35 +02:00
Antonio Rojas
161412cbcf Add cmake check for qtlocation QML module
So that packagers are notified that it is a dependency

(cherry picked from commit 763198b2c3)
2023-09-10 12:32:32 +02:00
l10n daemon script
fdb02bebf5 GIT_SILENT Sync po/docbooks with svn 2023-09-10 03:04:18 +00:00
l10n daemon script
a756288a07 GIT_SILENT made messages (after extraction) 2023-09-10 02:23:23 +00:00
Ingo Klöcker
4853212326 Add Windows-specific screenshots as screenshots with custom attribute
Instead of using translated custom values (which are not accepted by
AppStream validation) this adds the Windows-specific screenshots as
normal screenshots with custom attribute x-kde-os="windows". AppStream
validation accepts custom attributes (in fact, it ignores any attributes
other than "type").

Upstream appstream tooling will treat those screenshots as any other
extra screenshots (screenshots without type="default").

(cherry picked from commit 6f4d2c0216)
2023-09-09 17:59:04 +02:00
Heiko Becker
3f36cbc2a4 Revert "GIT_SILENT made messages (after extraction)"
And restore the mistakenly removeed appdata file.

This reverts commit fe3933c96a.
2023-09-09 15:54:52 +02:00
Heiko Becker
ac9629258f GIT_SILENT Upgrade release service version to 23.08.1. 2023-09-09 10:23:39 +02:00
l10n daemon script
c085a5815b GIT_SILENT made messages (after extraction) 2023-09-09 02:24:55 +00:00
Luigi Toscano
7a25b151d4 Revert "GIT_SILENT made messages (after extraction)"
Restore a file which was removed by mistake.
Maybe we need to check the new ITS file.
This reverts commit fe3933c96a.
2023-09-08 11:24:29 +02:00
l10n daemon script
fe3933c96a GIT_SILENT made messages (after extraction) 2023-09-08 02:24:52 +00:00
l10n daemon script
3f59d988e7 GIT_SILENT Sync po/docbooks with svn 2023-09-06 03:02:15 +00:00
l10n daemon script
4355afae30 GIT_SILENT Sync po/docbooks with svn 2023-09-04 03:10:19 +00:00
l10n daemon script
99c7d0f5cc GIT_SILENT Sync po/docbooks with svn 2023-09-03 04:04:27 +00:00
l10n daemon script
528ad54136 GIT_SILENT Sync po/docbooks with svn 2023-09-02 03:15:32 +00:00
l10n daemon script
ee18e92d12 GIT_SILENT Sync po/docbooks with svn 2023-09-01 03:05:30 +00:00
l10n daemon script
67c0bf1881 GIT_SILENT Sync po/docbooks with svn 2023-08-31 03:04:57 +00:00
l10n daemon script
4e0c0fc3e4 GIT_SILENT Sync po/docbooks with svn 2023-08-29 03:04:34 +00:00
Tobias Fella
4886f1c3b9 Deselect space when leaving it
BUG: 473271
(cherry picked from commit cc60dde62d)
2023-08-28 11:48:40 +02:00
l10n daemon script
d1efa96b79 GIT_SILENT Sync po/docbooks with svn 2023-08-28 04:04:14 +00:00
Tobias Fella
56d7d1f313 Fix opening settings
BUG: 473789
(cherry picked from commit d11d6c74b3)
2023-08-27 19:17:49 +02:00
Tobias Fella
04682b077a Fix opening report sheet
(cherry picked from commit 68298f038d)
2023-08-27 14:59:43 +02:00
Tobias Fella
ed496e04bb Force plain text user display name in user detail sheet
(cherry picked from commit 5da9bba844)
2023-08-27 14:59:29 +02:00
Tobias Fella
8439768cc3 Fix opening ban sheet
(cherry picked from commit 405fd5841a)
2023-08-27 14:59:19 +02:00
Carl Schwan
33a4d476c6 Add release note for last two version
(cherry picked from commit 550d55cb1a)
2023-08-25 15:23:53 +02:00
Carl Schwan
bb2e2fddcc Remove debuging value
Otherwise like of connectivity never disapear

(cherry picked from commit 1e3c3dd1f4)
2023-08-24 16:11:06 +02:00
l10n daemon script
0d890ce3d6 GIT_SILENT Sync po/docbooks with svn 2023-08-21 03:07:33 +00:00
l10n daemon script
081dd5f1b0 GIT_SILENT made messages (after extraction) 2023-08-21 02:26:15 +00:00
l10n daemon script
79bba63864 GIT_SILENT Sync po/docbooks with svn 2023-08-20 03:05:14 +00:00
l10n daemon script
2b131b58a9 GIT_SILENT Sync po/docbooks with svn 2023-08-17 03:05:43 +00:00
Carl Schwan
97b7047a48 Don't inherit color in TimelineContainer
(cherry picked from commit 3bd93996c0)
2023-08-16 23:45:04 +02:00
Heiko Becker
91b5573016 GIT_SILENT Update Appstream for new release 2023-08-16 23:33:42 +02:00
Heiko Becker
8e9bb8a7e4 GIT_SILENT Upgrade release service version to 23.08.0. 2023-08-16 23:33:39 +02:00
Joshua Goins
916755b4ad Use view-list-details instead of expand icon in room drawer
(cherry picked from commit 24d0082048)
2023-08-16 16:24:13 -04:00
Joshua Goins
9ce288fe47 Destroy the correct component in space list context menu
(cherry picked from commit 2980af11b0)
2023-08-16 16:24:12 -04:00
Joshua Goins
aa461fe078 Prevent the TapHandler in AvatarTabButton from triggering clicked()
This caused right-clicks on this component (for example, in the space
drawer) to cause clicked() to be called on the delegate.

(cherry picked from commit 91d9406c38)
2023-08-16 16:24:12 -04:00
Joshua Goins
15d8c39185 Add icons to the space list context menu items
(cherry picked from commit 280d1e38e2)
2023-08-16 16:24:12 -04:00
Joshua Goins
4a1aa97d7b Prevent the chat bar from pasting a local file URL if we save the bitmap
There's no point in also putting the file:// URL, so catch the event if
saving the bitmap from the clipboard is successful.

(cherry picked from commit 7b520da4b4)
2023-08-16 16:24:12 -04:00
Joshua Goins
e332179ac4 Ensure that the notification avatar is drawn with no pen
(cherry picked from commit 419b4cea98)
2023-08-16 16:22:38 -04:00
Joshua Goins
7534f293c5 Further notification avatar improvements
The correct render hint is actually SmoothPixmapTransform, as
Antialiasing only matters for primitives. Also, render a white
background for potentially transparent avatars.

(cherry picked from commit 38824f30ac)
2023-08-16 16:22:38 -04:00
Joshua Goins
fc60a7c13e Fix the account switcher width so it doesn't spill off the side
(cherry picked from commit 9c4d8ef823)
2023-08-16 16:22:38 -04:00
Joshua Goins
48b8703156 Pick a better shadow and clean up emoji popup backgrounds
These shadows are now closer to how other popups look.

(cherry picked from commit fe3bf3a638)
2023-08-16 16:22:38 -04:00
Joshua Goins
5ca85dc670 Don't clip the emoji popups as they destroy shadows
(cherry picked from commit d678a446e2)
2023-08-16 16:22:38 -04:00
l10n daemon script
bf8ea41e6a GIT_SILENT Sync po/docbooks with svn 2023-08-16 03:06:00 +00:00
l10n daemon script
fda4ae8735 GIT_SILENT Sync po/docbooks with svn 2023-08-15 03:08:17 +00:00
Tobias Fella
aa0e9fff15 Improve tooltips in chatbar
(cherry picked from commit 4ea183b139)
2023-08-14 09:55:08 +02:00
Tobias Fella
28391b53e5 Wrap room display name in Room Drawer
(cherry picked from commit 22fd24549b)
2023-08-14 09:51:34 +02:00
l10n daemon script
a8d7744b42 GIT_SILENT Sync po/docbooks with svn 2023-08-13 03:05:29 +00:00
l10n daemon script
9fcc858841 GIT_SILENT made messages (after extraction) 2023-08-13 02:23:56 +00:00
l10n daemon script
f7092eee69 GIT_SILENT Sync po/docbooks with svn 2023-08-12 03:11:35 +00:00
l10n daemon script
9a426f941a GIT_SILENT Sync po/docbooks with svn 2023-08-11 03:06:07 +00:00
Albert Astals Cid
c77dee09f5 GIT_SILENT Upgrade release service version to 23.07.90. 2023-08-10 21:00:24 +02:00
l10n daemon script
26dde017d0 GIT_SILENT Sync po/docbooks with svn 2023-08-10 03:07:45 +00:00
l10n daemon script
5b27a84bcc GIT_SILENT Sync po/docbooks with svn 2023-08-09 03:12:18 +00:00
Carl Schwan
fc3c36838c Display big emoji correctly
We already check with a regex if the message only contains emoji so it is safe
to hardcode the font family as emoji for these messages.


(cherry picked from commit 58213ee3e6)
2023-08-08 15:00:29 +02:00
l10n daemon script
1fd2072cf8 GIT_SILENT Sync po/docbooks with svn 2023-08-08 03:23:23 +00:00
l10n daemon script
7770d412b3 GIT_SILENT made messages (after extraction) 2023-08-08 02:36:56 +00:00
Tobias Fella
56184920b4 Force plain text in room delegate
(cherry picked from commit 3ab04583ae)
2023-08-07 23:14:58 +02:00
l10n daemon script
b61685b151 GIT_SILENT Sync po/docbooks with svn 2023-08-07 03:49:13 +00:00
l10n daemon script
be0212a686 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"
2023-08-07 03:39:29 +00:00
l10n daemon script
37ed09f957 GIT_SILENT made messages (after extraction) 2023-08-07 03:00:36 +00:00
l10n daemon script
1b7e8ae77a GIT_SILENT Sync po/docbooks with svn 2023-08-06 03:13:44 +00:00
Tobias Fella
7571b0a9a3 Remove leftover debug statement
(cherry picked from commit 88368cefaa)
2023-08-06 00:52:20 +02:00
Tobias Fella
9477e6343f Fix last active time in devices page
(cherry picked from commit ab4bb41979)
2023-08-05 20:34:00 +02:00
l10n daemon script
83241ea956 GIT_SILENT Sync po/docbooks with svn 2023-08-03 04:06:32 +00:00
l10n daemon script
f054467878 GIT_SILENT made messages (after extraction) 2023-08-03 03:11:57 +00:00
l10n daemon script
fb7154b492 GIT_SILENT Sync po/docbooks with svn 2023-08-02 03:20:51 +00:00
l10n daemon script
973a75128c 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"
2023-08-02 03:13:39 +00:00
l10n daemon script
9ea70d7eac GIT_SILENT made messages (after extraction) 2023-08-02 02:35:56 +00:00
l10n daemon script
7f8285037c GIT_SILENT Sync po/docbooks with svn 2023-08-01 03:26:38 +00:00
Carl Schwan
c5f19f12e5 Redesign timeline
Remove border von bubbles and only hilight the bubble on hover

(cherry picked from commit 7f459cb90f)
2023-07-31 21:51:12 +02:00
Carl Schwan
ed6379ff85 Fix opening account menu on right click
(cherry picked from commit 5f41b7b4c1)
2023-07-31 09:03:01 +02:00
l10n daemon script
9750df90e0 GIT_SILENT Sync po/docbooks with svn 2023-07-31 04:06:19 +00:00
l10n daemon script
bd90658668 GIT_SILENT made messages (after extraction) 2023-07-31 03:12:56 +00:00
l10n daemon script
f78b634720 GIT_SILENT Sync po/docbooks with svn 2023-07-30 03:13:52 +00:00
Tobias Fella
250476f77c Html-escape display names in state delegates
(cherry picked from commit 163c1ef407)
2023-07-29 22:31:46 +02:00
Tobias Fella
e293d1f644 Fix room list icon size in compact room list
BUG: 472682
(cherry picked from commit 3bd639775a)
2023-07-29 22:31:14 +02:00
l10n daemon script
cde50588cb GIT_SILENT Sync po/docbooks with svn 2023-07-29 03:09:59 +00:00
l10n daemon script
3972dab7b9 GIT_SILENT made messages (after extraction) 2023-07-29 02:26:29 +00:00
Carl Schwan
7c1b43bcad Fix compilation with e2ee disabled 2023-07-28 13:01:54 +02:00
l10n daemon script
c22c74f735 GIT_SILENT Sync po/docbooks with svn 2023-07-28 03:18:59 +00:00
l10n daemon script
9c42b3c1a7 GIT_SILENT Sync po/docbooks with svn 2023-07-27 03:12:00 +00:00
l10n daemon script
d45cd5d9c9 GIT_SILENT made messages (after extraction) 2023-07-27 02:26:28 +00:00
Joshua Goins
8c503b8258 Use prettier notification images
Avatars are shown rounded in the main interface, so they should look the
same in the notification tray too. On top of that, if the room is a
group then show that group's icon when applicable in the bottom right.


(cherry picked from commit 470418f14f)
2023-07-26 10:31:47 +02:00
Joshua Goins
7078caac15 Fix errors preventing the avatar from being shown when uploaded
(cherry picked from commit 807a9ee779)
2023-07-26 10:29:06 +02:00
Joshua Goins
94bf65ab43 Add an obvious "upload" button in the account editor
It's not obvious at first glance that you can click on the avatar in the
account editor to upload an image. This adds a new button when there is
no image uploaded.


(cherry picked from commit 0ac3932303)
2023-07-26 10:28:32 +02:00
Joshua Goins
244e93c926 Fix settings button tooltip
(cherry picked from commit 4f32a168cd)
2023-07-26 08:38:06 +02:00
l10n daemon script
412856e882 GIT_SILENT Sync po/docbooks with svn 2023-07-26 03:13:49 +00:00
l10n daemon script
2421f11717 GIT_SILENT Sync po/docbooks with svn 2023-07-25 03:20:17 +00:00
l10n daemon script
2d42cfedec GIT_SILENT made messages (after extraction) 2023-07-25 02:30:45 +00:00
l10n daemon script
9308c84696 GIT_SILENT Sync po/docbooks with svn 2023-07-24 11:06:34 +00:00
l10n daemon script
72fc242364 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"
2023-07-24 10:47:48 +00:00
Carl Schwan
7d3240c597 Refactor completion menu
- Use new Avatar
- Use required property in delegate


(cherry picked from commit 75cd77facb)
2023-07-24 10:46:57 +02:00
Carl Schwan
9906ba8b7d Move no connection info message to the top
(cherry picked from commit f8d3e25f8e)
2023-07-24 10:46:35 +02:00
Tobias Fella
b949092bc6 Fix clearing user search field text when switching rooms
(cherry picked from commit 9ba6b2f0a5)
2023-07-24 10:46:16 +02:00
Tobias Fella
5cce3bd692 Fix nullptr check 2023-07-23 23:12:01 +02:00
Carl Schwan
ef5d67e5bf Add back some spacing around the avatar in the timeline
(cherry picked from commit b486cb905c)
2023-07-22 22:32:28 +02:00
Carl Schwan
4711b954c5 Fix accessibility of StartChatPage and port to new components
(cherry picked from commit cdd5c9f484)
2023-07-22 22:32:27 +02:00
Carl Schwan
1688b00f48 Port Avatar usage in Settings KirigamiAddons
(cherry picked from commit 3e6c426397)
2023-07-22 22:32:27 +02:00
Carl Schwan
410befa4bf Port most Avatar usage to new KirigamiAddons Avatar
(cherry picked from commit 847f41e1ad)
2023-07-22 22:32:27 +02:00
Carl Schwan
b380c74b0e Remove hover effect on timeline events 2023-07-22 22:32:20 +02:00
Carl Schwan
da8227931f Remove duplicated code in QuickSwitcher for RoomDelegate 2023-07-22 22:31:42 +02:00
Carl Schwan
fdc45ec5c8 Redesign room list
Use KirigamiAddons.Delegated.RoundedItemDelegate
2023-07-22 22:31:38 +02:00
Carl Schwan
94dd69ea02 Scroll to top of RoomDrawer on loading of header
(cherry picked from commit c8b4da2b96)
2023-07-22 17:53:06 +00:00
Carl Schwan
80c3253a6e Discard wrong avatar urls
(cherry picked from commit ed5bb270ff)
2023-07-22 17:51:46 +00:00
l10n daemon script
c5f6a6607e GIT_SILENT Sync po/docbooks with svn 2023-07-21 03:39:31 +00:00
l10n daemon script
a10d5cbe75 GIT_SILENT Sync po/docbooks with svn 2023-07-20 03:31:32 +00:00
l10n daemon script
584cad19b1 GIT_SILENT made messages (after extraction) 2023-07-20 02:45:39 +00:00
Carl Schwan
3745414635 Port to KirigamAddons.Banner
(cherry picked from commit 3263a69880)
2023-07-19 22:48:56 +02:00
l10n daemon script
147ce42c04 GIT_SILENT Sync po/docbooks with svn 2023-07-19 05:18:38 +00:00
l10n daemon script
928feeca82 GIT_SILENT made messages (after extraction) 2023-07-18 08:31:37 +00:00
Tobias Fella
fc04af536d Fix opening user mentions
(cherry picked from commit 646c8ba8fe)
2023-07-18 08:06:16 +02:00
Tobias Fella
12b949d6cb Fix image loading in room avatars
(cherry picked from commit 918e805718)
2023-07-18 05:26:04 +00:00
l10n daemon script
6aaaabf159 GIT_SILENT Sync po/docbooks with svn 2023-07-18 04:13:52 +00:00
l10n daemon script
fe45ef85a6 GIT_SILENT made messages (after extraction) 2023-07-18 02:55:30 +00:00
Albert Astals Cid
13e38ed982 GIT_SILENT Upgrade release service version to 23.07.80. 2023-07-17 11:25:34 +02:00
376 changed files with 120811 additions and 144572 deletions

5
.craft.ini Normal file
View File

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

View File

@@ -26,7 +26,7 @@
"name": "kirigamiaddons",
"config-opts": [ "-DBUILD_TESTING=OFF" ],
"buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git" } ]
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "branch": "kf5" } ]
},
{
"name": "kquickimageeditor",

View File

@@ -2,9 +2,15 @@
# SPDX-License-Identifier: CC0-1.0
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/android.yml
# - /gitlab-templates/linux.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows.yml
# - /gitlab-templates/freebsd.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-apks.yml
- /gitlab-templates/craft-windows-x86-64.yml
- /gitlab-templates/craft-windows-appx-qt5.yml

View File

@@ -2,7 +2,35 @@
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
'libraries/kirigami-addons': '@latest'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/kdbusaddons': '@stable'
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
@@ -20,21 +48,15 @@ Dependencies:
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
- 'on': ['Windows/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
per-test-timeout: 90
require-passing-tests-on: [ '@all' ]
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]

View File

@@ -34,7 +34,7 @@ Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: src/qml/confetti.png src/qml/glowdot.png
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0

View File

@@ -8,14 +8,17 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MINOR "08")
set(RELEASE_SERVICE_VERSION_MICRO "5")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.240.0")
set(QT_MIN_VERSION "6.5")
set(KF_MIN_VERSION "5.105.0")
set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.10")
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -24,7 +27,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -45,6 +48,28 @@ if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
if(QT_MAJOR_VERSION STREQUAL "6")
set(BASICLISTITEM_BOLD "font.bold")
set(OVERLAYSHEET_OPEN "onOpened")
set(QTQUICK_MODULE_QML_VERSION "")
set(QTLOCATION_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_AUDIO "MediaPlayer")
# in Audio qt6 we don't have it but we disable it in qt5 => it seems ok
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "")
# In Video qml qt6 we don't have it.
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "")
else()
set(BASICLISTITEM_BOLD "bold")
set(OVERLAYSHEET_OPEN "onSheetOpenChanged")
set(QTQUICK_MODULE_QML_VERSION "2.15")
set(QTLOCATION_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_AUDIO "Audio")
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "autoLoad: false")
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "flushMode: VideoOutput.FirstFrame")
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_setup_version(${PROJECT_VERSION}
@@ -52,21 +77,27 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt6 PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF6 PROPERTIES
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
PURPOSE "Secure storage of account secrets"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if(ANDROID)
find_package(OpenSSL)
@@ -75,37 +106,30 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
find_package(ICU 61.0 COMPONENTS uc)
set_package_properties(ICU PROPERTIES
TYPE REQUIRED
PURPOSE "Unicode library"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.7)
set_package_properties(QuotientQt6 PROPERTIES
if(QT_MAJOR_VERSION STREQUAL "6" AND NOT ANDROID AND NOT WIN32)
set(QUOTIENT_SUFFIX "Qt6")
endif()
find_package(Quotient${QUOTIENT_SUFFIX} 0.7)
set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
PURPOSE "Talk with matrix server"
)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
find_package(cmark)
set_package_properties(cmark PROPERTIES
TYPE REQUIRED
@@ -127,16 +151,20 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF6DocTools ${KF_MIN_VERSION})
set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
if(NOT Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
@@ -152,14 +180,12 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
if (BUILD_TESTING AND Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
add_subdirectory(appiumtests)
endif()
if(KF6DocTools_FOUND)
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()

View File

@@ -9,7 +9,7 @@
A Qt/QML based Matrix client.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
## Introduction

View File

@@ -9,7 +9,7 @@
android:versionName="${versionName}"
android:versionCode="${versionCode}"
android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat"

View File

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

View File

@@ -1,42 +0,0 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/_matrix/client/v3/login", methods=["GET"])
def login_get():
result = dict()
result["flows"] = [dict()]
result["flows"][0]["type"] = "m.login.password"
return result
@app.route("/_matrix/client/v3/login", methods=["POST"])
def login_post():
data = request.get_json()
if data["identifier"]["user"] != "user" or data["password"] != "1234":
abort(403)
print(data)
result = dict()
result["access_token"] = "token_1234"
result["device_id"] = "device_1234"
result["user_id"] = "@user:localhost:1234"
return result
@app.route("/_matrix/client/r0/sync")
def sync():
result = dict()
result["next_batch"] = "batch1234"
return result
@app.route("/.well-known/matrix/client")
def well_known():
reply = dict()
reply["m.homeserver"] = dict()
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
return reply
if __name__ == "__main__":
app.run(ssl_context='adhoc', port=1234)

View File

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

View File

@@ -20,15 +20,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME mediasizehelpertest
)
ecm_add_test(
eventhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)

View File

@@ -1,729 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "eventhandler.h"
#include <KFormat>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
#include "utils.h"
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class EventHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
EventHandler eventHandler;
private Q_SLOTS:
void initTestCase();
void eventId();
void delegateType_data();
void delegateType();
void author();
void authorDisplayName();
void time();
void timeString();
void highlighted();
void hidden();
void body();
void genericBody_data();
void genericBody();
void mediaInfo();
void linkPreviewer();
void reactions();
void hasReply();
void replyId();
void replyDelegateType();
void replyAuthor();
void replyBody();
void replyMediaInfo();
void location();
void readMarkers();
};
void EventHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
const auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
}
void EventHandlerTest::eventId()
{
eventHandler.setEvent(room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<DelegateType::Type>("delegateType");
QTest::newRow("message") << 0 << DelegateType::Message;
QTest::newRow("state") << 1 << DelegateType::State;
QTest::newRow("message 2") << 2 << DelegateType::Message;
QTest::newRow("reaction") << 3 << DelegateType::Other;
QTest::newRow("video") << 4 << DelegateType::Video;
QTest::newRow("location") << 7 << DelegateType::Location;
}
void EventHandlerTest::delegateType()
{
QFETCH(int, eventNum);
QFETCH(DelegateType::Type, delegateType);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
eventHandler.setEvent(event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::authorDisplayName()
{
auto event = room->messageEvents().at(1).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::timeString()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
KFormat format;
QCOMPARE(eventHandler.getTimeString(false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::highlighted()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
auto event = room->messageEvents().at(3).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
}
void EventHandlerTest::genericBody()
{
QFETCH(int, eventNum);
QFETCH(QString, output);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
}
void EventHandlerTest::hasReply()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->user(replyEvent->senderId());
eventHandler.setEvent(event);
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
auto replyEvent = room->messageEvents().at(4).get();
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getReplyMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::location()
{
auto event = room->messageEvents().at(7).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::readMarkers()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -1,72 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include <qglobal.h>
#include <qtestcase.h>
#include <cmath>
#include "mediasizehelper.h"
#include "neochatconfig.h"
class MediaSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void uninitialized();
void limits_data();
void limits();
};
void MediaSizeHelperTest::uninitialized()
{
MediaSizeHelper mediasizehelper;
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
}
void MediaSizeHelperTest::limits_data()
{
QTest::addColumn<qreal>("mediaWidth");
QTest::addColumn<qreal>("mediaHeight");
QTest::addColumn<qreal>("contentMaxWidth");
QTest::addColumn<qreal>("contentMaxHeight");
QTest::addColumn<QSize>("currentSize");
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
QTest::newRow("limit by content width tall media")
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
QTest::newRow("limit by content height wide media")
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
}
void MediaSizeHelperTest::limits()
{
QFETCH(qreal, mediaWidth);
QFETCH(qreal, mediaHeight);
QFETCH(qreal, contentMaxWidth);
QFETCH(qreal, contentMaxHeight);
QFETCH(QSize, currentSize);
MediaSizeHelper mediasizehelper;
mediasizehelper.setMediaWidth(mediaWidth);
mediasizehelper.setMediaHeight(mediaHeight);
mediasizehelper.setContentMaxWidth(contentMaxWidth);
mediasizehelper.setContentMaxHeight(contentMaxHeight);
QCOMPARE(mediasizehelper.currentSize(), currentSize);
}
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
#include "mediasizehelpertest.moc"

View File

@@ -10,8 +10,6 @@
#include <Quotient/syncdata.h>
#include <qnamespace.h>
#include "utils.h"
using namespace Quotient;
class TestRoom : public NeoChatRoom
@@ -363,7 +361,6 @@ void TextHandlerTest::receiveRichInPlainOut_data()
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
}
void TextHandlerTest::receiveRichInPlainOut()
@@ -558,10 +555,10 @@ void TextHandlerTest::receiveRichPlainUrl()
void TextHandlerTest::receiveRichEmote()
{
auto event = room->messageEvents().at(1).get();
auto author = room->user(event->senderId());
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
const QString testInputString = QStringLiteral("This is an emote.");
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:")
+ Utils::getUserColor(author->hueF()).name() + QStringLiteral("\">@example:example.org</a> This is an emote.");
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -605,13 +602,13 @@ void TextHandlerTest::linkPreviewsMatch_data()
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org"_ls)});
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org"_ls)});
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org"_ls)});
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org")});
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org")});
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org")});
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
<< QList<QUrl>({
QUrl("https://kde.org"_ls),
QUrl("www.example.org"_ls),
QUrl("https://kde.org"),
QUrl("www.example.org"),
});
}
@@ -647,5 +644,5 @@ void TextHandlerTest::linkPreviewsReject()
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
QTEST_MAIN(TextHandlerTest)
QTEST_GUILESS_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

View File

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

View File

@@ -46,29 +46,45 @@
<name xml:lang="uk">NeoChat</name>
<name xml:lang="x-test">xxNeoChatxx</name>
<name xml:lang="zh-CN">NeoChat</name>
<summary>Chat with your friends on matrix</summary>
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<name xml:lang="zh-TW">NeoChat</name>
<summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
<summary xml:lang="ca">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="eo">Kliento por matrix, la malcentra komunikprotokolo</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada.</summary>
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
<summary xml:lang="ie">Un cliente de Matrix, li protocol de communication decentralisat</summary>
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix  protokollen for desentralisert kommunikasjon</summary>
<summary xml:lang="pa">ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ</summary>
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="ru">Клиент для Matrix — децентрализованного коммуникационного протокола</summary>
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
<summary xml:lang="zh-TW">去中心化通訊協定 Matrix 的用戶端</summary>
<description>
<p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami
to provide a convergent experience across multiple platforms.</p>
@@ -90,15 +106,17 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</p>
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
<p xml:lang="ru">NeoChat — клиент для Matrix, децентрализованного коммуникационного протокола для мгновенного обмена сообщениями. С его помощью возможно отправлять текстовые сообщения, видео- и звуковые файлы родственникам, коллегам и друзьям. В программе использованы библиотеки KDE, в частности Kirigami, для обеспечения единообразной работы на различных платформах.</p>
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
<p xml:lang="sv">NeoChat är en klient för Matrix, det decentraliserade kommunikationsprotokollet för direktmeddelanden. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner. Den använder KDE Ramverk, i synnerhet Kirigami, för att tillhandahålla en konvergent upplevelse på flera plattformar.</p>
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.xx</p>
<p xml:lang="zh-TW">NeoChat 是去中心化即時通訊協定 Matrix 的一個用戶端。它讓您可以傳送文字訊息、影片、音訊檔案給您的家人、同事或朋友。NeoChat 使用 KDE frameworks尤其是 Kirigami來提供跨平台的響應式體驗。</p>
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
@@ -114,11 +132,13 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="ru">Целью создания NeoChat является полноценная реализация программы для спецификации Matrix. Как следствие, реализовано всё в текущей стабильной спецификации (за исключением голосовой интернет-связи, потоков и некоторых аспектов сквозного шифрования). Есть также несколько других незначительных пробелов, обусловленных постоянными изменениями спецификации Matrix. Тем не менее, стоит задача в итоге предоставить полную поддержку спецификации.</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
<p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p>
<p>Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
@@ -138,12 +158,14 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
<p xml:lang="ru">В силу природы разработки спецификации Matrix в NeoChat тоже предусмотрена поддержка многочисленных нестабильных возможностей. В текущей версии это следующие возможности:</p>
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
<p xml:lang="sv">På grund av sättet Matrix-specifikationens utvecklas, stöder NeoChat också ett stor antal instabila funktioner. För närvarande är de:</p>
<p xml:lang="ta">மேட்ரிக்ஸு நெறிமுறை வரையறுக்கப்படும் வித‍த்தின் காரணமாக, பல நிலையற்ற அம்சங்களையும் நியோச்சாட் ஆதரிக்கிறது. தற்போது ஆதரிக்கப்படுபவை:</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
<p xml:lang="zh-TW">由於 Matrix 標準的開發流程的緣故NeoChat 也支援數個非穩定版的功能。目前這些功能是:</p>
<ul>
<li>Polls - MSC3381</li>
<li xml:lang="ar">التصويت - MSC3381</li>
@@ -164,12 +186,14 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="ru">Голосования — MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
<li xml:lang="tr">Anketler - MSC3381</li>
<li xml:lang="uk">Опитування - MSC3381</li>
<li xml:lang="x-test">xxPolls - MSC3381xx</li>
<li xml:lang="zh-TW">投票 - MSC3381</li>
<li>Sticker Packs - MSC2545</li>
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
@@ -189,12 +213,14 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="nn">Klistremerke-pakkar  MSC2545</li>
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="ru">Наборы наклеек — MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li xml:lang="zh-TW">貼圖包 - MSC2545</li>
<li>Location Events - MSC3488</li>
<li xml:lang="ar">موقع الأحداث - MSC3488</li>
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
@@ -214,12 +240,14 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="nn">Posisjonshendingar  MSC3488</li>
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="ru">События местоположения — MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li>
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
<li xml:lang="x-test">xxLocation Events - MSC3488xx</li>
<li xml:lang="zh-TW">位置事件 - MSC3488</li>
</ul>
</description>
<url type="homepage">https://apps.kde.org/neochat/</url>
@@ -264,6 +292,7 @@ to provide a convergent experience across multiple platforms.</p>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
<developer_name xml:lang="zh-TW">KDE 社群</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<custom>
@@ -303,12 +332,14 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="ru">Главное окно со списком комнат, чатом и информацией о комнате</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
<caption xml:lang="zh-TW">主頁面,包含聊天室列表、聊天內容,與聊天室資訊</caption>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
@@ -316,6 +347,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="ar">شاشة الدخول</caption>
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="cs">Přihlašovací obrazovka</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
@@ -330,18 +362,24 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pl">Ekran logowania</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="ru">Окно входа</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
<caption xml:lang="sv">Inloggningsfönster</caption>
<caption xml:lang="ta">நுழைவுத் திரை</caption>
<caption xml:lang="tr">Oturum açma ekranı</caption>
<caption xml:lang="uk">Вікно входу</caption>
<caption xml:lang="x-test">xxLogin screenxx</caption>
<caption xml:lang="zh-TW">登入畫面</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.5" date="2024-02-15"/>
<release version="23.08.4" date="2023-12-07"/>
<release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>

View File

@@ -41,6 +41,7 @@ Name[tr]=NeoChat
Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat
GenericName=Matrix Client
GenericName[ar]=عميل ماتركس
GenericName[az]=Matrix Müştərisi
@@ -80,6 +81,7 @@ GenericName[tr]=Matrix İstemcisi
GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端
Comment=Client for the Matrix protocol
Comment[ar]=عميل لميفاق ماتركس
Comment[az]=Matrix protokolu üçün müştəri
@@ -118,6 +120,7 @@ Comment[tr]=Matrix protokolü için istemci
Comment[uk]=Клієнт протоколу Matrix
Comment[x-test]=xxClient for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的客户端
Comment[zh_TW]=Matrix 通訊協定的用戶端
MimeType=x-scheme-handler/matrix;
Exec=neochat %u
Terminal=false

View File

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

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 928 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>Vegeu també</title>
<simplelist>
<member
>Una llista de les preguntes més freqüents quant a Matrix <ulink url="https://matrix.org/faq/"
>Una llista de les preguntes més freqüents quan a Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

225
snapcraft.yaml Normal file
View File

@@ -0,0 +1,225 @@
# SPDX-FileCopyrightText: 2023 Scarlett Moore <sgmoore@kde.org>
#
# SPDX-License-Identifier: CC0-1.0
---
name: neochat
base: core22
adopt-info: neochat
grade: stable
confinement: strict
apps:
neochat:
# extensions:
# - kde-neon
command: usr/bin/neochat
common-id: org.kde.neochat
desktop: usr/share/applications/org.kde.neochat.desktop
plugs:
- home
- removable-media
- desktop
- desktop-legacy
- opengl
- wayland
- x11
- audio-playback
- unity7
- network
- network-bind
- network-manager-observe
- password-manager-service
- accounts-service
command-chain:
- snap/command-chain/desktop-launch
assumes:
- snapd2.58.3
compression: lzo
plugs:
desktop:
mount-host-font-cache: false
icon-themes:
interface: content
target: $SNAP/data-dir/icons
default-provider: gtk-common-themes
sound-themes:
interface: content
target: $SNAP/data-dir/sounds
default-provider: gtk-common-themes
kf5-5-111-qt-5-15-11-core22:
content: kf5-5-111-qt-5-15-11-core22-all
interface: content
default-provider: kf5-5-111-qt-5-15-11-core22
target: $SNAP/kf5
environment:
SNAP_DESKTOP_RUNTIME: $SNAP/kf5
QML2_IMPORT_PATH: "$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/qt5/qml:$SNAP/kf5/usr/lib/$CRAFT_ARCH_TRIPLET/qt5/qml:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET/qml"
QT_QUICK_CONTROLS_STYLE: org.kde.desktop
XDG_DATA_DIRS: "$SNAP/.local"
XDG_CONFIG_DIRS: "$SNAP/etc/xdg"
hooks:
configure:
plugs:
- desktop
command-chain:
- snap/command-chain/hooks-configure-desktop
layout:
/usr/share/X11:
symlink: $SNAP/kf5/usr/share/X11
slots:
session-dbus-interface:
interface: dbus
name: org.kde.neochat
bus: session
package-repositories:
- type: apt
components:
- main
suites:
- jammy
key-id: 444DABCF3667D0283F894EDDE6D4736255751E5D
url: http://origin.archive.neon.kde.org/user
key-server: keyserver.ubuntu.com
parts:
kde-neon:
source: /snap/snapcraft/current/share/snapcraft/extensions/desktop/kde-neon
source-type: local
plugin: make
make-parameters:
- PLATFORM_PLUG=kf5-5-111-qt-5-15-11-core22
build-snaps:
- kf5-5-111-qt-5-15-11-core22-sdk
build-environment:
- &id001
PATH: /snap/kf5-5-111-qt-5-15-11-core22-sdk/current/usr/bin${PATH:+:$PATH}
- &id002
XDG_DATA_DIRS: $CRAFT_STAGE/usr/share:/snap/kf5-5-111-qt-5-15-11-core22-sdk/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}
- &id003
XDG_CONFIG_HOME: $CRAFT_STAGE/etc/xdg:/snap/kf5-5-111-qt-5-15-11-core22-sdk/current/etc/xdg:/etc/xdg${XDG_CONFIG_HOME:+:$XDG_CONFIG_HOME}
- &id004
CRAFT_CMAKE_ARGS: -DCMAKE_FIND_ROOT_PATH=/snap/kf5-5-111-qt-5-15-11-core22-sdk/current${CRAFT_CMAKE_ARGS:+:$CRAFT_CMAKE_ARGS}
qtkeychain:
after:
- kde-neon
source: https://github.com/frankosterfeld/qtkeychain.git
source-tag: v0.14.0
source-depth: 0
plugin: cmake
build-packages:
- libsecret-1-dev
stage-packages:
- libsecret-1-0
- gnome-keyring
cmake-parameters:
- "-DKDE_INSTALL_USE_QT_SYS_PATHS=FALSE"
- "-DCMAKE_INSTALL_PREFIX=/usr"
- "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
- "-DENABLE_TESTING=OFF"
- "-DBUILD_TESTING=OFF"
- "-DKDE_SKIP_TEST_SETTINGS=ON"
- "-DCMAKE_FIND_ROOT_PATH=$CRAFT_STAGE\\;/snap/kf5-5-111-qt-5-15-11-core22/current"
- "-DBUILD_TRANSLATIONS=OFF"
- "-DLIBSECRET_SUPPORT=ON"
- "-DKEYCHAIN_DBUS=1"
prime:
- "-usr/include/qt5keychain"
- "-usr/lib/$CRAFT_ARCH_TRIPLET/libqt5keychain.so"
- "-usr/mkspecs"
- "-usr/lib/$CRAFT_ARCH_TRIPLET/cmake"
libmatrixclient:
after:
- kde-neon
- qtkeychain
source: https://github.com/quotient-im/libQuotient.git
source-tag: 0.8.1.2
source-depth: 0
plugin: cmake
build-packages:
- qtmultimedia5-dev
- libolm-dev
- libssl-dev
cmake-parameters:
- "-DCMAKE_INSTALL_PREFIX=/usr"
- "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
- "-DENABLE_TESTING=OFF"
- "-DBUILD_TESTING=OFF"
- "-DKDE_SKIP_TEST_SETTINGS=ON"
- "-DQuotient_ENABLE_E2EE=ON"
- "-DBUILDING_SHARED_QUOTIENT=ON"
- "-DCMAKE_FIND_ROOT_PATH=$CRAFT_STAGE\\;/snap/kf5-5-111-qt-5-15-11-core22/current"
neochat:
after:
- kde-neon
- qtkeychain
- libmatrixclient
parse-info:
- usr/share/metainfo/org.kde.neochat.appdata.xml
source: .
source-type: local
plugin: cmake
build-packages:
- cmark
- gcc-10
- g++-10
- libcmark-dev
- kquickimageeditor-dev
- libkf5qqc2desktopstyle-dev
- qcoro-qt5-dev
- kirigami-addons-dev
- reuse
- libkf5doctools-dev
- qtbase5-private-dev
- qml-module-org-kde-kquickimageeditor
- qml-module-org-kde-qqc2desktopstyle
- kirigami-addons-data
- qml-module-org-kde-kirigamiaddons
- qml-module-org-kde-quickcharts
- libsqlite3-dev
- libolm-dev
stage-packages:
- libcmark0.30.2
- qml-module-org-kde-kquickimageeditor
- qml-module-org-kde-qqc2desktopstyle
- kirigami-addons-data
- qml-module-org-kde-kirigamiaddons
- qml-module-org-kde-quickcharts
- qml-module-org-kde-kirigami2
- reuse
- libsqlite3-0
- libolm3
cmake-parameters:
- "-DKDE_INSTALL_USE_QT_SYS_PATHS=FALSE"
- "-DCMAKE_INSTALL_PREFIX=/usr"
- "-DCMAKE_BUILD_TYPE=Release"
- "-DENABLE_TESTING=OFF"
- "-DBUILD_TESTING=OFF"
- "-DKDE_SKIP_TEST_SETTINGS=ON"
- "-DNEOCHAT_FLATPAK=ON"
- "-DCMAKE_FIND_ROOT_PATH=$CRAFT_STAGE\\;/snap/kf5-5-111-qt-5-15-11-core22-sdk/current"
- "-DKDE_INSTALL_PLUGINDIR=/usr/lib/$CRAFT_ARCH_TRIPLET/qt5/plugins/"
prime:
- "-usr/lib/*/cmake/*"
- "-usr/include/*"
- "-usr/share/ECM/*"
- "-usr/share/doc/*"
- "-usr/share/man/*"
- "-usr/share/icons/breeze-dark*"
- "-usr/bin/X11"
- "-usr/lib/gcc/$CRAFT_ARCH_TRIPLET/6.0.0"
- "-usr/lib/aspell/*"
- "-usr/share/lintian"
build-environment: &id005
- *id001
- *id002
- *id003
- *id004
cleanup:
after:
- kde-neon
- neochat
plugin: nil
override-prime: |
set -eux
# find $CRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete
# find $CRAFT_PRIME/usr/share -type d -empty -delete | /bin/true
# # Unused libraries found by linter

View File

@@ -3,6 +3,16 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
configure_file(qml/Page/RoomList/RoomDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Page/RoomList/RoomDelegate.qml)
configure_file(qml/Component/QuickSwitcher.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/QuickSwitcher.qml)
configure_file(qml/Dialog/UserDetailDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/UserDetailDialog.qml)
configure_file(qml/Dialog/PowerLevelDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/PowerLevelDialog.qml)
configure_file(qml/Component/Timeline/AudioDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/AudioDelegate.qml)
configure_file(qml/Component/Timeline/VideoDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/VideoDelegate.qml)
configure_file(qml/Component/Timeline/OsmLocationPlugin.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/OsmLocationPlugin.qml)
configure_file(res.qrc ${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc)
add_library(neochat STATIC
controller.cpp
controller.h
@@ -34,18 +44,14 @@ add_library(neochat STATIC
roommanager.h
neochatroom.cpp
neochatroom.h
neochatuser.cpp
neochatuser.h
models/userlistmodel.cpp
models/userlistmodel.h
models/userfiltermodel.cpp
models/userfiltermodel.h
models/publicroomlistmodel.cpp
models/publicroomlistmodel.h
models/spacechildrenmodel.cpp
models/spacechildrenmodel.h
models/spacechildsortfiltermodel.cpp
models/spacechildsortfiltermodel.h
models/spacetreeitem.cpp
models/spacetreeitem.h
models/userdirectorylistmodel.cpp
models/userdirectorylistmodel.h
models/pushrulemodel.cpp
@@ -61,8 +67,8 @@ add_library(neochat STATIC
models/devicesmodel.cpp
models/devicesmodel.h
models/devicesproxymodel.cpp
filetype.cpp
filetype.h
filetypesingleton.cpp
filetypesingleton.h
login.cpp
login.h
models/webshortcutmodel.cpp
@@ -71,6 +77,8 @@ add_library(neochat STATIC
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
models/collapsestateproxymodel.cpp
models/collapsestateproxymodel.h
models/mediamessagefiltermodel.cpp
models/mediamessagefiltermodel.h
urlhelper.cpp
@@ -119,171 +127,6 @@ add_library(neochat STATIC
locationhelper.h
events/pollevent.cpp
pollhandler.cpp
utils.h
registration.cpp
neochatconnection.cpp
neochatconnection.h
jobs/neochatdeactivateaccountjob.cpp
jobs/neochatdeactivateaccountjob.h
jobs/neochatdeletedevicejob.cpp
jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h
mediasizehelper.cpp
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
roomlastmessageprovider.cpp
roomlastmessageprovider.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
QML_FILES
qml/main.qml
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
qml/SpaceListContextMenu.qml
qml/UserInfo.qml
qml/LoadingPage.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/Security.qml
qml/PushNotification.qml
qml/Categories.qml
qml/Permissions.qml
qml/NeochatMaximizeComponent.qml
qml/FancyEffectsContainer.qml
qml/TypingPane.qml
qml/ShimmerGradient.qml
qml/QuickSwitcher.qml
qml/HoverActions.qml
qml/ChatBox.qml
qml/ChatBar.qml
qml/AttachmentPane.qml
qml/ReplyPane.qml
qml/CompletionMenu.qml
qml/PieProgressBar.qml
qml/QuickFormatBar.qml
qml/RoomData.qml
qml/ServerData.qml
qml/EmojiPicker.qml
qml/TimelineDelegate.qml
qml/ReplyComponent.qml
qml/StateDelegate.qml
qml/RichLabel.qml
qml/MessageDelegate.qml
qml/Bubble.qml
qml/SectionDelegate.qml
qml/VideoDelegate.qml
qml/ReactionDelegate.qml
qml/LinkPreviewDelegate.qml
qml/AudioDelegate.qml
qml/FileDelegate.qml
qml/ImageDelegate.qml
qml/EncryptedDelegate.qml
qml/EventDelegate.qml
qml/TextDelegate.qml
qml/ReadMarkerDelegate.qml
qml/PollDelegate.qml
qml/MimeComponent.qml
qml/StateComponent.qml
qml/MessageEditComponent.qml
qml/AvatarFlow.qml
qml/LoginStep.qml
qml/Login.qml
qml/Homeserver.qml
qml/Username.qml
qml/RegisterPassword.qml
qml/Captcha.qml
qml/Terms.qml
qml/Email.qml
qml/Password.qml
qml/LoginRegister.qml
qml/Loading.qml
qml/LoginMethod.qml
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/EmojiDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
qml/ConfirmLogoutDialog.qml
qml/PowerLevelDialog.qml
qml/Message.qml
qml/EmojiItem.qml
qml/EmojiRow.qml
qml/EmojiSas.qml
qml/ConfirmDeactivateAccountDialog.qml
qml/VerificationCanceled.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/SettingsPage.qml
qml/ThemeRadioButton.qml
qml/ColorScheme.qml
qml/GeneralSettingsPage.qml
qml/EmoticonsPage.qml
qml/EmoticonEditorPage.qml
qml/EmoticonFormCard.qml
qml/GlobalNotificationsPage.qml
qml/NotificationRuleItem.qml
qml/AppearanceSettingsPage.qml
qml/AccountsPage.qml
qml/AccountEditorPage.qml
qml/DevicesPage.qml
qml/DeviceDelegate.qml
qml/DevicesCard.qml
qml/About.qml
qml/AboutKDE.qml
qml/SonnetConfigPage.qml
qml/NetworkProxyPage.qml
qml/DevtoolsPage.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml
qml/EmojiGrid.qml
qml/SearchPage.qml
qml/LocationDelegate.qml
qml/LocationChooser.qml
qml/TimelineView.qml
qml/InvitationView.qml
qml/AvatarTabButton.qml
qml/SpaceDrawer.qml
qml/OsmLocationPlugin.qml
qml/LiveLocationDelegate.qml
qml/FullScreenMap.qml
qml/LocationsPage.qml
qml/LocationMapItem.qml
qml/RoomDrawer.qml
qml/RoomDrawerPage.qml
qml/DirectChatDrawerHeader.qml
qml/GroupChatDrawerHeader.qml
qml/RoomInformation.qml
qml/RoomMedia.qml
qml/ChooseRoomDialog.qml
qml/ShareAction.qml
qml/SpaceHomePage.qml
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
ecm_qt_declare_logging_category(neochat
@@ -295,22 +138,11 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling"
CATEGORY_NAME "org.kde.neochat.eventhandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app
main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
)
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
endif()
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
@@ -325,25 +157,25 @@ if(NOT ANDROID)
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem ICU::uc)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_sources(neochat-app PRIVATE res_desktop.qrc)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp)
else()
target_sources(neochat-app PRIVATE res_android.qrc)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF6::I18n KF6::Kirigami2 KF6::Notifications KF6::ConfigCore KF6::ConfigGui KF6::CoreAddons KF6::SonnetCore KF6::ItemModels QuotientQt6 cmark::cmark QCoro::Core)
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -430,7 +262,7 @@ if(ANDROID)
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -438,12 +270,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF6::DBusAddons)
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF6::KIOWidgets)
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()

View File

@@ -44,7 +44,7 @@ void ActionsHandler::handleNewMessage()
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
m_room->uploadFile(QUrl(path), m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : m_room->chatBoxText());
m_room->uploadFile(path, m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf('/') + 1) : m_room->chatBoxText());
m_room->setChatBoxAttachmentPath({});
m_room->setChatBoxText({});
return;
@@ -97,7 +97,7 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
auto match = sed.match(text);
if (match.hasMatch()) {
const QString regex = match.captured(1);
@@ -113,13 +113,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
} else {
originalString = event->plainBody();
}
if (flags == "/g"_ls) {
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
if (flags == "/g") {
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
} else {
m_room->postHtmlMessage(handledText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
"",
event->id());
}
return;
@@ -133,7 +133,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
if (handledText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (handledText.indexOf(action.prefix) == 1
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
&& (handledText.indexOf(" ") == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
if (action.messageType.has_value()) {
messageType = *action.messageType;
@@ -152,30 +152,30 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
if (handledText.count("<p>"_ls) == 1 && handledText.count("</p>"_ls) == 1) {
handledText.remove("<p>"_ls);
handledText.remove("</p>"_ls);
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
handledText.remove("<p>");
handledText.remove("</p>");
}
if (handledText.length() == 0) {
return;
}
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString());
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
}
void ActionsHandler::checkEffects(const QString &text)
{
std::optional<QString> effect = std::nullopt;
if (text.contains(QStringLiteral("\u2744"))) {
if (text.contains("\u2744")) {
effect = QLatin1String("snowflake");
} else if (text.contains(QStringLiteral("\u1F386"))) {
} else if (text.contains("\u1F386")) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u2F387"))) {
} else if (text.contains("\u2F387")) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u1F389"))) {
} else if (text.contains("\u1F389")) {
effect = QLatin1String("confetti");
} else if (text.contains(QStringLiteral("\u1F38A"))) {
} else if (text.contains("\u1F38A")) {
effect = QLatin1String("confetti");
}
if (effect.has_value()) {

View File

@@ -4,12 +4,12 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/roommessageevent.h>
#include "neochatroom.h"
class CustomEmojiModel;
class NeoChatRoom;
/**
@@ -33,7 +33,6 @@ class NeoChatRoom;
class ActionsHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room that messages will be sent to.

View File

@@ -134,7 +134,7 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID)
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
@@ -225,7 +225,7 @@ void ChatDocumentHandler::complete(int index)
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(name + QStringLiteral(" "));
cursor.insertText(name % " ");
cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
@@ -246,7 +246,7 @@ void ChatDocumentHandler::complete(int index)
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(alias + QStringLiteral(" "));
cursor.insertText(alias % " ");
cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);

View File

@@ -4,13 +4,13 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor>
#include "models/completionmodel.h"
#include "neochatroom.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;
@@ -59,7 +59,6 @@ class SyntaxHighlighter;
class ChatDocumentHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Is the instance being used to handle an edit message.
@@ -105,12 +104,12 @@ class ChatDocumentHandler : public QObject
/**
* @brief The color to highlight user mentions.
*/
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
/**
* @brief The color to highlight spelling errors.
*/
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);

View File

@@ -4,7 +4,6 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
class QClipboard;
class QImage;
@@ -19,8 +18,6 @@ class QImage;
class Clipboard : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Whether the current clipboard content is an image.

View File

@@ -4,7 +4,6 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
class QAbstractItemModel;
class KColorSchemeManager;
@@ -20,8 +19,6 @@ class KColorSchemeManager;
class ColorSchemer : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief A QAbstractItemModel of all available color schemes.

View File

@@ -4,7 +4,11 @@
#include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <KConfig>
#include <KConfigGroup>
@@ -17,6 +21,7 @@
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImageReader>
#include <QNetworkProxy>
#include <QQuickTextDocument>
#include <QQuickWindow>
@@ -28,15 +33,17 @@
#include <Quotient/accountregistry.h>
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/eventstats.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/user.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/eventstats.h>
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#include "roommanager.h"
#include "windowcontroller.h"
@@ -53,6 +60,7 @@ Controller::Controller(QObject *parent)
: QObject(parent)
{
Connection::setRoomType<NeoChatRoom>();
Connection::setUserType<NeoChatUser>();
setApplicationProxy();
@@ -93,11 +101,13 @@ Controller::Controller(QObject *parent)
}
#endif
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
static int oldAccountCount = 0;
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
auto connection = m_accountRegistry.accounts()[m_accountRegistry.size() - 1];
connect(connection, &Connection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
}
@@ -115,24 +125,43 @@ Controller &Controller::instance()
return _instance;
}
void Controller::toggleWindow()
void Controller::showWindow()
{
auto &instance = WindowController::instance();
auto window = instance.window();
if (window->isVisible()) {
if (window->windowStates() & Qt::WindowMinimized) {
window->showNormal();
window->requestActivate();
} else {
window->close();
}
} else {
instance.showAndRaiseWindow({});
instance.window()->requestActivate();
}
WindowController::instance().showAndRaiseWindow(QString());
}
void Controller::addConnection(NeoChatConnection *c)
void Controller::logout(Connection *conn, bool serverSideLogout)
{
if (!conn) {
qCritical() << "Attempt to logout null connection";
return;
}
SettingsGroup("Accounts").remove(conn->userId());
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(true);
job.setKey(conn->userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (m_accountRegistry.count() > 1) {
// Only set the connection if the the account being logged out is currently active
if (conn == activeConnection()) {
setActiveConnection(m_accountRegistry.accounts()[0]);
}
} else {
setActiveConnection(nullptr);
}
if (!serverSideLogout) {
return;
}
conn->logout();
}
void Controller::addConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
@@ -140,17 +169,17 @@ void Controller::addConnection(NeoChatConnection *c)
c->setLazyLoading(true);
connect(c, &NeoChatConnection::syncDone, this, [this, c] {
connect(c, &Connection::syncDone, this, [this, c] {
Q_EMIT syncDone();
c->sync(30000);
c->saveState();
});
connect(c, &NeoChatConnection::loggedOut, this, [this, c] {
connect(c, &Connection::loggedOut, this, [this, c] {
dropConnection(c);
});
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
connect(c, &Connection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequired) {
Q_EMIT userConsentRequired(job->errorUrl());
}
@@ -159,19 +188,20 @@ void Controller::addConnection(NeoChatConnection *c)
c->sync();
Q_EMIT connectionAdded(c);
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(NeoChatConnection *c)
void Controller::dropConnection(Connection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
m_accountRegistry.drop(c);
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
}
void Controller::invokeLogin()
{
const auto accounts = SettingsGroup("Accounts"_ls).childGroups();
const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
@@ -185,36 +215,36 @@ void Controller::invokeLogin()
AccountSettings account{accountId};
QString accessToken;
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData());
accessToken = accessTokenLoadingJob->binaryData();
} else {
return;
}
auto connection = new NeoChatConnection(account.homeserver());
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token"_ls) {
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
connection->logout(false);
} else if (error == "Connection closed"_ls) {
logout(connection, false);
} else if (error == "Connection closed") {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
connection->logout(true);
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken);
@@ -280,26 +310,100 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true;
}
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
{
auto job = conn->uploadFile(localFile.toLocalFile());
connect(job, &BaseJob::success, this, [conn, job] {
conn->callApi<SetAvatarUrlJob>(conn->userId(), job->contentUri());
});
}
void Controller::markAllMessagesAsRead(Connection *conn)
{
const auto rooms = conn->allRooms();
for (auto room : rooms) {
room->markAllMessagesAsRead();
}
}
bool Controller::supportSystemTray() const
{
#ifdef Q_OS_ANDROID
return false;
#else
auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
QString de = getenv("XDG_CURRENT_DESKTOP");
return de != QStringLiteral("GNOME") && de != QStringLiteral("Pantheon");
#endif
}
void Controller::changePassword(Connection *connection, const QString &currentPassword, const QString &newPassword)
{
NeochatChangePasswordJob *job = connection->callApi<NeochatChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword, connection] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
authData["password"] = currentPassword;
authData["type"] = "m.login.password";
authData["user"] = connection->user()->id();
QJsonObject identifier = {{"type", "m.id.user"}, {"user", connection->user()->id()}};
authData["identifier"] = identifier;
NeochatChangePasswordJob *innerJob = connection->callApi<NeochatChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, [this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
if (innerJob->jsonData()["errcode"] == "M_FORBIDDEN") {
Q_EMIT passwordStatus(PasswordStatus::Wrong);
} else {
Q_EMIT passwordStatus(PasswordStatus::Other);
}
});
}
});
}
bool Controller::setAvatar(Connection *connection, const QUrl &avatarSource)
{
User *localUser = connection->user();
QString decoded = avatarSource.path();
if (decoded.isEmpty()) {
connection->callApi<SetAvatarUrlJob>(localUser->id(), avatarSource);
return true;
}
if (QImageReader(decoded).read().isNull()) {
return false;
} else {
return localUser->setAvatar(decoded);
}
}
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}
int Controller::accountCount() const
{
return m_accountRegistry.count();
}
void Controller::setQuitOnLastWindowClosed()
{
#ifndef Q_OS_ANDROID
if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
if (m_trayIcon) {
disconnect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
delete m_trayIcon;
m_trayIcon = nullptr;
}
@@ -310,7 +414,7 @@ void Controller::setQuitOnLastWindowClosed()
#endif
}
NeoChatConnection *Controller::activeConnection() const
Connection *Controller::activeConnection() const
{
if (m_connection.isNull()) {
return nullptr;
@@ -318,43 +422,49 @@ NeoChatConnection *Controller::activeConnection() const
return m_connection;
}
void Controller::setActiveConnection(NeoChatConnection *connection)
void Controller::setActiveConnection(Connection *connection)
{
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
disconnect(m_connection, &Connection::syncError, this, nullptr);
disconnect(m_connection, &Connection::accountDataChanged, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &NeoChatConnection::networkError, this, [this]() {
connect(connection, &Connection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &NeoChatConnection::syncDone, this, [this] {
connect(connection, &Connection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
connect(connection, &Connection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
connect(connection, &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == QLatin1String("org.kde.neochat.account_label")) {
Q_EMIT activeAccountLabelChanged();
}
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
Q_EMIT activeAccountLabelChanged();
}
PushRuleModel *Controller::pushRuleModel() const
@@ -367,6 +477,44 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
void Controller::createSpace(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
{},
name,
topic,
QStringList(),
{},
{},
false,
{},
{},
QJsonObject{
{"type"_ls, "m.space"_ls},
});
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
bool Controller::isOnline() const
{
return m_isOnline;
@@ -375,16 +523,16 @@ bool Controller::isOnline() const
// TODO: Remove in favor of RoomManager::joinRoom
void Controller::joinRoom(const QString &alias)
{
if (!alias.contains(":"_ls)) {
if (!alias.contains(":")) {
Q_EMIT errorOccured(i18n("The room id you are trying to join is not valid"));
return;
}
const auto knownServer = alias.mid(alias.indexOf(":"_ls) + 1);
const auto knownServer = alias.mid(alias.indexOf(":") + 1);
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
}
void Controller::openOrCreateDirectChat(User *user)
void Controller::openOrCreateDirectChat(NeoChatUser *user)
{
const auto existing = activeConnection()->directChats();
@@ -434,6 +582,11 @@ bool Controller::hasWindowSystem() const
#endif
}
bool Controller::encryptionSupported() const
{
return Quotient::encryptionSupported();
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
@@ -478,6 +631,12 @@ int Controller::activeConnectionIndex() const
return result - m_accountRegistry.accounts().begin();
}
int Controller::quotientMinorVersion() const
{
// TODO libQuotient 0.7: Replace with version function from libQuotient
return 7;
}
bool Controller::isFlatpak() const
{
#ifdef NEOCHAT_FLATPAK
@@ -487,6 +646,41 @@ bool Controller::isFlatpak() const
#endif
}
void Controller::setActiveAccountLabel(const QString &label)
{
if (!m_connection) {
return;
}
QJsonObject json{
{"account_label", label},
};
m_connection->setAccountData("org.kde.neochat.account_label", json);
}
QString Controller::activeAccountLabel() const
{
if (!m_connection) {
return {};
}
return m_connection->accountDataJson("org.kde.neochat.account_label")["account_label"].toString();
}
QVariantList Controller::getSupportedRoomVersions(Quotient::Connection *connection)
{
auto roomVersions = connection->availableRoomVersions();
QVariantList supportedRoomVersions;
for (const Quotient::Connection::SupportedRoomVersion &v : roomVersions) {
QVariantMap roomVersionMap;
roomVersionMap.insert("id", v.id);
roomVersionMap.insert("status", v.status);
roomVersionMap.insert("isStable", v.isStable());
supportedRoomVersions.append(roomVersionMap);
}
return supportedRoomVersions;
}
AccountRegistry &Controller::accounts()
{
return m_accountRegistry;

View File

@@ -5,24 +5,24 @@
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
class NeoChatRoom;
class NeoChatUser;
class TrayIcon;
class QWindow;
class QQuickTextDocument;
namespace Quotient
{
class Connection;
class Room;
class User;
}
namespace QKeychain
@@ -41,13 +41,16 @@ class ReadPasswordJob;
class Controller : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief The number of logged in accounts.
*/
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
/**
* @brief The current connection for the rest of NeoChat to use.
*/
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
Q_PROPERTY(Quotient::Connection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
@@ -59,6 +62,16 @@ class Controller : public QObject
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/**
* @brief The account label for the active account.
*
* Account labels are a concept specific to NeoChat, allowing accounts to be
* labelled, e.g. for "Work", "Private", etc.
*
* Set to an empty string to remove the label.
*/
Q_PROPERTY(QString activeAccountLabel READ activeAccountLabel WRITE setActiveAccountLabel NOTIFY activeAccountLabelChanged)
/**
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/
@@ -74,6 +87,20 @@ class Controller : public QObject
*/
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
/**
* @brief Whether the ecryption support has been enabled.
*/
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
/**
* @brief The current minor version number of libQuotient being used.
*
* This is the only way to gate NeoChat features by libQuotient version in QML.
*
* @note No major version because libQuotient doesn't have any; All are 0.x.
*/
Q_PROPERTY(int quotientMinorVersion READ quotientMinorVersion CONSTANT)
/**
* @brief Whether NeoChat is running as a flatpak.
*
@@ -93,34 +120,59 @@ public:
Q_ENUM(PasswordStatus)
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] int accountCount() const;
void setActiveConnection(Quotient::Connection *connection);
[[nodiscard]] Quotient::Connection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
void addConnection(NeoChatConnection *c);
void addConnection(Quotient::Connection *c);
/**
* @brief Drop a connection from the account registry.
*/
void dropConnection(NeoChatConnection *c);
void dropConnection(Quotient::Connection *c);
int activeConnectionIndex() const;
[[nodiscard]] QString activeAccountLabel() const;
void setActiveAccountLabel(const QString &label);
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Change the password for an account.
*
* The function emits a passwordStatus signal with a PasswordStatus value when
* complete.
*
* @sa PasswordStatus, passwordStatus
*/
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
/**
* @brief Change the avatar for an account.
*/
Q_INVOKABLE bool setAvatar(Quotient::Connection *connection, const QUrl &avatarSource);
/**
* @brief Create new room for a group chat.
*/
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
/**
* @brief Create new space.
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
/**
* @brief Join a room.
*/
@@ -131,7 +183,7 @@ public:
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
Q_INVOKABLE void openOrCreateDirectChat(NeoChatUser *user);
[[nodiscard]] bool supportSystemTray() const;
@@ -142,6 +194,8 @@ public:
bool isOnline() const;
bool encryptionSupported() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
@@ -149,6 +203,8 @@ public:
*/
Q_INVOKABLE void setApplicationProxy();
int quotientMinorVersion() const;
bool isFlatpak() const;
/**
@@ -172,12 +228,14 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
Q_INVOKABLE QVariantList getSupportedRoomVersions(Quotient::Connection *connection);
Quotient::AccountRegistry &accounts();
private:
explicit Controller(QObject *parent = nullptr);
QPointer<NeoChatConnection> m_connection;
QPointer<Quotient::Connection> m_connection;
TrayIcon *m_trayIcon = nullptr;
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account);
@@ -194,7 +252,7 @@ private:
private Q_SLOTS:
void invokeLogin();
void toggleWindow();
void showWindow();
void setQuitOnLastWindowClosed();
Q_SIGNALS:
@@ -204,17 +262,42 @@ Q_SIGNALS:
/// Error occurred because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void connectionAdded(Quotient::Connection *_t1);
void connectionDropped(Quotient::Connection *_t1);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
void quitOnLastWindowClosedChanged();
void unreadCountChanged();
void activeConnectionChanged();
void passwordStatus(Controller::PasswordStatus status);
void passwordStatus(Controller::PasswordStatus _t1);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
void isOnlineChanged(bool isOnline);
void keyVerificationRequest(int timeLeft, Quotient::Connection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationStart();
void keyVerificationAccept(const QString &commitment);
void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
void activeAccountLabelChanged();
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry();
};
// TODO libQuotient 0.7: Drop
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -124,7 +124,7 @@ int DelegateSizeHelper::calculateCurrentPercentageWidth() const
int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth;
int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth;
int calcPercentWidth = std::round(m * m_parentWidth + c);
int calcPercentWidth = std::ceil(m * m_parentWidth + c);
return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth);
}
@@ -146,9 +146,9 @@ qreal DelegateSizeHelper::currentWidth() const
qreal absoluteWidth = m_parentWidth * percentWidth * 0.01;
if (m_maxWidth < 0.0) {
return std::round(absoluteWidth);
return std::ceil(absoluteWidth);
} else {
return std::round(std::min(absoluteWidth, m_maxWidth));
return std::ceil(std::min(absoluteWidth, m_maxWidth));
}
}

View File

@@ -4,7 +4,6 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class DelegateSizeHelper
@@ -24,7 +23,6 @@
class DelegateSizeHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The width of the component's parent.

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class DelegateType
*
* This class is designed to define the DelegateType enumeration.
*/
class DelegateType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum Type {
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);
};

View File

@@ -1,993 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "eventhandler.h"
#include <QMovie>
#include <KLocalizedString>
#include <Quotient/eventitem.h>
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
#include "utils.h"
using namespace Quotient;
const NeoChatRoom *EventHandler::getRoom() const
{
return m_room;
}
void EventHandler::setRoom(const NeoChatRoom *room)
{
if (room == m_room) {
return;
}
m_room = room;
}
const Quotient::Event *EventHandler::getEvent() const
{
return m_event;
}
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (event == m_event) {
return;
}
m_event = event;
}
QString EventHandler::getId() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
return {};
}
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
{
if (auto e = eventCast<const RoomMessageEvent>(event)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(*event)) {
return DelegateType::Sticker;
}
if (event->isStateEvent()) {
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(*event)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(*event)) {
const auto pollEvent = eventCast<const PollStartEvent>(event);
if (pollEvent->isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return DelegateType::Other;
}
DelegateType::Type EventHandler::getDelegateType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
return getDelegateTypeForEvent(m_event);
}
QVariantMap EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->getUser(author);
}
QString EventHandler::getAuthorDisplayName(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& m_event->stateKey() == m_event->senderId()) {
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = m_event->senderId();
}
return previousDisplayName;
} else {
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->htmlSafeMemberName(author->id());
}
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
return isPending ? lastUpdated : m_event->originTimestamp();
}
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
auto ts = getTime(isPending, lastUpdated);
if (ts.isValid()) {
if (relative) {
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
}
return {};
}
bool EventHandler::isHighlighted()
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
return false;
}
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
}
bool EventHandler::isHidden()
{
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return true;
}
}
if (m_event->isStateEvent()) {
auto *stateEvent = eventCast<const StateEvent>(m_event);
if (stateEvent && stateEvent->repeatsState()) {
return true;
}
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty()) {
return true;
}
}
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
return true;
}
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return true;
}
}
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
return true;
}
// hide ending live location beacons
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
return true;
}
return false;
}
QString EventHandler::getRichBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::RichText, stripNewlines);
}
QString EventHandler::getPlainBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
}
const bool prettyPrint = (format == Qt::RichText);
return switchOnType(
*event,
[this, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(event, format, stripNewlines);
},
[](const StickerEvent &e) {
return e.body();
},
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = m_room->htmlSafeMemberName(e.userId());
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
}
}
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName);
} else {
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
}
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
},
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic")
: i18n("set the topic to: %1",
prettyPrint ? Quotient::prettyPrint(e.topic())
: stripNewlines ? e.topic().replace(u'\n', u' ')
: e.topic());
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.contentJson().isEmpty()) {
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return e.contentJson()["description"_ls].toString();
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
[](const PollStartEvent &e) {
return e.question();
},
i18n("Unknown event"));
}
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
{
TextHandler textHandler;
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
}
textHandler.setData(fileCaption);
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
textHandler.setData(body);
Qt::TextFormat inputFormat;
if (event.mimeType().name() == "text/plain"_ls) {
inputFormat = Qt::PlainText;
} else {
inputFormat = Qt::RichText;
}
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
}
QString EventHandler::getGenericBody() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
return {};
}
if (m_event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return switchOnType(
*m_event,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
return i18n("sent a message");
},
[](const StickerEvent &e) {
Q_UNUSED(e)
return i18n("sent a sticker");
},
[](const RoomMemberEvent &e) {
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
return i18n("reinvited someone to the room");
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
}
if (!text.isEmpty()) {
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name");
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
}
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
return i18n("banned a user from the room");
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
return i18n("requested an invite");
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
},
[](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("added a widget");
}
if (e.contentJson().isEmpty()) {
return i18n("removed a widget");
}
return i18n("configured a widget");
}
return i18n("updated the state");
},
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
i18n("Unknown event"));
}
QVariantMap EventHandler::getMediaInfo() const
{
return getMediaInfoForEvent(m_event);
}
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
{
QString eventId = event->id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
fileInfo = roomMessageEvent->content()->fileInfo();
} else if (event->is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
{
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration;
}
}
return mediaInfo;
}
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
QString text;
auto event = eventCast<const RoomMessageEvent>(m_event);
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
} else {
return nullptr;
}
}
QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
return nullptr;
}
auto eventId = m_event->id();
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
return nullptr;
};
QMap<QString, QList<User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId()));
}
}
if (reactions.isEmpty()) {
return nullptr;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_room->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (res.size() > 0) {
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
} else {
return nullptr;
}
}
bool EventHandler::hasReply() const
{
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::getReplyId() const
{
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
DelegateType::Type EventHandler::getReplyDelegateType() const
{
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return DelegateType::Other;
}
return getDelegateTypeForEvent(replyEvent);
}
QVariantMap EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (replyPtr) {
auto replyUser = m_room->user(replyPtr->senderId());
return m_room->getUser(replyUser);
} else {
return m_room->getUser(nullptr);
}
}
QString EventHandler::getReplyRichBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::RichText, stripNewlines);
}
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::PlainText, stripNewlines);
}
QVariantMap EventHandler::getReplyMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
return {};
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(replyPtr);
}
float EventHandler::getLatitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
float EventHandler::getLongitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
}
QString EventHandler::getLocationAssetType() const
{
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
}
bool EventHandler::hasReadMarkers() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
}
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
users += m_room->getUser(user);
}
return users;
}
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
} else {
return QString();
}
}
QString EventHandler::getReadMarkersString() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

View File

@@ -1,400 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <KFormat>
#include <Quotient/eventitem.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/delegatetype.h"
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
/**
* @class EventHandler
*
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
* in a form ready for the NeoChat UI.
*
* To use this properly both the room and the event should be set (and the event should
* be from the given room).
*
* @note EventHandler will always try to return something even when not properly
* initialised, this is usually the best empty value it can create with available
* information. This is to minimize warnings from QML especially during startup
* and room changes.
*/
class EventHandler : public QObject
{
Q_OBJECT
public:
/**
* @brief Return the current room the EventHandler is using.
*/
const NeoChatRoom *getRoom() const;
/**
* @brief Set the current room the EventHandler to using.
*/
void setRoom(const NeoChatRoom *room);
/**
* @brief Return the current event the EventHandler is using.
*/
const Quotient::Event *getEvent() const;
/**
* @brief Set the current event the EventHandler to using.
*/
void setEvent(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the event.
*/
QString getId() const;
/**
* @brief Return the DelegateType of the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getDelegateType() const;
/**
* @brief Get the author of the event in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getAuthor(bool isPending = false) const;
/**
* @brief Get the display name of the event author.
*
* This method is separate from getAuthor() and special in that it will return
* the old display name of the author if the current event is one that caused it
* to change. This allows for scenarios where the UI wishes to notify that a
* user's display name has changed and what it changed from.
*
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param relative whether the string is realtive to the current date, i.e.
* Yesterday or Wednesday, etc.
* @param format the QLocale::FormatType to use.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Whether the event should be highlighted in the timeline.
*
* @note Messages in direct chats are never highlighted.
*/
bool isHighlighted();
/**
* @brief Whether the event should be hidden in the timeline.
*
* This could be for numerous reasons, e.g. if it's a replacement event, if the
* user has hidden all state events or if the sender has been ignored by the local
* user.
*/
bool isHidden();
/**
* @brief Output a string for the message content ready for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content ready for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getPlainBody(bool stripNewlines = false) const;
/**
* @brief Output a generic string for the message content ready for display.
*
* The output string is dependant upon the event type.
*
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
* is the same for all events of the same type.
*
* E.g. For a message the text will be:
* "sent a message"
*
* @sa getRichBody(), getPlainBody()
*/
QString getGenericBody() const;
/**
* @brief Return the media info for the event.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
*/
QVariantMap getMediaInfo() const;
/**
* @brief Return a LinkPreviewer object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty LinkPreviewer provided if null.
*
* @sa LinkPreviewer
*/
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
/**
* @brief Return a ReactionModel object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty QVariantList (or other suitable
* empty mode) provided if null.
*/
QSharedPointer<ReactionModel> getReactions() const;
/**
* @brief Whether the event is a reply to another in the timeline.
*/
bool hasReply() const;
/**
* @brief Return the Matrix ID of the event replied to.
*/
QString getReplyId() const;
/**
* @brief Return the DelegateType of the event replied to.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getReplyDelegateType() const;
/**
* @brief Get the author of the event replied to in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getReplyAuthor() const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyPlainBody(bool stripNewlines = false) const;
/**
* @brief Return the media info for the event replied to.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Return the latitude for the event.
*
* Returns -100.0 if the event doesn't have a location (latitudes are in the
* range -90deg to +90deg so -100 is out of range).
*/
float getLatitude() const;
/**
* @brief Return the longitude for the event.
*
* Returns -200.0 if the event doesn't have a location (latitudes are in the
* range -180deg to +180deg so -200 is out of range).
*/
float getLongitude() const;
/**
* @brief Return the type of location marker for the event.
*/
QString getLocationAssetType() const;
/**
* @brief Whether the event has any read marker for other users.
*/
bool hasReadMarkers() const;
/**
* @brief Returns a list of user read marker for the event.
*
* @param maxMarkers the maximum number of users to return. Usually the number
* of user read makers shown is limited to not clutter the UI.
* This needs to be the same as used in getNumberExcessReadMarkers
* so that the markers line up with the number displayed, i.e.
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QVariantList getReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
* be the same as used in getReadMarkers so that the value lines
* up with the number displayed, i.e. the number of users shown
* plus the excess number will be the total number of other user
* read markers at an event.
*/
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
QString getReadMarkersString() const;
private:
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
KFormat m_format;
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
};

View File

@@ -10,29 +10,29 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
{
if (json.contains(QStringLiteral("pack"))) {
pack = ImagePackEventContent::Pack{
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<Omittable<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<Omittable<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<Omittable<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
fromJson<Omittable<QString>>(json["pack"].toObject()["display_name"]),
fromJson<Omittable<QUrl>>(json["pack"].toObject()["avatar_url"]),
fromJson<Omittable<QStringList>>(json["pack"].toObject()["usage"]),
fromJson<Omittable<QString>>(json["pack"].toObject()["attribution"]),
};
} else {
pack = none;
}
const auto &keys = json["images"_ls].toObject().keys();
const auto &keys = json["images"].toObject().keys();
for (const auto &k : keys) {
Omittable<EventContent::ImageInfo> info;
if (json["images"_ls][k].toObject().contains(QStringLiteral("info"))) {
info = EventContent::ImageInfo(QUrl(json["images"_ls][k]["url"_ls].toString()), json["images"_ls][k]["info"_ls].toObject(), k);
if (json["images"][k].toObject().contains(QStringLiteral("info"))) {
info = EventContent::ImageInfo(QUrl(json["images"][k]["url"].toString()), json["images"][k]["info"].toObject(), k);
} else {
info = none;
}
images += ImagePackImage{
k,
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
fromJson<QUrl>(json["images"][k]["url"].toString()),
fromJson<Omittable<QString>>(json["images"][k]["body"]),
info,
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
fromJson<Omittable<QStringList>>(json["images"][k]["usage"]),
};
}
}
@@ -42,20 +42,20 @@ void ImagePackEventContent::fillJson(QJsonObject *o) const
if (pack) {
QJsonObject packJson;
if (pack->displayName) {
packJson["display_name"_ls] = *pack->displayName;
packJson["display_name"] = *pack->displayName;
}
if (pack->usage) {
QJsonArray usageJson;
for (const auto &usage : *pack->usage) {
usageJson += usage;
}
packJson["usage"_ls] = usageJson;
packJson["usage"] = usageJson;
}
if (pack->avatarUrl) {
packJson["avatar_url"_ls] = pack->avatarUrl->toString();
packJson["avatar_url"] = pack->avatarUrl->toString();
}
if (pack->attribution) {
packJson["attribution"_ls] = *pack->attribution;
packJson["attribution"] = *pack->attribution;
}
(*o)["pack"_ls] = packJson;
}
@@ -63,16 +63,16 @@ void ImagePackEventContent::fillJson(QJsonObject *o) const
QJsonObject imagesJson;
for (const auto &image : images) {
QJsonObject imageJson;
imageJson["url"_ls] = image.url.toString();
imageJson["url"] = image.url.toString();
if (image.body) {
imageJson["body"_ls] = *image.body;
imageJson["body"] = *image.body;
}
if (image.usage) {
QJsonArray usageJson;
for (const auto &usage : *image.usage) {
usageJson += usage;
}
imageJson["usage"_ls] = usageJson;
imageJson["usage"] = usageJson;
}
imagesJson[image.shortcode] = imageJson;
}

View File

@@ -12,12 +12,12 @@ PollStartEvent::PollStartEvent(const QJsonObject &obj)
int PollStartEvent::maxSelections() const
{
return contentJson()["org.matrix.msc3381.poll.start"_ls]["max_selections"_ls].toInt();
return contentJson()["org.matrix.msc3381.poll.start"]["max_selections"].toInt();
}
QString PollStartEvent::question() const
{
return contentJson()["org.matrix.msc3381.poll.start"_ls]["question"_ls]["body"_ls].toString();
return contentJson()["org.matrix.msc3381.poll.start"]["question"]["body"].toString();
}
PollResponseEvent::PollResponseEvent(const QJsonObject &obj)
@@ -32,7 +32,7 @@ PollEndEvent::PollEndEvent(const QJsonObject &obj)
PollResponseEvent::PollResponseEvent(const QString &pollStartEventId, QStringList responses)
: RoomEvent(basicJson(TypeId,
{{"org.matrix.msc3381.poll.response"_ls, QJsonObject{{"answers"_ls, QJsonArray::fromStringList(responses)}}},
{"m.relates_to"_ls, QJsonObject{{"rel_type"_ls, "m.reference"_ls}, {"event_id"_ls, pollStartEventId}}}}))
{{"org.matrix.msc3381.poll.response", QJsonObject{{"answers", QJsonArray::fromStringList(responses)}}},
{"m.relates_to", QJsonObject{{"rel_type", "m.reference"}, {"event_id", pollStartEventId}}}}))
{
}

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
#include "filetype.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for (const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypePrivate
{
Q_DECLARE_PUBLIC(FileType)
Q_DISABLE_COPY(FileTypePrivate)
public:
FileTypePrivate(FileType *qq);
FileType *const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypePrivate::FileTypePrivate(FileType *qq)
: q_ptr(qq)
{
}
FileType::FileType(QObject *parent)
: QObject(parent)
, d_ptr(new FileTypePrivate(this))
{
}
FileType::~FileType() noexcept
{
}
QMimeType FileType::mimeTypeForName(const QString &nameOrAlias) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileType::mimeTypeForFile(const QString &fileName, MatchMode mode) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileType::mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileType::mimeTypesForFileName(const QString &fileName) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileType::mimeTypeForData(const QByteArray &data) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileType::mimeTypeForData(QIODevice *device) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileType::mimeTypeForUrl(const QUrl &url) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileType::mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileType::mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileType::suffixForFileName(const QString &fileName) const
{
Q_D(const FileType);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileType::supportedImageFormats() const
{
Q_D(const FileType);
return d->supportedImageFormats;
}
QStringList FileType::supportedAnimatedImageFormats() const
{
Q_D(const FileType);
return d->supportedAnimatedImageFormats;
}
#include "moc_filetype.cpp"

116
src/filetypesingleton.cpp Normal file
View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
#include "filetypesingleton.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for (const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypeSingletonPrivate
{
Q_DECLARE_PUBLIC(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingletonPrivate)
public:
FileTypeSingletonPrivate(FileTypeSingleton *qq);
FileTypeSingleton *const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypeSingletonPrivate::FileTypeSingletonPrivate(FileTypeSingleton *qq)
: q_ptr(qq)
{
}
FileTypeSingleton::FileTypeSingleton(QObject *parent)
: QObject(parent)
, d_ptr(new FileTypeSingletonPrivate(this))
{
}
FileTypeSingleton::~FileTypeSingleton() noexcept
{
}
QMimeType FileTypeSingleton::mimeTypeForName(const QString &nameOrAlias) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QString &fileName, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileTypeSingleton::mimeTypesForFileName(const QString &fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileTypeSingleton::mimeTypeForData(const QByteArray &data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileTypeSingleton::mimeTypeForData(QIODevice *device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileTypeSingleton::mimeTypeForUrl(const QUrl &url) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileTypeSingleton::suffixForFileName(const QString &fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileTypeSingleton::supportedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedImageFormats;
}
QStringList FileTypeSingleton::supportedAnimatedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedAnimatedImageFormats;
}
#include "moc_filetypesingleton.cpp"

View File

@@ -8,10 +8,9 @@
#include <QFileInfo>
#include <QMimeDatabase>
#include <QObject>
#include <QQmlEngine>
#include <qqml.h>
class FileTypePrivate;
class FileTypeSingletonPrivate;
/**
* @class FileTypeSingleton
@@ -20,11 +19,9 @@ class FileTypePrivate;
*
* @sa QMimeDatabase
*/
class FileType : public QObject
class FileTypeSingleton : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief List of supported image formats.
@@ -40,9 +37,12 @@ class FileType : public QObject
*/
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
QML_NAMED_ELEMENT(FileType)
QML_SINGLETON
public:
explicit FileType(QObject *parent = nullptr);
~FileType();
explicit FileTypeSingleton(QObject *parent = nullptr);
~FileTypeSingleton();
/**
* @brief Returns a MIME type for nameOrAlias or an invalid one if none found.
@@ -59,14 +59,14 @@ public:
*
* @sa QMimeDatabase
*/
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, FileType::MatchMode mode = MatchDefault) const;
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, FileTypeSingleton::MatchMode mode = MatchDefault) const;
/**
* @brief Returns a MIME type for fileInfo.
*
* @sa QMimeDatabase
*/
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, FileType::MatchMode mode = MatchDefault) const;
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, FileTypeSingleton::MatchMode mode = MatchDefault) const;
/**
* @brief Returns the MIME types for the file name fileName.
@@ -121,7 +121,9 @@ public:
QStringList supportedAnimatedImageFormats() const;
private:
const QScopedPointer<FileTypePrivate> d_ptr;
Q_DECLARE_PRIVATE(FileType)
Q_DISABLE_COPY(FileType)
const QScopedPointer<FileTypeSingletonPrivate> d_ptr;
Q_DECLARE_PRIVATE(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingleton)
};
QML_DECLARE_TYPE(FileTypeSingleton)

View File

@@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatchangepasswordjob.h"
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeactivateaccountjob.h"
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeletedevicejob.h"
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -8,22 +8,17 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h>
#include "neochatconfig.h"
#include "neochatroom.h"
using namespace Quotient;
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
: QObject(parent)
, m_currentRoom(room)
, m_loaded(false)
, m_url(url)
{
loadUrlPreview();
if (m_currentRoom) {
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
}
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
}
bool LinkPreviewer::loaded() const
@@ -62,22 +57,19 @@ void LinkPreviewer::setUrl(QUrl url)
void LinkPreviewer::loadUrlPreview()
{
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
return;
}
if (m_url.scheme() == QStringLiteral("https")) {
m_loaded = false;
Q_EMIT loadedChanged();
auto conn = m_currentRoom->connection();
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url);
GetUrlPreviewJob *job = conn->callApi<GetUrlPreviewJob>(m_url.toString());
connect(job, &BaseJob::success, this, [this, job, conn]() {
const auto json = job->jsonData();
m_title = json["og:title"_ls].toString().trimmed();
m_description = json["og:description"_ls].toString().trimmed().replace("\n"_ls, " "_ls);
m_title = json["og:title"].toString().trimmed();
m_description = json["og:description"].toString().trimmed().replace("\n", " ");
auto imageUrl = QUrl(json["og:image"_ls].toString());
auto imageUrl = QUrl(json["og:image"].toString());
if (imageUrl.isValid() && imageUrl.scheme() == QStringLiteral("mxc")) {
m_imageSource = conn->makeMediaUrl(imageUrl);
} else {
@@ -93,9 +85,4 @@ void LinkPreviewer::loadUrlPreview()
}
}
bool LinkPreviewer::empty() const
{
return m_url.isEmpty();
}
#include "moc_linkpreviewer.cpp"

View File

@@ -4,7 +4,6 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
class NeoChatRoom;
@@ -20,8 +19,6 @@ class NeoChatRoom;
class LinkPreviewer : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The URL to get the preview for.
*/
@@ -47,15 +44,8 @@ class LinkPreviewer : public QObject
*/
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
/**
* @brief Whether the there is a link to preview.
*
* A linkPreviwer is empty if the URL is empty.
*/
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
[[nodiscard]] QUrl url() const;
void setUrl(QUrl);
@@ -63,10 +53,9 @@ public:
[[nodiscard]] QString title() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QUrl imageSource() const;
[[nodiscard]] bool empty() const;
private:
const NeoChatRoom *m_currentRoom = nullptr;
NeoChatRoom *m_currentRoom = nullptr;
bool m_loaded;
QString m_title = QString();
@@ -82,6 +71,5 @@ Q_SIGNALS:
void descriptionChanged();
void imageSourceChanged();
void urlChanged();
void emptyChanged();
};
Q_DECLARE_METATYPE(LinkPreviewer *)

View File

@@ -2,17 +2,14 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "linkpreviewer.h"
#include <QMetaType>
#include <QObject>
#include <QQmlEngine>
#include <QRectF>
/** Location related helper functions for QML. */
class LocationHelper : public QObject
class LocationHelper
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Q_GADGET
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);

View File

@@ -87,32 +87,32 @@ public:
QMutexLocker locker(&mutex);
QByteArray buf;
QTextStream str(&buf);
str << QDateTime::currentDateTime().toString(Qt::ISODate) << QStringLiteral(" [");
str << QDateTime::currentDateTime().toString(Qt::ISODate) << " [";
switch (type) {
case QtDebugMsg:
str << QStringLiteral("DEBUG");
str << "DEBUG";
break;
case QtInfoMsg:
str << QStringLiteral("INFO ");
str << "INFO ";
break;
case QtWarningMsg:
str << QStringLiteral("WARN ");
str << "WARN ";
break;
case QtFatalMsg:
str << QStringLiteral("FATAL");
str << "FATAL";
break;
case QtCriticalMsg:
str << QStringLiteral("CRITICAL");
str << "CRITICAL";
break;
}
str << QStringLiteral("] ") << context.category << QStringLiteral(": ");
str << "] " << context.category << ": ";
if (context.file && *context.file && context.line) {
str << context.file << QStringLiteral(":") << context.line << QStringLiteral(": ");
str << context.file << ":" << context.line << ": ";
}
if (context.function && *context.function) {
str << context.function << QStringLiteral(": ");
str << context.function << ": ";
}
str << message << QStringLiteral("\n");
str << message << "\n";
str.flush();
file.write(buf.constData(), buf.size());
file.flush();
@@ -129,18 +129,18 @@ public:
if (file.isOpen()) {
file.close();
}
const auto &filePath = QStringLiteral("%1%2%3").arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), QDir::separator(), appName);
auto filePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator() + appName;
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator());
auto entryList = dir.entryList({appName + QStringLiteral(".*")});
std::sort(entryList.begin(), entryList.end(), [](const auto &left, const auto &right) {
auto leftIndex = left.split(QStringLiteral(".")).last().toInt();
auto rightIndex = right.split(QStringLiteral(".")).last().toInt();
auto leftIndex = left.split(".").last().toInt();
auto rightIndex = right.split(".").last().toInt();
return leftIndex > rightIndex;
});
for (const auto &entry : entryList) {
bool ok = false;
const auto index = entry.split(QStringLiteral(".")).last().toInt(&ok);
const auto index = entry.split(".").last().toInt(&ok);
if (!ok) {
continue;
}
@@ -151,8 +151,8 @@ public:
file.remove();
continue;
}
const auto &newName = QStringLiteral("%1.%2").arg(filePath, QString::number(index + 1));
const auto success = file.copy(newName);
const QString newName = filePath + QStringLiteral(".%1").arg(index + 1);
const bool success = file.copy(newName);
if (success) {
file.remove();
} else {
@@ -216,7 +216,7 @@ void initLogging()
oldCategoryFilter = QLoggingCategory::installFilter(filter);
oldHandler = qInstallMessageHandler(messageHandler);
sInstance->setOrigHandler(oldHandler);
sInstance->setName(QStringLiteral("neochat.log"));
sInstance->setName("neochat.log");
}
#include "logger.moc"

View File

@@ -13,16 +13,16 @@
using namespace Quotient;
LoginHelper::LoginHelper(QObject *parent)
Login::Login(QObject *parent)
: QObject(parent)
{
init();
}
void LoginHelper::init()
void Login::init()
{
m_homeserverReachable = false;
m_connection = new NeoChatConnection();
m_connection = new Connection();
m_matrixId = QString();
m_password = QString();
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
@@ -31,14 +31,14 @@ void LoginHelper::init()
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &LoginHelper::matrixIdChanged, this, [this]() {
connect(this, &Login::matrixIdChanged, this, [this]() {
setHomeserverReachable(false);
QRegularExpression validator(QStringLiteral("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$"));
QRegularExpression validator("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$");
if (!validator.match(m_matrixId).hasMatch()) {
return;
}
if (m_matrixId == QLatin1Char('@')) {
if (m_matrixId == "@") {
return;
}
@@ -51,7 +51,7 @@ void LoginHelper::init()
m_testing = true;
Q_EMIT testingChanged();
if (!m_connection) {
m_connection = new NeoChatConnection();
m_connection = new Connection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -87,11 +87,7 @@ void LoginHelper::init()
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
}
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -105,81 +101,80 @@ void LoginHelper::init()
});
}
void LoginHelper::setHomeserverReachable(bool reachable)
void Login::setHomeserverReachable(bool reachable)
{
m_homeserverReachable = reachable;
Q_EMIT homeserverReachableChanged();
}
bool LoginHelper::homeserverReachable() const
bool Login::homeserverReachable() const
{
return m_homeserverReachable;
}
QString LoginHelper::matrixId() const
QString Login::matrixId() const
{
return m_matrixId;
}
void LoginHelper::setMatrixId(const QString &matrixId)
void Login::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if (!m_matrixId.startsWith(QLatin1Char('@'))) {
m_matrixId.prepend(QLatin1Char('@'));
if (!m_matrixId.startsWith('@')) {
m_matrixId.prepend('@');
}
Q_EMIT matrixIdChanged();
}
QString LoginHelper::password() const
QString Login::password() const
{
return m_password;
}
void LoginHelper::setPassword(const QString &password)
void Login::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
QString LoginHelper::deviceName() const
QString Login::deviceName() const
{
return m_deviceName;
}
void LoginHelper::setDeviceName(const QString &deviceName)
void Login::setDeviceName(const QString &deviceName)
{
m_deviceName = deviceName;
Q_EMIT deviceNameChanged();
}
void LoginHelper::login()
void Login::login()
{
m_isLoggingIn = true;
Q_EMIT isLoggingInChanged();
// Some servers do not have a .well_known file. So we login via the username part from the mxid,
// rather than with the full mxid, as that would lead to an invalid user.
auto username = m_matrixId.mid(1, m_matrixId.indexOf(QLatin1Char(':')) - 1);
auto username = m_matrixId.mid(1, m_matrixId.indexOf(":") - 1);
m_connection->loginWithPassword(username, m_password, m_deviceName, QString());
}
bool LoginHelper::supportsPassword() const
bool Login::supportsPassword() const
{
return m_supportsPassword;
}
bool LoginHelper::supportsSso() const
bool Login::supportsSso() const
{
return m_supportsSso;
}
QUrl LoginHelper::ssoUrl() const
QUrl Login::ssoUrl() const
{
return m_ssoUrl;
}
void LoginHelper::loginWithSso()
void Login::loginWithSso()
{
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -189,30 +184,19 @@ void LoginHelper::loginWithSso()
});
}
bool LoginHelper::testing() const
bool Login::testing() const
{
return m_testing;
}
bool LoginHelper::isLoggingIn() const
bool Login::isLoggingIn() const
{
return m_isLoggingIn;
}
bool LoginHelper::isLoggedIn() const
bool Login::isLoggedIn() const
{
return m_isLoggedIn;
}
void LoginHelper::setInvalidPassword(bool invalid)
{
m_invalidPassword = invalid;
Q_EMIT isInvalidPasswordChanged();
}
bool LoginHelper::isInvalidPassword() const
{
return m_invalidPassword;
}
#include "moc_login.cpp"

View File

@@ -4,21 +4,21 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
class NeoChatConnection;
namespace Quotient
{
class Connection;
}
/**
* @class LoginHelper
* @class Login
*
* A helper class for logging into a Matrix account.
*/
class LoginHelper : public QObject
class Login : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Whether the home server for the account is reachable.
@@ -73,13 +73,8 @@ class LoginHelper : public QObject
*/
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
/**
* @brief Whether the password (or the username) is invalid.
*/
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit LoginHelper(QObject *parent = nullptr);
explicit Login(QObject *parent = nullptr);
Q_INVOKABLE void init();
@@ -105,9 +100,6 @@ public:
bool isLoggedIn() const;
bool isInvalidPassword() const;
void setInvalidPassword(bool invalid);
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
@@ -124,7 +116,6 @@ Q_SIGNALS:
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
private:
void setHomeserverReachable(bool reachable);
@@ -135,10 +126,9 @@ private:
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
NeoChatConnection *m_connection = nullptr;
Quotient::Connection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing = false;
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
};

View File

@@ -3,9 +3,7 @@
#include <QCommandLineParser>
#include <QIcon>
#include <QNetworkDiskCache>
#include <QNetworkProxyFactory>
#include <QObject>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQmlNetworkAccessManagerFactory>
@@ -18,10 +16,6 @@
#include <QApplication>
#endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
@@ -34,20 +28,67 @@
#include "neochat-version.h"
#include <Quotient/keyverificationsession.h>
#include <Quotient/accountregistry.h>
#include <Quotient/networkaccessmanager.h>
#include <Quotient/room.h>
#include <Quotient/util.h>
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatdocumenthandler.h"
#include "clipboard.h"
#include "controller.h"
#include "delegatesizehelper.h"
#include "filetypesingleton.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "models/accountemoticonmodel.h"
#include "models/collapsestateproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/imagepacksmodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/reactionmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/statefiltermodel.h"
#include "models/stickermodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "windowcontroller.h"
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
#include "models/completionmodel.h"
#include "models/statemodel.h"
#include "neochatuser.h"
#ifdef HAVE_RUNNER
#include "runner.h"
@@ -60,23 +101,11 @@
using namespace Quotient;
void qml_register_types_org_kde_neochat();
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
QNetworkAccessManager *create(QObject *) override
{
auto nam = NetworkAccessManager::instance();
nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/hsts/"));
nam->setStrictTransportSecurityEnabled(true);
auto namDiskCache = new QNetworkDiskCache(nam);
namDiskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/nam/"));
nam->setCache(namDiskCache);
return nam;
return NetworkAccessManager::instance();
}
};
@@ -93,19 +122,17 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
#else
QIcon::setFallbackThemeName("breeze"_ls);
QIcon::setFallbackThemeName("breeze");
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
@@ -127,7 +154,7 @@ int main(int argc, char *argv[])
#endif
KLocalizedString::setApplicationDomain("neochat");
QGuiApplication::setOrganizationName("KDE"_ls);
QGuiApplication::setOrganizationName("KDE");
KAboutData about(QStringLiteral("neochat"),
i18n("NeoChat"),
@@ -158,28 +185,111 @@ int main(int argc, char *argv[])
initLogging();
#if Quotient_VERSION_MINOR == 8
#ifdef Quotient_E2EE_ENABLED
Connection::setEncryptionDefault(true);
#endif
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy(QStringLiteral("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"),
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
#endif
Clipboard clipboard;
auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
Login *login = new Login();
UrlHelper urlHelper;
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
if (!config->colorScheme().isEmpty()) {
colorScheme.apply(config->colorScheme());
}
#endif
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", &EmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<ServerListModel>("org.kde.neochat", 1, 0, "ServerListModel");
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
qmlRegisterType<DevicesProxyModel>("org.kde.neochat", 1, 0, "DevicesProxyModel");
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<StateFilterModel>("org.kde.neochat", 1, 0, "StateFilterModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterType<LiveLocationsModel>("org.kde.neochat", 1, 0, "LiveLocationsModel");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
qmlRegisterType<PushRuleModel>("org.kde.neochat", 1, 0, "PushRuleModel");
qmlRegisterType<StickerModel>("org.kde.neochat", 1, 0, "StickerModel");
qmlRegisterType<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM");
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM");
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
qmlRegisterUncreatableType<NeoChatUser>("org.kde.neochat", 1, 0, "NeoChatUser", {});
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<Connection *>("Connection*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
#ifdef Quotient_E2EE_ENABLED
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
#endif
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(KAboutData::applicationData());
});
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "LocationHelper", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(LocationHelper());
});
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Emoji>();
#endif
QQmlApplicationEngine engine;
@@ -215,33 +325,26 @@ int main(int argc, char *argv[])
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
if (parser.isSet("ignore-ssl-errors"_ls)) {
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
reply->ignoreSslErrors();
});
}
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty()) {
return -1;
}
if (!parser.positionalArguments().isEmpty()) {
if (parser.positionalArguments().length() > 0) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
QDBusConnection::sessionBus().registerObject("/RoomRunner", &runner, QDBusConnection::ExportScriptableContents);
#endif
QWindow *window = windowFromEngine(&engine);

View File

@@ -25,13 +25,13 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, errorStr("Image request hasn't started"_ls)
, errorStr("Image request hasn't started")
{
if (requestedSize.isEmpty()) {
requestedSize.setHeight(100);
requestedSize.setWidth(100);
}
if (mediaId.count(QLatin1Char('/')) != 1) {
if (mediaId.count('/') != 1) {
if (mediaId.startsWith(QLatin1Char('/'))) {
mediaId = mediaId.mid(1);
} else {

View File

@@ -10,6 +10,11 @@
#include <QReadWriteLock>
namespace Quotient
{
class Connection;
}
/**
* @class ThumbnailResponse
*

View File

@@ -1,163 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "mediasizehelper.h"
#include "neochatconfig.h"
MediaSizeHelper::MediaSizeHelper(QObject *parent)
: QObject(parent)
{
}
qreal MediaSizeHelper::contentMaxWidth() const
{
return m_contentMaxWidth;
}
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
{
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
m_contentMaxWidth = -1.0;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
return;
}
m_contentMaxWidth = contentMaxWidth;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::contentMaxHeight() const
{
return m_contentMaxHeight;
}
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
{
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
m_contentMaxHeight = -1.0;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
return;
}
m_contentMaxHeight = contentMaxHeight;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaWidth() const
{
return m_mediaWidth;
}
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
{
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
m_mediaWidth = -1.0;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
return;
}
m_mediaWidth = mediaWidth;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaHeight() const
{
return m_mediaHeight;
}
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
{
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
m_mediaHeight = -1.0;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
return;
}
m_mediaHeight = mediaHeight;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::resolvedMediaWidth() const
{
if (m_mediaWidth > 0.0) {
return m_mediaWidth;
}
return widthLimit();
}
qreal MediaSizeHelper::resolvedMediaHeight() const
{
if (m_mediaHeight > 0.0) {
return m_mediaHeight;
}
return widthLimit() / 16.0 * 9.0;
}
qreal MediaSizeHelper::aspectRatio() const
{
return resolvedMediaWidth() / resolvedMediaHeight();
}
bool MediaSizeHelper::limitWidth() const
{
// If actual data isn't available we'll be using a placeholder that is width
// limited so return true.
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
return true;
}
return m_mediaWidth >= m_mediaHeight;
}
qreal MediaSizeHelper::widthLimit() const
{
if (m_contentMaxWidth < 0.0) {
return NeoChatConfig::self()->mediaMaxWidth();
}
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
}
qreal MediaSizeHelper::heightLimit() const
{
if (m_contentMaxHeight < 0.0) {
return NeoChatConfig::self()->mediaMaxHeight();
}
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
}
QSize MediaSizeHelper::currentSize() const
{
if (limitWidth()) {
qreal width = std::min(widthLimit(), resolvedMediaWidth());
qreal height = width / aspectRatio();
if (height > heightLimit()) {
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
}
return QSize(qRound(width), qRound(height));
} else {
qreal height = std::min(heightLimit(), resolvedMediaHeight());
qreal width = height * aspectRatio();
if (width > widthLimit()) {
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
}
return QSize(qRound(width), qRound(height));
}
}
#include "moc_mediasizehelper.cpp"

View File

@@ -1,105 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QSize>
/**
* @class MediaSizeHelper
*
* A class to help calculate the current width of a media item within a chat delegate.
*
* The only realistic way to guarantee that a media item (e.g. an image or video)
* is the correct size in QML is to calculate the size manually.
*
* The rules for this component work as follows:
* - The output will always try to keep the media size if no limits are breached.
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
* calcualated from either the configured max width or the contentMaxWidth, whichever
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
* - The current size will never be larger than any of the limits in either direction.
* - If any limit is breached the image size will be reduced while maintaining aspect
* ration, i.e. no stretching or squashing. This can mean that the width or height
* is reduced even if that parameter doesn't breach the limit itself.
*/
class MediaSizeHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The maximum width (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
/**
* @brief The maximum height (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
/**
* @brief The base width (in px) of the media.
*/
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
/**
* @brief The base height (in px) of the media.
*/
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
/**
* @brief The size (in px) of the component based on the current input.
*
* Will always try to return a value even if some of the inputs are not set to
* account for being called before the parameters are intialised. For any parameters
* not set these will just be left out of the calcs.
*
* If no input values are provided a default placeholder value will be returned.
*/
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
public:
explicit MediaSizeHelper(QObject *parent = nullptr);
qreal contentMaxWidth() const;
void setContentMaxWidth(qreal contentMaxWidth);
qreal contentMaxHeight() const;
void setContentMaxHeight(qreal contentMaxHeight);
qreal mediaWidth() const;
void setMediaWidth(qreal mediaWidth);
qreal mediaHeight() const;
void setMediaHeight(qreal mediaHeight);
QSize currentSize() const;
Q_SIGNALS:
void contentMaxWidthChanged();
void contentMaxHeightChanged();
void mediaWidthChanged();
void mediaHeightChanged();
void currentSizeChanged();
private:
qreal m_contentMaxWidth = -1.0;
qreal m_contentMaxHeight = -1.0;
qreal m_mediaWidth = -1.0;
qreal m_mediaHeight = -1.0;
qreal resolvedMediaWidth() const;
qreal resolvedMediaHeight() const;
qreal aspectRatio() const;
bool limitWidth() const;
qreal widthLimit() const;
qreal heightLimit() const;
};

View File

@@ -138,7 +138,7 @@ QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl sourc
if (job->error() != BaseJob::NoError) {
co_return;
}
m_images->images[index].url = job->contentUri();
m_images->images[index].url = job->contentUri().toString();
m_images->images[index].info = none;
QJsonObject data;
m_images->fillJson(&data);

View File

@@ -9,11 +9,12 @@
#include <QCoroTask>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
#include <Quotient/connection.h>
class ImagePacksModel;
/**
* @class AccountEmoticonModel
*
@@ -24,8 +25,6 @@
class AccountEmoticonModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The connection to get emoticons from.
*/

View File

@@ -14,10 +14,9 @@
using Action = ActionsModel::Action;
using namespace Quotient;
QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls, "#ffd500"_ls, "#ffff00"_ls, "#d4ff00"_ls, "#aaff00"_ls, "#80ff00"_ls,
"#55ff00"_ls, "#2bff00"_ls, "#00ff00"_ls, "#00ff2b"_ls, "#00ff55"_ls, "#00ff80"_ls, "#00ffaa"_ls, "#00ffd5"_ls, "#00ffff"_ls,
"#00d4ff"_ls, "#00aaff"_ls, "#007fff"_ls, "#0055ff"_ls, "#002bff"_ls, "#0000ff"_ls, "#2a00ff"_ls, "#5500ff"_ls, "#7f00ff"_ls,
"#aa00ff"_ls, "#d400ff"_ls, "#ff00ff"_ls, "#ff00d4"_ls, "#ff00aa"_ls, "#ff0080"_ls, "#ff0055"_ls, "#ff002b"_ls, "#ff0000"_ls};
QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00",
"#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff",
"#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
if (text.isEmpty()) {
@@ -261,7 +260,7 @@ QVector<ActionsModel::Action> actions{
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = Controller::instance().activeConnection();
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
const auto knownServer = roomName.mid(roomName.indexOf(":") + 1);
if (parts.length() >= 2) {
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
} else {
@@ -451,7 +450,7 @@ QVector<ActionsModel::Action> actions{
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
return QString();
}
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(" ") : QString());
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
return QString();
},
@@ -528,7 +527,7 @@ QVector<ActionsModel::Action> actions{
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
return QString();
}
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(" ") : QString());
Q_EMIT room->showMessage(NeoChatRoom::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
return QString();
},

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