Compare commits

..

180 Commits

Author SHA1 Message Date
Tobias Fella
819d88e18c Assorted Qt6 android fixes 2023-11-07 23:17:38 +01:00
James Graham
dbbad2cf13 Restricted Room Security
Create the required ux to allow the restricted room security setting to be re-enabled

BUG: 471307
2023-11-07 20:43:49 +00:00
Albert Astals Cid
08b84c6592 GIT_SILENT Upgrade release service version to 24.01.75. 2023-11-07 21:11:26 +01:00
l10n daemon script
b8abf0540d GIT_SILENT Sync po/docbooks with svn 2023-11-07 03:22:56 +00:00
Tobias Fella
7f9e709559 Improve initial active connection handling
If the last active connection is not reachable (server down, keychain problems,
token revoked, etc.), NeoChat currently fails to load at all, with the only fix
being to delete a line from the config file. This is surprisingly hard to fix with
a nice UX as long as we stick to the principle of loading the user's last active
connection automatically.

This patch thus drops that principle; instead, the user is always asked to choose
the connection to continue with.
2023-11-06 12:26:25 +00:00
l10n daemon script
c4f6abee9d GIT_SILENT Sync po/docbooks with svn 2023-11-06 02:17:00 +00:00
Joshua Goins
1a4947b98a Set accessible press actions for RoomDelegate 2023-11-05 18:09:20 -05:00
Tobias Fella
6ba2b715c3 Port away from Controller::activeConnectionIndex 2023-11-05 21:03:04 +00:00
Joshua Goins
e9e1e223f7 Allow right-clicking on a room without selecting it
Same idea as the fix I did for spaces, we introduce a new signal called
selected instead of using ItemDelegate's button signals.
2023-11-05 20:43:08 +00:00
Joshua Goins
006da1fb16 Show color scheme option on Android
This works on Android now, and the rest of the code changed to enable it
anyway.
2023-11-05 20:38:33 +00:00
James Graham
3aff1795c8 Accounts Popup Close on Hide
Make sure that the accounts popup is closed when the UserInfo component is hidden
2023-11-05 19:57:15 +00:00
Joshua Goins
576b1f928f Set RoomManager connection when opening a room
This was only set for the Controller, but it needed to be set for the
RoomManager too before opening a room. This could cause NeoChat to crash
when activating a notification, for example.
2023-11-05 14:48:55 -05:00
l10n daemon script
fc3ab50701 GIT_SILENT Sync po/docbooks with svn 2023-11-05 14:37:01 +00:00
James Graham
e7fa3ad524 Explicit room parameter DirectChatDrawerHeader
Create explicit room parameter and reference it in DirectChatDrawerHeader
2023-11-05 13:49:13 +00:00
James Graham
5adffddbd8 Fix InviteUserPage
Port the InviteUserPage to Qt6 and fix close action

fixes network/neochat#614
2023-11-05 13:45:10 +00:00
Tobias Fella
1d95d5aa15 Fix Runner and MatrixImageProvider singletons 2023-11-05 14:35:28 +01:00
Carl Schwan
dabd6291a5 roomlist: Fix ListSectionDelegate usage
The default property of ListSectionDelegate is already an alias to the
RowLayout.data and trying to use RoomLayout.children didn't work as
expected.
2023-11-05 13:08:29 +00:00
Tobias Fella
0e55c3b38f Port some things away from Controller::activeConnection 2023-11-05 13:02:18 +00:00
Carl Schwan
ff4cf86ea5 Add profile picture in about data 2023-11-05 12:17:26 +01:00
l10n daemon script
f4d5ccbf12 GIT_SILENT Sync po/docbooks with svn 2023-11-05 11:14:20 +00:00
l10n daemon script
192601d358 GIT_SILENT Sync po/docbooks with svn 2023-11-05 01:41:24 +00:00
l10n daemon script
d7c432119e GIT_SILENT Sync po/docbooks with svn 2023-11-04 17:24:07 +00:00
Tobias Fella
eddb2b73c2 Use new notification action api 2023-11-04 16:18:00 +00:00
Tobias Fella
0c60bfdb83 Don't crash on quit
BUG: 474386
2023-11-04 16:03:54 +00:00
James Graham
2bfb2fa1f9 Improve ChatDocumentHandler handling of null properties
Improve ChatDocumentHandler handling of null properties
2023-11-04 14:43:31 +00:00
l10n daemon script
d1aac971bf GIT_SILENT Sync po/docbooks with svn 2023-11-04 12:51:19 +00:00
Heiko Becker
284cadf305 GIT_SILENT Update Appstream for new release
(cherry picked from commit 753e879556)
2023-11-04 12:33:19 +01:00
l10n daemon script
4697b7fcf1 GIT_SILENT Sync po/docbooks with svn 2023-11-04 02:10:15 +00:00
Tobias Fella
9f356912c9 Add placeholder message to devices page 2023-11-03 22:06:33 +00:00
Tobias Fella
1cf891f845 Fix device logout dialog 2023-11-03 22:06:14 +00:00
Joshua Goins
52e2d636b9 Adjust tests to blockquote formatting changes 2023-11-03 21:52:47 +00:00
Joshua Goins
dc6b539ddf Add somewhat better blockquote styling
Quotes are now "quoted" and also have a different background.
2023-11-03 21:52:47 +00:00
Joshua Goins
e3cf85aa8c Yank "Qt5" out of the description for libQuotient
We aren't using thr Qt5 version anymore, and it's not really a point in
describing which exact Qt version we use with libQuotient in a user
facing string like this anyway.
2023-11-03 17:39:02 -04:00
James Graham
2065eb6684 Mobile explore component
Create a mobile version of explore component and place it at the bottom for single handed use. This also refactors the UserInfo component so it can be at the top on mobile as well as the bottom on dektop.

This should have no effect on desktop and should be identical.

![image](/uploads/9b3133fbde74ca27069d6b039efb1079/image.png)
2023-11-03 17:22:57 +00:00
Tobias Fella
9cac2a8abd Delete KeywordNotificationRuleModel
It's unused
2023-11-03 17:39:10 +01:00
Tobias Fella
feb87e6f70 Show QR codes in UserDetailDialog 2023-11-03 16:23:54 +00:00
l10n daemon script
a0057b8a49 GIT_SILENT Sync po/docbooks with svn 2023-11-03 02:12:15 +00:00
Tobias Fella
dbb0269354 Fix sending locations 2023-11-02 19:46:06 +01:00
Tobias Fella
7fdb617b33 Port RoundButtons in TimelineView to FloatingButton
BUG: 476124
2023-11-02 18:15:57 +01:00
Tobias Fella
d798b0dec9 HTML-escape mentions 2023-11-02 17:04:26 +00:00
James Graham
95cf23eb5b Remove GridLayout ReplyComponent
Stop using GridLayout in ReplyComponent as they are notoriously terribleRemove
2023-11-02 17:04:18 +00:00
James Graham
7e3db20229 Remove isEdit from ChatDocumentHandler
Remove isEdit from ChatDocumentHandler as it was made redundant by the ChatCache rework.
2023-11-02 17:00:41 +00:00
Laurent Montel
12689babfb Add missing moc 2023-11-02 08:50:43 +01:00
l10n daemon script
211407da44 GIT_SILENT Sync po/docbooks with svn 2023-11-02 02:13:36 +00:00
l10n daemon script
400a84e48d GIT_SILENT Sync po/docbooks with svn 2023-11-01 02:15:14 +00:00
Tobias Fella
7a45640e5e Fix string 2023-10-31 17:13:05 +01:00
Tobias Fella
8af20885ab Add account / device security settings page 2023-10-31 14:59:08 +01:00
Tobias Fella
4033f07272 Port away from custom format* QML functions 2023-10-31 14:56:52 +01:00
Tobias Fella
5df4fa297d Move Controller::setBlur and Controller::hasWindowSystem to WindowController 2023-10-31 09:17:44 +00:00
Tobias Fella
33c5b418d2 Move Controller::openOrCreateDirectChat to NeoChatConnection 2023-10-31 09:16:59 +00:00
l10n daemon script
69d378a17b GIT_SILENT Sync po/docbooks with svn 2023-10-31 02:11:02 +00:00
Tobias Fella
f1b1b8ce53 Remove unused include 2023-10-30 21:29:44 +01:00
Tobias Fella
526d4748e0 Remove unused includes 2023-10-30 21:27:36 +01:00
Tobias Fella
c5f93adbf4 Don't percent encode job parameter 2023-10-30 20:51:44 +01:00
Tobias Fella
61630cbe90 Rename "Security.qml" to "RoomSecurity.qml" 2023-10-30 11:07:42 +01:00
l10n daemon script
8c435e9d6d GIT_SILENT Sync po/docbooks with svn 2023-10-30 02:13:41 +00:00
James Graham
57978b1a6e EventHandler Nullptr checks
So I got lazy halfway through doing this the first time.

Add missing checks for null m_room and m_event to EventHandler and add tests for them.
2023-10-29 14:51:25 +00:00
James Graham
f3c4d9449a Move remaining tests to external Json files
Title
2023-10-29 13:12:05 +00:00
James Graham
9b37777f20 Remove getUsers
Remove getUsers as it's unused
2023-10-29 13:07:45 +00:00
l10n daemon script
d6d6c161db GIT_SILENT Sync po/docbooks with svn 2023-10-29 02:23:11 +00:00
Tobias Fella
772bca5ba6 Fix sending messages that were typed before switching rooms 2023-10-28 17:21:12 +02:00
Tobias Fella
036a60a095 Don't linkify urls in code blocks
BUG: 475301
2023-10-28 16:19:32 +02:00
Tobias Fella
b3315e1ed4 Add test for bug 475301
BUG: 475301
2023-10-28 16:19:31 +02:00
Tobias Fella
d300e9cf52 Slightly reformat cmake 2023-10-28 16:18:21 +02:00
Tobias Fella
1f4bcd150f Make SectionDelegate separator thickness consistent with other Separators 2023-10-28 16:16:37 +02:00
Tobias Fella
9ad8894983 Fix connection switching 2023-10-28 15:13:22 +02:00
Tobias Fella
9e63ca5eb7 Remove ShimmerGradient
It's unused
2023-10-28 14:08:51 +02:00
Tobias Fella
ade66242bb Remove unused import 2023-10-28 13:59:23 +02:00
Tobias Fella
2c6932b4cb Fix crash when there is an empty logging category
BUG: 475248
2023-10-28 09:12:59 +00:00
l10n daemon script
5cce9e7205 GIT_SILENT Sync po/docbooks with svn 2023-10-28 02:20:45 +00:00
Tobias Fella
7dd3ad9548 Hide info banner when changing room
BUG: 476128
2023-10-28 00:21:57 +02:00
Tobias Fella
f690b76efa Fix crash for events that are not RoomMessageEvents
BUG: 476153
2023-10-27 22:11:12 +02:00
James Graham
87b8d6710e Update Kirigami PlatformTheme
Update Kirigami PlatformTheme to new path.
2023-10-27 16:13:22 +00:00
l10n daemon script
965b890346 GIT_SILENT Sync po/docbooks with svn 2023-10-27 02:16:58 +00:00
Tobias Fella
02b4e5cc70 Port away from CheckableListItem 2023-10-26 10:07:35 +00:00
Tobias Fella
410add04fb Fix subtle erroneous event casts
Sometimes, the previous code would static_cast an EncryptedEvent to a RoomMessageEvent.
This was mostly unnoticed, since the types are similar enough.
2023-10-26 10:03:09 +00:00
l10n daemon script
b575b1e700 GIT_SILENT Sync po/docbooks with svn 2023-10-26 02:27:46 +00:00
l10n daemon script
5828ee1ada 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-10-26 02:17:19 +00:00
l10n daemon script
a64c80109e GIT_SILENT made messages (after extraction) 2023-10-26 01:45:07 +00:00
Tobias Fella
de47f7f2fa Remove duplicate signal 2023-10-25 20:28:14 +02:00
Laurent Montel
19adc7b9e5 GIT_SILENT: use KLocalizedString::setApplicationDomain(QByteArrayLiteral => saving one deep data copy 2023-10-25 08:25:27 +02:00
l10n daemon script
23f60a59fe GIT_SILENT Sync po/docbooks with svn 2023-10-25 02:25:42 +00:00
l10n daemon script
946ba2e56d GIT_SILENT made messages (after extraction) 2023-10-25 01:50:11 +00:00
Tobias Fella
9dcb7b49fa Use KColorScheme 2023-10-24 07:55:01 +00:00
Tobias Fella
52f5901642 Update replacement room filtering in real-time 2023-10-24 07:51:15 +00:00
Tobias Fella
d4b4a7e1ff Only hide replaced rooms if we're joined to the replacement room 2023-10-24 07:51:15 +00:00
Tobias Fella
4449678b74 Don't abort in the message logger
Let the app do whatever it would do naturally
2023-10-24 07:32:09 +00:00
Tobias Fella
19e197e0ec Fix maps
In Qt6, Map just shows a static map, while MapView allows for zooming, moving, etc.
2023-10-24 07:08:05 +00:00
l10n daemon script
cfc5202645 GIT_SILENT Sync po/docbooks with svn 2023-10-24 02:14:54 +00:00
Tobias Fella
9b80d9e7aa Minor QML cleanup 2023-10-23 22:28:34 +02:00
Tobias Fella
dc409387bd Remove check for libQuotient 0.8 2023-10-23 22:00:34 +02:00
l10n daemon script
1e73a7bda4 GIT_SILENT Sync po/docbooks with svn 2023-10-23 02:12:23 +00:00
James Graham
ef1d62d45c ActionsHandler poperties
Make sure that all low level components get ActionsHandler through actual properties rather than magically
2023-10-22 13:39:34 +00:00
James Graham
c2d82750b1 Fix opening a room in a new window.
Fix opening a room in a new window. This is done by makeing the model top level parameters in RoomPage set from RoomManager but overwritten by RoomWindow
2023-10-22 11:57:27 +00:00
l10n daemon script
c97d276b36 GIT_SILENT Sync po/docbooks with svn 2023-10-22 02:48:07 +00:00
l10n daemon script
681a0b1e93 GIT_SILENT Sync po/docbooks with svn 2023-10-21 02:16:44 +00:00
James Graham
39556f45ab Send Threaded Messages
This MR deals with only sending threaded messages. Showing threads will turn up in a follow up. This allows you to start a new thread by clicking reply in thread to a normal message. 

You can also do a threaded reply to a threaded message in the main timeline at the moment because those messages aren't shown in a separate thread timeline yet but will be in future.
2023-10-20 17:07:09 +00:00
Joshua Goins
3c7774800a Remove now unused QDataStream operators for Emoji 2023-10-20 15:27:33 +00:00
Joshua Goins
bc7530eaa1 Store last used emojis in the state config file
Instead of crumming up the main settings file with the last used emojis,
they now live in the state (where they belong.)
2023-10-20 15:27:33 +00:00
l10n daemon script
82d11f79d6 GIT_SILENT Sync po/docbooks with svn 2023-10-20 02:11:46 +00:00
Joshua Goins
25d0368d41 Compact the event cache JSON
By default, Qt will spit out indented JSON when writing to the config
file, which is useless as it wastes space and no one will read this.
2023-10-19 20:46:17 +00:00
Joshua Goins
83b7e7d121 Use KSharedConfig::openStateConfig() instead of using a "data" file
This function will automatically create a "neochatstarerc" for us, and
KConfig will decide the best place for us to place our state. It won't
always be in AppDataLocation.
2023-10-19 20:39:07 +00:00
Tobias Fella
26fd26f9fd Enable android qt6 ci 2023-10-19 20:18:15 +00:00
James Graham
0029567c3a Reorganise HoverActions Code
Move most of the external code for hover actions into the component.
2023-10-19 19:38:57 +00:00
Laurent Montel
c7614caf41 There's no QVector anymore, QList is the QVector in Qt6 2023-10-19 13:41:46 +02:00
l10n daemon script
6571dbe554 GIT_SILENT Sync po/docbooks with svn 2023-10-19 02:14:15 +00:00
l10n daemon script
b3a29068cc GIT_SILENT Sync po/docbooks with svn 2023-10-18 02:15:55 +00:00
Tobias Fella
5adda55a85 Show rooms instead of spaces in JoinRoomPage 2023-10-17 19:14:07 +00:00
l10n daemon script
d56f0d6086 GIT_SILENT Sync po/docbooks with svn 2023-10-17 02:10:40 +00:00
Tobias Fella
60772be391 Enforce symbolic icon in ExploreComponent 2023-10-16 22:12:13 +02:00
Yuri Chornoivan
27f1679741 Fix minor typo 2023-10-16 09:27:27 +03:00
l10n daemon script
838596c3ae GIT_SILENT Sync po/docbooks with svn 2023-10-16 02:10:50 +00:00
James Graham
a57744891a ChatCache
Move the functionality to cache the contents of a chat bar from the room directly and to a new ChatCache object. This works pretty much the same with a few extra check and balances, this also made it easy to put a test suite around the functionality so I did. The current functionality should be identical to what exists.

This is in prep for threads which will require managing even more caches if we create one per thread.
2023-10-15 12:55:56 +00:00
James Graham
f5417a6227 New parent dialog
Move the add new offical parent to a dialog and make sure that the join room dialog only shows spaces.
2023-10-15 11:28:17 +00:00
l10n daemon script
ac6f9ea219 GIT_SILENT Sync po/docbooks with svn 2023-10-15 02:11:25 +00:00
Tobias Fella
4b49559d39 Fix LocationHelper registration 2023-10-14 23:38:31 +02:00
Laurent Montel
baa33f1843 Remove unused import module 2023-10-14 20:27:18 +00:00
Tobias Fella
dae5718c6c Fix broken shortcut 2023-10-14 22:18:37 +02:00
Tobias Fella
60260cff3b Fix bugs in state delegates
- Don't show a dot over profile pictures.
  The dot was intended to be part of the Avatar when there is no image and the Avatar is too small to show an icon.
  Currently, it is visible over the profile picture. If we still want the dot, it should be upstreamed

- Fix avatar images in StateComponent
2023-10-14 19:33:44 +00:00
Laurent Montel
2df9a26cdc QTLOCATION_MODULE_QML_VERSION was used during qt5/qt6 support. Now version is not necessary in qt6 2023-10-14 20:42:19 +02:00
l10n daemon script
b16cd12b33 GIT_SILENT Sync po/docbooks with svn 2023-10-14 02:21:14 +00:00
Ingo Klöcker
6a3b22ef2d Remove the obsolete x-kde-os attribute for Windows screenshots 2023-10-13 14:06:47 +00:00
Tobias Fella
b28a85ff05 Fix AboutPage 2023-10-13 15:35:39 +02:00
James Graham
e480299563 Canonical Parent
So the original space parent and child stuff was technically a bit naughty in that it allowed multiple rooms to be set as the canonical parent. Because while a room can have multiple parents only one should be canonical. This adds the following:
- When adding a child or parent there is an extra check to select if the new parent should be canonical
- Any parent can be selected as the canonical one from the room settings
- All functions ensure that there is only ever one canonical parent by ensuring all others are false when a new one is set.
2023-10-13 12:00:47 +00:00
Tobias Fella
fe70e2773f Fix logging category filtering 2023-10-13 13:42:15 +02:00
l10n daemon script
e78ea4721a GIT_SILENT Sync po/docbooks with svn 2023-10-13 02:14:45 +00:00
l10n daemon script
1699dcf0c4 GIT_SILENT Sync po/docbooks with svn 2023-10-12 02:15:09 +00:00
Rohan Kumar
9d6aef6c2b Flatpak: allow talking to org.freedesktop.secrets
Previously, kwalletd5 was hard-coded. This should allow secret service
access for users with other secret service providers.
2023-10-11 09:31:30 -07:00
James Graham
a9c2428498 Manual Explore Rooms
This is an update to searching the public room list. Currently if you can't find the room you're looking for you can type a full alias of room ID into the search bar and a view/join button appears. This is hard to discover and technically broken since it was turned into a generic component for finding rooms (it kinda works but doesn't fit now as it's focussed on the joining rooms not adding new ones to spaces). It is also not very discoverable if you don't know it's there.

This patch patch updates the workflow to be truly generic and hopefully more discoverable. Instead of using the search bar if no results are found a button asking if someone wants to manually enter a room ID or alias appears. This launches a dialog where the user can type in an alias or ID and it has some basic checking to make sure the string looks as expected.

The new functionality also generically works for joining rooms and adding children to spaces.
2023-10-11 15:53:21 +00:00
l10n daemon script
0730f15e2b GIT_SILENT Sync po/docbooks with svn 2023-10-11 02:48:51 +00:00
l10n daemon script
136856f3c3 GIT_SILENT Sync po/docbooks with svn 2023-10-10 02:20:01 +00:00
l10n daemon script
763b6af076 GIT_SILENT Sync po/docbooks with svn 2023-10-09 02:21:59 +00:00
Tobias Fella
ac231320a3 Fix closing the login dialog 2023-10-08 10:01:01 +00:00
Tobias Fella
87aee162f1 Fix logout from accounts page 2023-10-08 09:52:47 +00:00
Tobias Fella
0899db31af Fix showing logout confirmation dialog 2023-10-08 09:48:27 +00:00
l10n daemon script
b4198bc13b GIT_SILENT Sync po/docbooks with svn 2023-10-08 02:10:30 +00:00
Tobias Fella
c6bfe73d26 Don't show "No Topic" 2023-10-07 16:43:07 +00:00
Tobias Fella
d490dffa36 Fix opening encryption confirmation dialog 2023-10-07 16:26:43 +00:00
Heiko Becker
43b2b71b73 GIT_SILENT Update Appstream for new release
(cherry picked from commit ffbd92317e)
2023-10-07 18:01:42 +02:00
l10n daemon script
39a51d1f35 GIT_SILENT Sync po/docbooks with svn 2023-10-06 02:14:03 +00:00
Carl Schwan
2eb26ffbb3 Fix typo 2023-10-05 11:13:09 +02:00
Carl Schwan
87ef55215f Allow reporting others
Instead of only being able to report yourself

BUG: 475227
2023-10-05 11:02:39 +02:00
Carl Schwan
f6186aad2e Fix right clicking on chat list delegate
BUG: 475226
2023-10-05 10:59:37 +02:00
Carl Schwan
2251edbf86 Fix invalid attempt to destroy() an indestructible object 2023-10-05 10:58:00 +02:00
l10n daemon script
1c55649740 GIT_SILENT Sync po/docbooks with svn 2023-10-05 02:11:56 +00:00
l10n daemon script
aa0b6613de GIT_SILENT Sync po/docbooks with svn 2023-10-04 02:11:42 +00:00
Yuri Chornoivan
f948e813b6 Fix minor typo 2023-10-03 08:36:04 +03:00
l10n daemon script
b5c6411aad GIT_SILENT Sync po/docbooks with svn 2023-10-03 02:13:22 +00:00
James Graham
b1daa76d9f Fix image reply sizing
Use height rather than implicitHeight for the loader so that replies with images always size properly
2023-10-02 18:55:54 +00:00
James Graham
7180fa022b Room Settings Parents
Add the ability to manage parent rooms from a child, this includes:
- viewing parents
- adding a new parent
- removing an existing one

Follows the rules from the matrix spec https://spec.matrix.org/v1.7/client-server-api/#mspaceparent-relationships
2023-10-02 18:41:17 +00:00
Ingo Klöcker
17bc08270d Tag Windows screenshots with new environment attribute
appstream now officially supports an environment attribute for screenshots.
2023-10-02 19:43:56 +02:00
Tobias Fella
d4cb27eca4 Make singletons owned by the C++ side 2023-10-02 16:29:04 +00:00
Fushan Wen
541350e678 appiumtests: port away from deprecated desired_capabilities
AppiumOptions replaces it
2023-10-02 15:48:58 +00:00
l10n daemon script
843deefaf8 GIT_SILENT Sync po/docbooks with svn 2023-10-02 02:16:12 +00:00
James Graham
070d579bc2 Restore the show author functionality to bubble 2023-10-01 14:05:42 +00:00
l10n daemon script
add283c9fb GIT_SILENT Sync po/docbooks with svn 2023-10-01 02:30:40 +00:00
James Graham
fe4230b5fd Use variable placeholder instead of string concatenation 2023-09-30 10:54:07 +01:00
l10n daemon script
e8f40d98de GIT_SILENT Sync po/docbooks with svn 2023-09-30 02:13:47 +00:00
James Graham
eba62103a4 Remove Space Child
Add button to remove a child in a space if the user has the correct power levels
2023-09-29 20:15:17 +00:00
Tobias Fella
925393deab Add type registration for KeyVerificationSession 2023-09-29 19:26:41 +00:00
Laurent Montel
abe881caf7 Add missing include moc 2023-09-29 13:45:46 +02:00
Yuri Chornoivan
237a3c9dfb Fix minor typo 2023-09-29 09:08:37 +03:00
l10n daemon script
9715440854 GIT_SILENT Sync po/docbooks with svn 2023-09-29 02:10:20 +00:00
James Graham
ecdad9f965 Space Home Page
Add a space homepage with the ability to both create new room and add existing rooms to the space. This uses a tree model for the space hierarchy and will go to any number of levels. The user should only see the add options if they have appropriate permissions.

This MR also combines the create space and room pages and adds a lot of optional functionality for managing space children.

![image](/uploads/1764b0319241ff870dc39b18b39f5d51/image.png)
2023-09-28 17:36:23 +00:00
Carl Schwan
08711fc927 Fix missing renaming in roomlastmessageprovider 2023-09-28 10:38:31 +02:00
Carl Schwan
e44cd405b7 Fix import name 2023-09-28 10:31:14 +02:00
Carl Schwan
8945e004e2 Optimize room config 2023-09-28 07:37:22 +00:00
Janet Blackquill
c04d8d6f59 Redraw tray icon
It seems at some point in time the 16x16 tray icon got lost/hastily upscaled to a 22x22 tray icon,
which resulted in proportions as well as icon guidelines being slightly off.

This replaces the tray icon with a new one redrawn to adhere to icon guidelines and proportions closer
to the colour icon.
2023-09-27 23:43:55 -04:00
l10n daemon script
58a73c0208 GIT_SILENT Sync po/docbooks with svn 2023-09-28 02:11:52 +00:00
Joshua Goins
852110debd Make it clear that the session is broken when the keys are lost
If you use your private keys (like when deleting the quotient database)
your session is broken as you have differing keys on the server. While
it is possible to work your way out of it, it's better to warn users to
bite the bullet and log in again.
2023-09-27 15:13:06 -04:00
Joshua Goins
6b71d3c78d Make the key verification message horizontally centered 2023-09-27 15:13:00 -04:00
Christophe Marin
f3a0adee39 Fix manpage installation 2023-09-27 16:23:29 +02:00
l10n daemon script
6e7b6c9ce0 GIT_SILENT Sync po/docbooks with svn 2023-09-27 02:12:21 +00:00
James Graham
f67cd7deb5 Remove the now unused author ID role from MessageEventModel
Remove the now unused author ID role from `MessageEventModel`. This can be obtained from the author roles object.
2023-09-26 20:21:08 +00:00
l10n daemon script
931b4b1f9a GIT_SILENT Sync po/docbooks with svn 2023-09-26 02:22:24 +00:00
l10n daemon script
167ed4eca3 GIT_SILENT made messages (after extraction) 2023-09-26 01:46:11 +00:00
l10n daemon script
7d5b2c1b6a GIT_SILENT Sync po/docbooks with svn 2023-09-25 02:15:50 +00:00
l10n daemon script
be7b1e49b4 GIT_SILENT Sync po/docbooks with svn 2023-09-24 02:09:06 +00:00
Tobias Fella
957419070a Remove unused includes 2023-09-23 22:43:48 +02:00
Carl Schwan
f22107c8ab Colorful emoji in reaction
Use ICU to determine if the string contains only emojis
2023-09-23 22:16:11 +02:00
215 changed files with 119206 additions and 101280 deletions

View File

@@ -19,6 +19,7 @@
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [

View File

@@ -3,7 +3,7 @@
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/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

View File

@@ -13,8 +13,10 @@ Dependencies:
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
@@ -25,7 +27,6 @@ Dependencies:
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'

View File

@@ -41,3 +41,7 @@ License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: autotests/data/*
Copyright: none
License: CC0-1.0

View File

@@ -7,9 +7,9 @@
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_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "01")
set(RELEASE_SERVICE_VERSION_MICRO "75")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -37,6 +37,7 @@ include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -57,15 +58,15 @@ set_package_properties(Qt6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if(ANDROID)
@@ -76,11 +77,17 @@ if(ANDROID)
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
set_package_properties(KF6QQC2DesktopStyle 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)
@@ -95,7 +102,8 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server"
)
if (NOT TARGET Olm::Olm)
# The android part is just for CI. We do NOT support any builds without E2EE
if (NOT TARGET Olm::Olm AND NOT ANDROID)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
@@ -111,7 +119,8 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
ecm_find_qmlmodule(QtLocation)
ecm_find_qmlmodule(org.kde.prison)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES

View File

@@ -9,9 +9,9 @@
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.qt.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:label="NeoChat"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop"

View File

@@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.4.1'
}
}
@@ -35,7 +35,7 @@ android {
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
@@ -44,17 +44,20 @@ android {
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']

View File

@@ -4,25 +4,26 @@
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
import os
import time
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(self):
desired_caps = {}
desired_caps["app"] = "neochat --ignore-ssl-errors"
desired_caps["timeouts"] = {'implicit': 10000}
self.driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723',
desired_capabilities=desired_caps)
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
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
@@ -45,5 +46,6 @@ class LoginTest(unittest.TestCase):
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

@@ -3,6 +3,8 @@
enable_testing()
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
@@ -32,3 +34,15 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)

View File

@@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTest>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include "chatbarcache.h"
#include "neochatroom.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 ChatBarCacheTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void edit();
void attachment();
};
void ChatBarCacheTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
QFile testMinSyncFile;
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
testMinSyncFile.open(QIODevice::ReadOnly);
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
room->update(std::move(roomData));
}
void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::noRoom()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
}
QTEST_MAIN(ChatBarCacheTest)
#include "chatbarcachetest.moc"

View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -0,0 +1,381 @@
{
"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
}
},
{
"content": {
"body": "Thread root",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$threadroot:example.org",
"origin_server_ts": 1690821582879,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "Thread message 1",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadroot:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage1:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "Thread message 2",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadmessage1:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage2:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,87 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,124 @@
{
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -39,33 +39,61 @@ private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
EventHandler eventHandler;
EventHandler emptyHandler;
EventHandler noEventHandler;
private Q_SLOTS:
void initTestCase();
void nullSetEvent();
void eventId();
void nullEventId();
void delegateType_data();
void delegateType();
void nullDelegateType();
void author();
void nullAuthor();
void authorDisplayName();
void nullAuthorDisplayName();
void time();
void nullTime();
void timeString();
void nullTimeString();
void highlighted();
void nullHighlighted();
void hidden();
void nullHidden();
void body();
void nullBody();
void genericBody_data();
void genericBody();
void nullGenericBody();
void mediaInfo();
void nullMediaInfo();
void linkPreviewer();
void nullLinkPreviewer();
void reactions();
void nullReactions();
void hasReply();
void nullHasReply();
void replyId();
void nullReplyId();
void replyDelegateType();
void nullReplyDelegateType();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
void nullReplyBody();
void replyMediaInfo();
void nullReplyMediaInfo();
void thread();
void nullThread();
void location();
void nullLocation();
void readMarkers();
void nullReadMarkers();
void cleanup();
};
void EventHandlerTest::initTestCase()
@@ -73,332 +101,21 @@ 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());
QFile testEventHandlerSyncFile;
testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json"));
testEventHandlerSyncFile.open(QIODevice::ReadOnly);
const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
noEventHandler.setRoom(room);
}
void EventHandlerTest::nullSetEvent()
{
QTest::ignoreMessage(QtWarningMsg, "cannot setEvent when m_room is set to nullptr.");
emptyHandler.setEvent(room->messageEvents().at(0).get());
}
void EventHandlerTest::eventId()
@@ -408,6 +125,12 @@ void EventHandlerTest::eventId()
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
@@ -431,6 +154,12 @@ void EventHandlerTest::delegateType()
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::nullDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
@@ -448,6 +177,15 @@ void EventHandlerTest::author()
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::authorDisplayName()
{
auto event = room->messageEvents().at(1).get();
@@ -456,6 +194,15 @@ void EventHandlerTest::authorDisplayName()
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();
@@ -465,6 +212,16 @@ void EventHandlerTest::time()
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTime(true), QDateTime());
}
void EventHandlerTest::timeString()
{
auto event = room->messageEvents().at(0).get();
@@ -486,6 +243,16 @@ void EventHandlerTest::timeString()
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::nullTimeString()
{
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
}
void EventHandlerTest::highlighted()
{
auto event = room->messageEvents().at(2).get();
@@ -499,6 +266,15 @@ void EventHandlerTest::highlighted()
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
auto event = room->messageEvents().at(3).get();
@@ -512,6 +288,15 @@ void EventHandlerTest::hidden()
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
auto event = room->messageEvents().at(0).get();
@@ -523,6 +308,15 @@ void EventHandlerTest::body()
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullBody()
{
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getPlainBody(), QString());
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
@@ -545,6 +339,12 @@ void EventHandlerTest::genericBody()
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::nullGenericBody()
{
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
@@ -568,6 +368,15 @@ void EventHandlerTest::mediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
}
void EventHandlerTest::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
@@ -581,6 +390,15 @@ void EventHandlerTest::linkPreviewer()
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::nullLinkPreviewer()
{
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions()
{
auto event = room->messageEvents().at(0).get();
@@ -589,6 +407,15 @@ void EventHandlerTest::reactions()
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
}
void EventHandlerTest::nullReactions()
{
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReactions(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReactions(), nullptr);
}
void EventHandlerTest::hasReply()
{
auto event = room->messageEvents().at(5).get();
@@ -602,6 +429,12 @@ void EventHandlerTest::hasReply()
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::nullHasReply()
{
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
auto event = room->messageEvents().at(5).get();
@@ -615,6 +448,12 @@ void EventHandlerTest::replyId()
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
@@ -628,6 +467,15 @@ void EventHandlerTest::replyDelegateType()
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::nullReplyDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
@@ -651,6 +499,15 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
auto event = room->messageEvents().at(5).get();
@@ -662,6 +519,15 @@ void EventHandlerTest::replyBody()
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullReplyBody()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReplyPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyPlainBody(), QString());
}
void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
@@ -686,6 +552,47 @@ void EventHandlerTest::replyMediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullReplyMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
}
void EventHandlerTest::thread()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), false);
QCOMPARE(eventHandler.threadRoot(), QString());
event = room->messageEvents().at(9).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org"));
event = room->messageEvents().at(10).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::nullThread()
{
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
QCOMPARE(emptyHandler.isThreaded(), false);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
}
void EventHandlerTest::location()
{
auto event = room->messageEvents().at(7).get();
@@ -696,6 +603,18 @@ void EventHandlerTest::location()
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::nullLocation()
{
QTest::ignoreMessage(QtWarningMsg, "getLatitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLatitude(), -100.0);
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLongitude(), -200.0);
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
}
void EventHandlerTest::readMarkers()
{
auto event = room->messageEvents().at(0).get();
@@ -725,5 +644,37 @@ void EventHandlerTest::readMarkers()
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
void EventHandlerTest::nullReadMarkers()
{
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
void EventHandlerTest::cleanup()
{
eventHandler.setEvent(nullptr);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -42,101 +42,18 @@ void NeoChatRoomTest::initTestCase()
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1235
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
QFile testMinSyncFile;
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
testMinSyncFile.open(QIODevice::ReadOnly);
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
room->update(std::move(roomData));
}
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example\ntext message"));
}
void NeoChatRoomTest::eventTest()

View File

@@ -40,6 +40,7 @@ private Q_SLOTS:
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void formatBlockQuote();
void sendSimpleStringCase();
void sendSingleParaMarkup();
@@ -66,6 +67,7 @@ private Q_SLOTS:
void receiveRichEdited_data();
void receiveRichEdited();
void receiveLineSeparator();
void receiveRichCodeUrl();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
@@ -78,131 +80,11 @@ void TextHandlerTest::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"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an **example** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
QFile testTextHandlerSyncFile;
testTextHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-texthandler-sync.json"));
testTextHandlerSyncFile.open(QIODevice::ReadOnly);
const auto testTextHandlerSyncJson = QJsonDocument::fromJson(testTextHandlerSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testTextHandlerSyncJson.object());
room->update(std::move(roomData));
}
@@ -265,6 +147,16 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::formatBlockQuote()
{
auto input = QStringLiteral("<blockquote>\n<p>Lorem Ispum</p>\n</blockquote>");
auto expectedOutput = QStringLiteral("<blockquote><table><tr><td>\u201CLorem Ispum\u201D</td></tr></table></blockquote>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), expectedOutput);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
@@ -577,8 +469,9 @@ void TextHandlerTest::receiveRichEdited_data()
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote")
<< QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote><table><tr><td>\u201CEdited\u201D</td></tr></table></blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
}
void TextHandlerTest::receiveRichEdited()
@@ -647,5 +540,13 @@ void TextHandlerTest::linkPreviewsReject()
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
void TextHandlerTest::receiveRichCodeUrl()
{
auto input = QStringLiteral("<code>https://kde.org</code>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
}
QTEST_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 ${MAN_INSTALL_DIR})
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})

View File

@@ -11,6 +11,7 @@
</provides>
<name>NeoChat</name>
<name xml:lang="ar">نيوتشات</name>
<name xml:lang="ast">NeoChat</name>
<name xml:lang="az">NeoChat</name>
<name xml:lang="ca">NeoChat</name>
<name xml:lang="ca-valencia">NeoChat</name>
@@ -53,6 +54,7 @@
<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>
@@ -97,7 +99,7 @@ to provide a convergent experience across multiple platforms.</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>
@@ -282,7 +284,7 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot x-kde-os="windows">
<screenshot environment="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
@@ -309,7 +311,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot x-kde-os="windows">
<screenshot environment="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
@@ -341,6 +343,8 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<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

@@ -4,6 +4,7 @@
Version=1.5
Name=NeoChat
Name[ar]=نيوتشات
Name[ast]=NeoChat
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat

View File

@@ -1 +1,8 @@
<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>
<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>

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 752 B

File diff suppressed because it is too large Load Diff

4414
po/ast/neochat.po Normal file

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,12 @@ add_library(neochat STATIC
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
@@ -127,6 +133,12 @@ add_library(neochat STATIC
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
roomlastmessageprovider.cpp
roomlastmessageprovider.h
chatbarcache.cpp
chatbarcache.h
colorschemer.cpp
colorschemer.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -134,30 +146,31 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/main.qml
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
qml/SpaceListContextMenu.qml
qml/UserInfo.qml
qml/LoadingPage.qml
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/Security.qml
qml/RoomSecurity.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
@@ -207,7 +220,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/CreateSpaceDialog.qml
qml/EmojiDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
@@ -271,6 +283,13 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomMedia.qml
qml/ChooseRoomDialog.qml
qml/ShareAction.qml
qml/SpaceHomePage.qml
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
@@ -292,6 +311,13 @@ ecm_qt_declare_logging_category(neochat
DEFAULT_SEVERITY Info
)
ecm_qt_declare_logging_category(neochat
HEADER "chatdocumenthandler_logging.h"
IDENTIFIER "ChatDocumentHandling"
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app
main.cpp
)
@@ -312,16 +338,15 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
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)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_link_libraries(neochat PUBLIC KF6::WindowSystem ICU::uc)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
@@ -331,7 +356,27 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
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_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::ColorScheme
KF6::ItemModels
QuotientQt6
cmark::cmark
QCoro::Core
)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -417,7 +462,9 @@ if(ANDROID)
"preferences-desktop-notification"
"computer-symbolic"
"gps"
"system-users-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR/android})
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})

View File

@@ -38,45 +38,33 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
void ActionsHandler::handleNewMessage()
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
{
checkEffects(m_room->chatBoxText());
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->setChatBoxAttachmentPath({});
m_room->setChatBoxText({});
if (!chatBarCache) {
return;
}
QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
checkEffects(chatBarCache->text());
if (!chatBarCache->attachmentPath().isEmpty()) {
QUrl url(chatBarCache->attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
m_room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text());
chatBarCache->setAttachmentPath({});
chatBarCache->setText({});
return;
}
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
}
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
@@ -87,14 +75,14 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
}
handledText = handledText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
mentions->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
@@ -134,7 +122,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
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 = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
@@ -161,7 +149,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
return;
}
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString());
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
void ActionsHandler::checkEffects(const QString &text)

View File

@@ -8,6 +8,7 @@
#include <Quotient/events/roommessageevent.h>
#include "chatbarcache.h"
#include "neochatroom.h"
class NeoChatRoom;
@@ -51,21 +52,15 @@ Q_SIGNALS:
void showEffect(const QString &effect);
public Q_SLOTS:
/**
* @brief Pre-process text and send message.
* @brief Pre-process text and send message event.
*/
void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
void handleMessageEvent(ChatBarCache *chatBarCache);
private:
NeoChatRoom *m_room = nullptr;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
QString handleMentions(QString handledText, QList<Mention> *mentions);
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
};

177
src/chatbarcache.cpp Normal file
View File

@@ -0,0 +1,177 @@
// 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 "chatbarcache.h"
#include "eventhandler.h"
#include "neochatroom.h"
ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent)
{
}
QString ChatBarCache::text() const
{
return m_text;
}
void ChatBarCache::setText(const QString &text)
{
if (text == m_text) {
return;
}
m_text = text;
Q_EMIT textChanged();
}
bool ChatBarCache::isReplying() const
{
return m_relationType == Reply && !m_relationId.isEmpty();
}
QString ChatBarCache::replyId() const
{
if (m_relationType != Reply) {
return {};
}
return m_relationId;
}
void ChatBarCache::setReplyId(const QString &replyId)
{
if (m_relationType == Reply && m_relationId == replyId) {
return;
}
m_relationId = replyId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Reply;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
bool ChatBarCache::isEditing() const
{
return m_relationType == Edit && !m_relationId.isEmpty();
}
QString ChatBarCache::editId() const
{
if (m_relationType != Edit) {
return {};
}
return m_relationId;
}
void ChatBarCache::setEditId(const QString &editId)
{
if (m_relationType == Edit && m_relationId == editId) {
return;
}
m_relationId = editId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Edit;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
QVariantMap ChatBarCache::relationUser() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
if (m_relationId.isEmpty()) {
return room->getUser(nullptr);
}
return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId()));
}
QString ChatBarCache::relationMessage() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
if (m_relationId.isEmpty()) {
return {};
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
EventHandler eventhandler;
eventhandler.setRoom(room);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
eventhandler.setEvent(&**event);
return eventhandler.getPlainBody();
}
return {};
}
bool ChatBarCache::isThreaded() const
{
return !m_threadId.isEmpty();
}
QString ChatBarCache::threadId() const
{
return m_threadId;
}
void ChatBarCache::setThreadId(const QString &threadId)
{
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
}
QString ChatBarCache::attachmentPath() const
{
return m_attachmentPath;
}
void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
{
if (attachmentPath == m_attachmentPath) {
return;
}
m_attachmentPath = attachmentPath;
m_relationType = None;
m_relationId = QString();
Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;
}
QString ChatBarCache::savedText() const
{
return m_savedText;
}
void ChatBarCache::setSavedText(const QString &savedText)
{
m_savedText = savedText;
}
#include "moc_chatbarcache.cpp"

201
src/chatbarcache.h Normal file
View File

@@ -0,0 +1,201 @@
// 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 <QTextCursor>
/**
* @brief Defines a user mention in the current chat or edit text.
*/
struct Mention {
QTextCursor cursor; /**< Contains the mention's text and position in the text. */
QString text; /**< The inserted text of the mention. */
int start = 0; /**< Start position of the mention. */
int position = 0; /**< End position of the mention. */
QString id; /**< The id the mention (used to create link when sending the message). */
};
/**
* @class ChatBarCache
*
* A class to cache data from a chat bar.
*
* A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
*
* This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
* have a separate cache for each chat bar.
*
* @note The NeoChatRoom which this component is created in is expected to be set
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, MessageEditComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief The text in the chat bar.
*
* Due to problems with QTextDocument, unlike the other properties here,
* text is *not* used to store the text when switching rooms.
*/
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
/**
* @brief Whether the chat bar is currently replying to a message.
*/
Q_PROPERTY(bool isReplying READ isReplying NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being replied to, if any.
*
* Will return empty if the RelationType is currently set to None or Edit.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString replyId READ replyId WRITE setReplyId NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is currently editing a message.
*/
Q_PROPERTY(bool isEditing READ isEditing NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being edited, if any.
*
* Will return empty if the RelationType is currently set to None or Reply.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an reply or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged)
/**
* @brief Get the user for the message being replied to.
*
* 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.
*
* Returns an empty user if not replying to a message.
*
* The user QVariantMap has 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 getUser, Quotient::User
*/
Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
/**
* @brief The content of the related message.
*
* Will be QString() if no related message.
*/
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is replying in a thread.
*/
Q_PROPERTY(bool isThreaded READ isThreaded NOTIFY threadIdChanged)
/**
* @brief The Matrix message ID of thread root event, if any.
*/
Q_PROPERTY(QString threadId READ threadId WRITE setThreadId NOTIFY threadIdChanged)
/**
* @brief The local path for a file to send, if any.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or reply.
*/
Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged)
public:
/**
* @brief Describes the type of relation which relationId can refer to.
*
* A chat bar can only be relating to a single message at a time making these
* exclusive.
*/
enum RelationType {
Reply, /**< The current relation is a message being replied to. */
Edit, /**< The current relation is a message being edited. */
None, /**< There is currently no relation event */
};
Q_ENUM(RelationType)
explicit ChatBarCache(QObject *parent = nullptr);
QString text() const;
void setText(const QString &text);
bool isReplying() const;
QString replyId() const;
void setReplyId(const QString &replyId);
bool isEditing() const;
QString editId() const;
void setEditId(const QString &editId);
QVariantMap relationUser() const;
QString relationMessage() const;
bool isThreaded() const;
QString threadId() const;
void setThreadId(const QString &threadId);
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
QList<Mention> *mentions();
/**
* @brief Get the saved chat bar text.
*/
QString savedText() const;
/**
* @brief Save the chat bar text.
*/
void setSavedText(const QString &savedText);
Q_SIGNALS:
void textChanged();
void relationIdChanged();
void threadIdChanged();
void attachmentPathChanged();
private:
QString m_text = QString();
QString m_relationId = QString();
RelationType m_relationType = RelationType::None;
QString m_threadId = QString();
QString m_attachmentPath = QString();
QList<Mention> m_mentions;
QString m_savedText;
};

View File

@@ -14,6 +14,8 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "chatdocumenthandler_logging.h"
class SyntaxHighlighter : public QSyntaxHighlighter
{
public:
@@ -57,11 +59,12 @@ public:
setFormat(error.first, error.second.size(), errorFormat);
}
}
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
auto room = handler->room();
if (!room) {
return;
}
auto mentions = room->mentions();
auto mentions = handler->chatBarCache()->mentions();
mentions->erase(std::remove_if(mentions->begin(),
mentions->end(),
[this](auto &mention) {
@@ -97,21 +100,16 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
, m_document(nullptr)
, m_cursorPosition(-1)
, m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel())
, m_completionModel(new CompletionModel(this))
{
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr;
if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
}
previousRoom = m_room;
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
@@ -152,20 +150,6 @@ int ChatDocumentHandler::completionStartIndex() const
return start;
}
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_document;
@@ -215,8 +199,31 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
ChatBarCache *ChatDocumentHandler::chatBarCache() const
{
return m_chatBarCache;
}
void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
{
if (m_chatBarCache == chatBarCache) {
return;
}
m_chatBarCache = chatBarCache;
Q_EMIT chatBarCacheChanged();
}
void ChatDocumentHandler::complete(int index)
{
if (m_document == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
@@ -300,26 +307,20 @@ void ChatDocumentHandler::setSelectionEnd(int position)
QString ChatDocumentHandler::getText() const
{
if (!m_room) {
return QString();
}
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
if (!m_chatBarCache) {
qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
return {};
}
return m_chatBarCache->text();
}
void ChatDocumentHandler::pushMention(const Mention mention) const
{
if (!m_room) {
if (!m_chatBarCache) {
qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
return;
}
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
m_room->mentions()->push_back(mention);
}
m_chatBarCache->mentions()->push_back(mention);
}
QColor ChatDocumentHandler::mentionColor() const

View File

@@ -8,6 +8,7 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "chatbarcache.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
@@ -61,14 +62,6 @@ class ChatDocumentHandler : public QObject
Q_OBJECT
QML_ELEMENT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
/**
* @brief The QQuickTextDocument that is being handled.
*/
@@ -95,13 +88,18 @@ class ChatDocumentHandler : public QObject
* This is typically provided to a qml component to visualise the current
* completion results.
*/
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/**
* @brief The current room that the the text document is being handled for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The cache for the chat bar the text document is being handled for.
*/
Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged)
/**
* @brief The color to highlight user mentions.
*/
@@ -115,9 +113,6 @@ class ChatDocumentHandler : public QObject
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
@@ -133,9 +128,11 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] ChatBarCache *chatBarCache() const;
void setChatBarCache(ChatBarCache *chatBarCache);
Q_INVOKABLE void complete(int index);
void updateCompletions();
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
@@ -145,11 +142,10 @@ public:
void setErrorColor(const QColor &color);
Q_SIGNALS:
void isEditChanged();
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void completionModelChanged();
void chatBarCacheChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
@@ -158,11 +154,10 @@ Q_SIGNALS:
private:
int completionStartIndex() const;
bool m_isEdit = false;
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;
QPointer<ChatBarCache> m_chatBarCache;
bool completionVisible = false;
QColor m_mentionColor;

View File

@@ -6,13 +6,8 @@
#include <qt6keychain/keychain.h>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KWindowConfig>
#ifdef HAVE_WINDOWSYSTEM
#include <KWindowEffects>
#endif
#include <QFile>
#include <QFileInfo>
@@ -65,7 +60,8 @@ Controller::Controller(QObject *parent)
invokeLogin();
});
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] {
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
NeoChatConfig::self()->save();
});
@@ -103,10 +99,6 @@ Controller::Controller(QObject *parent)
}
oldAccountCount = m_accountRegistry.size();
});
QTimer::singleShot(0, this, [this] {
m_pushRuleModel = new PushRuleModel;
});
}
Controller &Controller::instance()
@@ -175,6 +167,8 @@ void Controller::invokeLogin()
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
@@ -194,6 +188,8 @@ void Controller::invokeLogin()
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
Q_EMIT accountsLoadingChanged();
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
@@ -354,12 +350,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
}
PushRuleModel *Controller::pushRuleModel() const
{
return m_pushRuleModel;
}
void Controller::saveWindowGeometry()
@@ -384,56 +374,6 @@ void Controller::joinRoom(const QString &alias)
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
}
void Controller::openOrCreateDirectChat(User *user)
{
const auto existing = activeConnection()->directChats();
if (existing.contains(user)) {
const auto &room = static_cast<NeoChatRoom *>(activeConnection()->room(existing.value(user)));
if (room) {
RoomManager::instance().enterRoom(room);
return;
}
}
activeConnection()->requestDirectChat(user);
}
QString Controller::formatByteSize(double size, int precision) const
{
return QLocale().formattedDataSize(size, precision);
}
QString Controller::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const
{
return KFormat().formatDuration(msecs, options);
}
void Controller::setBlur(QQuickItem *item, bool blur)
{
#ifdef HAVE_WINDOWSYSTEM
auto setWindows = [item, blur]() {
auto reg = QRect(QPoint(0, 0), item->window()->size());
KWindowEffects::enableBackgroundContrast(item->window(), blur, 1, 1, 1, reg);
KWindowEffects::enableBlurBehind(item->window(), blur, reg);
};
disconnect(item->window(), &QQuickWindow::heightChanged, this, nullptr);
disconnect(item->window(), &QQuickWindow::widthChanged, this, nullptr);
connect(item->window(), &QQuickWindow::heightChanged, this, setWindows);
connect(item->window(), &QQuickWindow::widthChanged, this, setWindows);
setWindows();
#endif
}
bool Controller::hasWindowSystem() const
{
#ifdef HAVE_WINDOWSYSTEM
return true;
#else
return false;
#endif
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
@@ -470,14 +410,6 @@ void Controller::setApplicationProxy()
QNetworkProxy::setApplicationProxy(proxy);
}
int Controller::activeConnectionIndex() const
{
auto result = std::find_if(m_accountRegistry.accounts().begin(), m_accountRegistry.accounts().end(), [this](const auto &it) {
return it == m_connection;
});
return result - m_accountRegistry.accounts().begin();
}
bool Controller::isFlatpak() const
{
#ifdef NEOCHAT_FLATPAK

View File

@@ -3,13 +3,10 @@
#pragma once
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
@@ -49,26 +46,11 @@ class Controller : public QObject
*/
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
*/
Q_PROPERTY(PushRuleModel *pushRuleModel READ pushRuleModel CONSTANT)
/**
* @brief The row number in the accounts directory of the active connection.
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/**
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
/**
* @brief Whether KWindowSystem specific features are available.
*/
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
@@ -81,6 +63,8 @@ class Controller : public QObject
*/
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
public:
/**
* @brief Defines the status after an attempt to change the password on an account.
@@ -93,16 +77,15 @@ public:
Q_ENUM(PasswordStatus)
static Controller &instance();
static Controller *create(QQmlEngine *, QJSEngine *)
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
@@ -113,8 +96,6 @@ public:
*/
void dropConnection(NeoChatConnection *c);
int activeConnectionIndex() const;
/**
* @brief Save an access token to the keychain for the given account.
*/
@@ -125,20 +106,8 @@ public:
*/
Q_INVOKABLE void joinRoom(const QString &alias);
/**
* @brief Join a direct chat with the given user.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
[[nodiscard]] bool supportSystemTray() const;
/**
* @brief Set the background blur status of the given item.
*/
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
bool isOnline() const;
/**
@@ -150,20 +119,6 @@ public:
bool isFlatpak() const;
/**
* @brief Return a string for the input timestamp.
*
* The output format depends on the KFormat::DurationFormatOptions chosen.
*
* @sa KFormat::DurationFormatOptions
*/
Q_INVOKABLE QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options = KFormat::DefaultDuration) const;
/**
* @brief Return a human readable string for a given input number of bytes.
*/
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
@@ -186,10 +141,8 @@ private:
bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
QPointer<PushRuleModel> m_pushRuleModel;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
private Q_SLOTS:
void invokeLogin();
@@ -212,7 +165,7 @@ Q_SIGNALS:
void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url);
void isOnlineChanged(bool isOnline);
void activeConnectionIndexChanged();
void accountsLoadingChanged();
public Q_SLOTS:
void saveWindowGeometry();

View File

@@ -19,6 +19,7 @@
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "delegatetype.h"
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
@@ -50,6 +51,10 @@ const Quotient::Event *EventHandler::getEvent() const
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "cannot setEvent when m_room is set to nullptr.";
return;
}
if (event == m_event) {
return;
}
@@ -181,7 +186,7 @@ QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
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.";
qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
@@ -216,6 +221,15 @@ bool EventHandler::isHighlighted()
bool EventHandler::isHidden()
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
return false;
}
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
@@ -611,6 +625,14 @@ QString EventHandler::getGenericBody() const
QVariantMap EventHandler::getMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
return {};
}
return getMediaInfoForEvent(m_event);
}
@@ -723,6 +745,14 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
@@ -754,7 +784,7 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
@@ -806,16 +836,33 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
bool EventHandler::hasReply() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
return false;
}
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::getReplyId() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
return {};
}
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
DelegateType::Type EventHandler::getReplyDelegateType() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
return DelegateType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return DelegateType::Other;
@@ -901,8 +948,45 @@ QVariantMap EventHandler::getReplyMediaInfo() const
return getMediaInfoForEvent(replyPtr);
}
bool EventHandler::isThreaded() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
return false;
}
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists.
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return getId();
}
return {};
}
float EventHandler::getLatitude() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
return -100.0;
}
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.
@@ -913,6 +997,11 @@ float EventHandler::getLatitude() const
float EventHandler::getLongitude() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
return -200.0;
}
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.
@@ -923,6 +1012,11 @@ float EventHandler::getLongitude() const
QString EventHandler::getLocationAssetType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
return {};
}
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
@@ -932,6 +1026,15 @@ QString EventHandler::getLocationAssetType() const
bool EventHandler::hasReadMarkers() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
return false;
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
@@ -939,6 +1042,15 @@ bool EventHandler::hasReadMarkers() const
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
@@ -959,6 +1071,15 @@ QVariantList EventHandler::getReadMarkers(int maxMarkers) const
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
@@ -971,6 +1092,15 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
QString EventHandler::getReadMarkersString() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
@@ -986,3 +1116,5 @@ QString EventHandler::getReadMarkersString() const
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

View File

@@ -326,6 +326,20 @@ public:
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
bool isThreaded() const;
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
QString threadRoot() const;
/**
* @brief Return the latitude for the event.
*

View File

@@ -3,7 +3,7 @@
#pragma once
#include <QVector>
#include <QList>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/stateevent.h>
@@ -60,7 +60,7 @@ public:
*
* @sa ImagePackImage
*/
QVector<ImagePackEventContent::ImagePackImage> images;
QList<ImagePackEventContent::ImagePackImage> images;
explicit ImagePackEventContent(const QJsonObject &o);

View File

@@ -12,7 +12,8 @@ class LocationHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
QML_SINGLETON
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);

View File

@@ -117,7 +117,7 @@ public:
file.write(buf.constData(), buf.size());
file.flush();
if (oldHandler && (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled)) {
if (oldHandler && (!context.category || (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled))) {
oldHandler(type, context, message);
}
}
@@ -197,7 +197,6 @@ void messageHandler(QtMsgType type, const QMessageLogContext &context, const QSt
break;
case QtFatalMsg:
sInstance()->log(QtInfoMsg, context, message);
abort();
}
}
@@ -212,7 +211,7 @@ void filter(QLoggingCategory *category)
void initLogging()
{
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtDebugMsg).isEnabled(QtDebugMsg);
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtInfoMsg).isEnabled(QtDebugMsg);
oldCategoryFilter = QLoggingCategory::installFilter(filter);
oldHandler = qInstallMessageHandler(messageHandler);
sInstance->setOrigHandler(oldHandler);

View File

@@ -100,7 +100,8 @@ void LoginHelper::init()
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection, &Connection::syncDone, this, []() {
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
Q_EMIT loaded();
Q_EMIT Controller::instance().initiated();
});
}

View File

@@ -79,7 +79,17 @@ class LoginHelper : public QObject
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit LoginHelper(QObject *parent = nullptr);
static LoginHelper &instance()
{
static LoginHelper _instance;
return _instance;
}
static LoginHelper *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
Q_INVOKABLE void init();
@@ -125,6 +135,7 @@ Q_SIGNALS:
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
void loaded();
private:
void setHomeserverReachable(bool reachable);
@@ -141,4 +152,5 @@ private:
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
explicit LoginHelper(QObject *parent = nullptr);
};

View File

@@ -34,69 +34,21 @@
#include "neochat-version.h"
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include <Quotient/room.h>
#include <Quotient/user.h>
#include <Quotient/util.h>
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatdocumenthandler.h"
#include "controller.h"
#include "delegatesizehelper.h"
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "mediasizehelper.h"
#include "models/accountemoticonmodel.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 "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 "controller.h"
#include "logger.h"
#include "matriximageprovider.h"
#include "neochatconfig.h"
#include "roommanager.h"
#include "windowcontroller.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -169,7 +121,7 @@ int main(int argc, char *argv[])
font.setHintingPreference(QFont::PreferNoHinting);
app.setFont(font);
#endif
KLocalizedString::setApplicationDomain("neochat");
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
QGuiApplication::setOrganizationName("KDE"_ls);
@@ -179,7 +131,11 @@ int main(int argc, char *argv[])
i18n("Matrix client"),
KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community"));
about.addAuthor(i18n("Carl Schwan"), i18n("Maintainer"), QStringLiteral("carl@carlschwan.eu"), QStringLiteral("https://carlschwan.eu"));
about.addAuthor(i18n("Carl Schwan"),
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QStringLiteral("https://carlschwan.eu/avatar.png"));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
@@ -188,7 +144,7 @@ int main(int argc, char *argv[])
about.setOrganizationDomain("kde.org");
about.addComponent(QStringLiteral("libQuotient"),
i18n("A Qt5 library to write cross-platform clients for Matrix"),
i18n("A Qt library to write cross-platform clients for Matrix"),
i18nc("<version number> (built against <possibly different version number>)",
"%1 (built against %2)",
Quotient::versionString(),
@@ -201,9 +157,7 @@ int main(int argc, char *argv[])
initLogging();
#if Quotient_VERSION_MINOR == 8
Connection::setEncryptionDefault(true);
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
@@ -212,18 +166,16 @@ int main(int argc, char *argv[])
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
#endif
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->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());
// qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
QQmlApplicationEngine engine;
@@ -271,7 +223,7 @@ int main(int argc, char *argv[])
});
}
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
@@ -284,8 +236,8 @@ int main(int argc, char *argv[])
}
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
auto runner = Runner::create(&engine, &engine);
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, runner, QDBusConnection::ExportScriptableContents);
#endif
QWindow *window = windowFromEngine(&engine);

View File

@@ -13,11 +13,13 @@
#include <KLocalizedString>
#include "controller.h"
#include "neochatconnection.h"
#include <Quotient/connection.h>
using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id))
, requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
@@ -25,6 +27,7 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
@@ -51,24 +54,24 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
return;
}
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
qWarning() << "Current connection is null";
return;
}
// Execute a request on the main thread asynchronously
moveToThread(Controller::instance().activeConnection()->thread());
moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
return;
}
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == Controller::instance().activeConnection()->thread());
job = Controller::instance().activeConnection()->getThumbnail(mediaId, requestedSize);
Q_ASSERT(QThread::currentThread() == m_connection->thread());
job = m_connection->getThumbnail(mediaId, requestedSize);
// Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
@@ -118,7 +121,7 @@ QString ThumbnailResponse::errorString() const
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new ThumbnailResponse(id, requestedSize);
return new ThumbnailResponse(id, requestedSize, m_connection);
}
#include "moc_matriximageprovider.cpp"

View File

@@ -10,6 +10,8 @@
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
@@ -21,7 +23,7 @@ class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize);
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
@@ -33,6 +35,7 @@ private:
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
NeoChatConnection *m_connection;
QImage image;
QString errorStr;
@@ -51,11 +54,27 @@ private:
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider instance;
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
return &instance;
}
/**
* @brief Return a job to provide the image with the given ID.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
NeoChatConnection *m_connection = nullptr;
MatrixImageProvider() = default;
};

View File

@@ -7,10 +7,10 @@
#include <QAbstractListModel>
#include <QCoroTask>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
#include <Quotient/connection.h>

View File

@@ -3,6 +3,7 @@
#include "actionsmodel.h"
#include "chatbarcache.h"
#include "controller.h"
#include "neochatroom.h"
#include "roommanager.h"
@@ -19,7 +20,7 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_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};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
@@ -45,7 +46,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -54,10 +55,10 @@ auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
return QString();
};
QVector<ActionsModel::Action> actions{
QList<ActionsModel::Action> actions{
Action{
QStringLiteral("shrug"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("¯\\\\_(ツ)_/¯ %1").arg(message);
},
true,
@@ -67,7 +68,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("lenny"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("( ͡° ͜ʖ ͡°) %1").arg(message);
},
true,
@@ -77,7 +78,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("tableflip"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("(╯°□°)╯︵ ┻━┻ %1").arg(message);
},
true,
@@ -87,7 +88,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unflip"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("┬──┬ ( ゜-゜ノ) %1").arg(message);
},
true,
@@ -97,7 +98,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbow"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -106,8 +107,8 @@ QVector<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -117,7 +118,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbowme"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -126,8 +127,8 @@ QVector<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Emote,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -137,7 +138,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("plain"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {});
return QString();
},
@@ -148,13 +149,13 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -164,7 +165,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("me"),
[](const QString &text, NeoChatRoom *) {
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
true,
@@ -174,7 +175,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("notice"),
[](const QString &text, NeoChatRoom *) {
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
true,
@@ -184,7 +185,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("invite"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -220,7 +221,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("join"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -244,7 +245,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("knock"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
QString roomName = parts[0];
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
@@ -260,7 +261,7 @@ QVector<ActionsModel::Action> actions{
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = Controller::instance().activeConnection();
auto connection = room->connection();
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
if (parts.length() >= 2) {
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
@@ -276,7 +277,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -284,7 +285,7 @@ QVector<ActionsModel::Action> actions{
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
if (Controller::instance().activeConnection()->room(text) || Controller::instance().activeConnection()->roomByAlias(text)) {
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString();
}
@@ -315,7 +316,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("nick"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -346,7 +347,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ignore"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -374,7 +375,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unignore"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -402,9 +403,8 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("react"),
[](const QString &text, NeoChatRoom *room) {
QString replyEventId = room->chatBoxReplyId();
if (replyEventId.isEmpty()) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
if (chatBarCache->replyId().isEmpty()) {
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
const auto &evt = **it;
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
@@ -413,7 +413,7 @@ QVector<ActionsModel::Action> actions{
}
}
}
room->toggleReaction(replyEventId, text);
room->toggleReaction(chatBarCache->replyId(), text);
return QString();
},
false,
@@ -423,7 +423,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ban"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -462,7 +462,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unban"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -495,7 +495,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("kick"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -574,7 +574,7 @@ QHash<int, QByteArray> ActionsModel::roleNames() const
};
}
QVector<Action> &ActionsModel::allActions() const
QList<Action> &ActionsModel::allActions() const
{
return actions;
}

View File

@@ -7,6 +7,7 @@
#include <QAbstractListModel>
#include <Quotient/events/roommessageevent.h>
class ChatBarCache;
class NeoChatRoom;
/**
@@ -28,7 +29,7 @@ public:
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *)> handle;
std::function<QString(const QString &, NeoChatRoom *, ChatBarCache *)> handle;
/**
* @brief Whether the action is a message type action.
*
@@ -87,7 +88,7 @@ public:
/**
* @brief Return a vector with all supported actions.
*/
QVector<Action> &allActions() const;
QList<Action> &allActions() const;
private:
ActionsModel() = default;

View File

@@ -15,13 +15,28 @@
using namespace Quotient;
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchEmojis();
}
NeoChatConnection *CustomEmojiModel::connection() const
{
return m_connection;
}
void CustomEmojiModel::fetchEmojis()
{
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
return;
}
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
if (data == nullptr) {
return;
}
@@ -53,10 +68,10 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
using namespace Quotient;
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_ls].toObject();
@@ -78,7 +93,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
});
json["images"_ls] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
});
}
@@ -86,7 +101,7 @@ void CustomEmojiModel::removeEmoji(const QString &name)
{
using namespace Quotient;
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
Q_ASSERT(data);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
@@ -109,18 +124,18 @@ void CustomEmojiModel::removeEmoji(const QString &name)
emojiData.remove(_name);
json["emoticons"_ls] = emojiData;
}
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
}
CustomEmojiModel::CustomEmojiModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
if (!Controller::instance().activeConnection()) {
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!m_connection) {
return;
}
CustomEmojiModel::fetchEmojis();
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &id) {
connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
if (id != QStringLiteral("im.ponies.user_emotes")) {
return;
}

View File

@@ -8,6 +8,8 @@
#include <QRegularExpression>
#include <memory>
class NeoChatConnection;
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
@@ -31,6 +33,8 @@ class CustomEmojiModel : public QAbstractListModel
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
@@ -51,8 +55,9 @@ public:
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *, QJSEngine *)
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
@@ -97,9 +102,16 @@ public:
*/
Q_INVOKABLE void removeEmoji(const QString &name);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
NeoChatConnection *m_connection = nullptr;
void fetchEmojis();
};

View File

@@ -24,8 +24,8 @@ DevicesModel::DevicesModel(QObject *parent)
void DevicesModel::fetchDevices()
{
if (Controller::instance().activeConnection()) {
auto job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
if (m_connection) {
auto job = m_connection->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel();
m_devices = job->devices();
@@ -102,7 +102,7 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
for (index = 0; m_devices[index].deviceId != deviceId; index++)
;
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
auto job = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {
@@ -117,9 +117,9 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls;
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, Controller::instance().activeConnection()->user()->id()}};
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, m_connection->user()->id()}};
authData["identifier"_ls] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
auto *innerJob = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess);
} else {
onSuccess();
@@ -132,7 +132,7 @@ void DevicesModel::setName(const QString &deviceId, const QString &name)
int index;
for (index = 0; m_devices[index].deviceId != deviceId; index++);
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
auto job = m_connection->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
QString oldName = m_devices[index].displayName;
beginResetModel();
m_devices[index].displayName = name;
@@ -160,7 +160,7 @@ void DevicesModel::setConnection(Connection *connection)
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) {
Q_UNUSED(deviceId);
if (userId == Controller::instance().activeConnection()->userId()) {
if (userId == m_connection->userId()) {
fetchDevices();
}
});

View File

@@ -98,6 +98,6 @@ Q_SIGNALS:
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
QList<Quotient::Device> m_devices;
QPointer<Quotient::Connection> m_connection;
};

View File

@@ -14,6 +14,8 @@
EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent)
, m_config(KSharedConfig::openStateConfig())
, m_configGroup(KConfigGroup(m_config, QStringLiteral("Editor")))
{
if (_emojis.isEmpty()) {
#include "emojis.h"
@@ -61,9 +63,9 @@ QHash<int, QByteArray> EmojiModel::roleNames() const
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
}
QVariantList EmojiModel::history() const
QStringList EmojiModel::lastUsedEmojis() const
{
return m_settings.value(QStringLiteral("Editor/emojis"), QVariantList()).toList();
return m_configGroup.readEntry(QStringLiteral("lastUsedEmojis"), QStringList());
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
@@ -93,19 +95,21 @@ QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
void EmojiModel::emojiUsed(const QVariant &modelData)
{
QVariantList list = history();
auto list = lastUsedEmojis();
const auto emoji = modelData.value<Emoji>();
auto it = list.begin();
while (it != list.end()) {
if ((*it).value<Emoji>().unicode == modelData.value<Emoji>().unicode) {
if (*it == emoji.shortName) {
it = list.erase(it);
} else {
it++;
}
}
list.push_front(modelData);
m_settings.setValue(QStringLiteral("Editor/emojis"), list);
list.push_front(emoji.shortName);
m_configGroup.writeEntry(QStringLiteral("lastUsedEmojis"), list);
Q_EMIT historyChanged();
}
@@ -113,11 +117,11 @@ void EmojiModel::emojiUsed(const QVariant &modelData)
QVariantList EmojiModel::emojis(Category category) const
{
if (category == History) {
return history();
return emojiHistory();
}
if (category == HistoryNoCustom) {
QVariantList list;
for (const auto &e : history()) {
for (const auto &e : emojiHistory()) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
@@ -217,4 +221,19 @@ QVariantList EmojiModel::categoriesWithCustom() const
return cats;
}
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
for (const auto &historicEmoji : lastUsedEmojis()) {
for (const auto &emojiCategory : _emojis) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);
}
}
}
}
return list;
}
#include "moc_emojimodel.cpp"

View File

@@ -3,10 +3,11 @@
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
#include <QSettings>
struct Emoji {
Emoji(QString unicode, QString shortname, bool isCustom = false)
@@ -23,21 +24,6 @@ struct Emoji {
}
Emoji() = default;
friend QDataStream &operator<<(QDataStream &arch, const Emoji &object)
{
arch << object.unicode;
arch << object.shortName;
return arch;
}
friend QDataStream &operator>>(QDataStream &arch, Emoji &object)
{
arch >> object.unicode;
arch >> object.shortName;
object.isCustom = object.unicode.startsWith(QStringLiteral("image://"));
return arch;
}
QString unicode;
QString shortName;
QString description;
@@ -63,11 +49,6 @@ class EmojiModel : public QAbstractListModel
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of recently used emojis.
*/
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
/**
* @brief Return a list of emoji categories.
*
@@ -86,8 +67,9 @@ public:
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *, QJSEngine *)
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
@@ -176,7 +158,11 @@ public:
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
Q_INVOKABLE QVariantList history() const;
/**
* @brief Return a list of the last used emoji shortnames
*/
QStringList lastUsedEmojis() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;
@@ -189,7 +175,10 @@ public Q_SLOTS:
private:
static QHash<Category, QVariantList> _emojis;
// TODO: Port away from QSettings
QSettings m_settings;
/// Returns QVariants containing the last used Emojis
QVariantList emojiHistory() const;
KSharedConfig::Ptr m_config;
KConfigGroup m_configGroup;
EmojiModel(QObject *parent = nullptr);
};

View File

@@ -151,7 +151,7 @@ void ImagePacksModel::setShowEmoticons(bool showEmoticons)
m_showEmoticons = showEmoticons;
Q_EMIT showEmoticonsChanged();
}
QVector<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
{
if (index < 0 || index >= m_events.size()) {
return {};

View File

@@ -5,9 +5,9 @@
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QList>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
class NeoChatRoom;
@@ -86,7 +86,7 @@ public:
/**
* @brief Return a vector of the images in the pack at the given index.
*/
[[nodiscard]] QVector<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
Q_SIGNALS:
void roomChanged();
@@ -96,7 +96,7 @@ Q_SIGNALS:
private:
QPointer<NeoChatRoom> m_room;
QVector<Quotient::ImagePackEventContent> m_events;
QList<Quotient::ImagePackEventContent> m_events;
bool m_showStickers = true;
bool m_showEmoticons = true;
void reloadImages();

View File

@@ -1,112 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "keywordnotificationrulemodel.h"
#include "controller.h"
#include "notificationsmanager.h"
#include <QDebug>
#include <Quotient/connection.h>
#include <Quotient/converters.h>
#include <Quotient/csapi/definitions/push_ruleset.h>
#include <Quotient/csapi/pushrules.h>
#include <Quotient/jobs/basejob.h>
KeywordNotificationRuleModel::KeywordNotificationRuleModel(QObject *parent)
: QAbstractListModel(parent)
{
if (Controller::instance().activeConnection()) {
controllerConnectionChanged();
}
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &KeywordNotificationRuleModel::controllerConnectionChanged);
}
void KeywordNotificationRuleModel::controllerConnectionChanged()
{
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &KeywordNotificationRuleModel::updateNotificationRules);
updateNotificationRules("m.push_rules");
}
void KeywordNotificationRuleModel::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
return;
}
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"].toObject());
const QVector<Quotient::PushRule> contentRules = ruleData.content;
beginResetModel();
m_notificationRules.clear();
for (const auto &i : contentRules) {
if (!m_notificationRules.contains(i.ruleId) && i.ruleId[0] != '.') {
m_notificationRules.append(i.ruleId);
}
}
endResetModel();
}
QVariant KeywordNotificationRuleModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= m_notificationRules.count()) {
qDebug() << "KeywordNotificationRuleModel, something's wrong: index.row() >= m_notificationRules.count()";
return {};
}
if (role == NameRole) {
return m_notificationRules.at(index.row());
}
return {};
}
int KeywordNotificationRuleModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_notificationRules.count();
}
void KeywordNotificationRuleModel::addKeyword(const QString &keyword)
{
if (m_notificationRules.count() == 0) {
NotificationsManager::instance().initializeKeywordNotificationAction();
}
const QVector<QVariant> actions = NotificationsManager::instance().getKeywordNotificationActions();
auto job = Controller::instance()
.activeConnection()
->callApi<Quotient::SetPushRuleJob>("global", "content", keyword, actions, "", "", QVector<Quotient::PushCondition>(), keyword);
connect(job, &Quotient::BaseJob::success, this, [this, keyword]() {
beginInsertRows(QModelIndex(), m_notificationRules.count(), m_notificationRules.count());
m_notificationRules.append(keyword);
endInsertRows();
});
}
void KeywordNotificationRuleModel::removeKeywordAtIndex(int index)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>("global", "content", m_notificationRules[index]);
connect(job, &Quotient::BaseJob::success, this, [this, index]() {
beginRemoveRows(QModelIndex(), index, index);
m_notificationRules.removeAt(index);
endRemoveRows();
if (m_notificationRules.count() == 0) {
NotificationsManager::instance().deactivateKeywordNotificationAction();
}
});
}
QHash<int, QByteArray> KeywordNotificationRuleModel::roleNames() const
{
return {{NameRole, QByteArrayLiteral("name")}};
}
#include "moc_keywordnotificationrulemodel.cpp"

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/csapi/definitions/push_rule.h>
#include <QAbstractListModel>
/**
* @class KeywordNotificationRuleModel
*
* This class defines the model for managing notification push rule keywords.
*/
class KeywordNotificationRuleModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum EventRoles {
NameRole = Qt::DisplayRole, /**< The push rule keyword. */
};
KeywordNotificationRuleModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa EventRoles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Add a new keyword to the model.
*/
Q_INVOKABLE void addKeyword(const QString &keyword);
/**
* @brief Remove a keyword from the model.
*/
Q_INVOKABLE void removeKeywordAtIndex(int index);
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
QList<QString> m_notificationRules;
};

View File

@@ -47,6 +47,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ReplyDelegateTypeRole] = "replyDelegateType";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[IsThreadedRole] = "isThreaded";
roles[ThreadRootRole] = "threadRoot";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
@@ -55,7 +57,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
roles[AuthorIdRole] = "authorId";
roles[VerifiedRole] = "verified";
roles[AuthorDisplayNameRole] = "authorDisplayName";
roles[IsRedactedRole] = "isRedacted";
@@ -99,7 +100,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
createEventObjects(&*event->viewAs<RoomMessageEvent>());
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
createEventObjects(roomMessageEvent);
}
}
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
@@ -119,9 +122,8 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
for (auto &&event : events) {
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
if (message != nullptr) {
createEventObjects(message);
if (NeoChatConfig::self()->showFancyEffects()) {
QString planBody = message->plainBody();
// snowflake
@@ -157,8 +159,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
});
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
createEventObjects(message);
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
createEventObjects(roomMessageEvent);
}
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
@@ -227,7 +230,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) {
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
createEventObjects(event);
}
}
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
});
@@ -267,7 +272,7 @@ int MessageEventModel::timelineBaseIndex() const
return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0;
}
void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles)
void MessageEventModel::refreshEventRoles(int row, const QList<int> &roles)
{
const auto idx = index(row);
Q_EMIT dataChanged(idx, idx, roles);
@@ -311,7 +316,7 @@ void MessageEventModel::moveReadMarker(const QString &toEventId)
endMoveRows();
}
int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &roles)
int MessageEventModel::refreshEventRoles(const QString &id, const QList<int> &roles)
{
// On 64-bit platforms, difference_type for std containers is long long
// but Qt uses int throughout its interfaces; hence casting to int below.
@@ -587,6 +592,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getReplyMediaInfo();
}
if (role == IsThreadedRole) {
return eventHandler.isThreaded();
}
if (role == ThreadRootRole) {
return eventHandler.threadRoot();
}
if (role == ShowAuthorRole) {
for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r);
@@ -659,10 +672,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_reactionModels.contains(evt.id());
}
if (role == AuthorIdRole) {
return evt.senderId();
}
if (role == VerifiedRole) {
if (evt.originalEvent()) {
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
@@ -699,10 +708,6 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return;
}
auto eventId = event->id();
EventHandler eventHandler;

View File

@@ -63,6 +63,9 @@ public:
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
IsThreadedRole,
ThreadRootRole,
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -73,8 +76,6 @@ public:
ReactionRole, /**< List model for this event. */
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
AuthorIdRole, /**< Matrix ID of the message author. */
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
IsRedactedRole, /**< Whether an event has been deleted. */
@@ -139,8 +140,8 @@ private:
void fetchMore(const QModelIndex &parent) override;
void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QVector<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void refreshEventRoles(int row, const QList<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
void moveReadMarker(const QString &toEventId);
void createEventObjects(const Quotient::RoomMessageEvent *event);

View File

@@ -124,6 +124,20 @@ void PublicRoomListModel::setKeyword(const QString &value)
Q_EMIT hasMoreChanged();
}
bool PublicRoomListModel::showOnlySpaces() const
{
return m_showOnlySpaces;
}
void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
{
if (showOnlySpaces == m_showOnlySpaces) {
return;
}
m_showOnlySpaces = showOnlySpaces;
Q_EMIT showOnlySpacesChanged();
}
void PublicRoomListModel::next(int count)
{
if (count < 1) {
@@ -136,7 +150,11 @@ void PublicRoomListModel::next(int count)
return;
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, {}});
QStringList roomTypes;
if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space");
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes});
Q_EMIT loadingChanged();
connect(job, &BaseJob::finished, this, [this] {

View File

@@ -45,6 +45,11 @@ class PublicRoomListModel : public QAbstractListModel
*/
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged)
/**
* @brief Whether only space rooms should be shown.
*/
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/**
* @brief Whether the model has more items to load.
*/
@@ -103,6 +108,9 @@ public:
[[nodiscard]] QString keyword() const;
void setKeyword(const QString &value);
[[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool hasMore() const;
[[nodiscard]] bool loading() const;
@@ -118,12 +126,13 @@ private:
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
bool attempted = false;
bool m_loading = false;
QString nextBatch;
QVector<Quotient::PublicRoomsChunk> rooms;
QList<Quotient::PublicRoomsChunk> rooms;
Quotient::QueryPublicRoomsJob *job = nullptr;
@@ -131,6 +140,7 @@ Q_SIGNALS:
void connectionChanged();
void serverChanged();
void keywordChanged();
void showOnlySpacesChanged();
void hasMoreChanged();
void loadingChanged();
};

View File

@@ -66,19 +66,6 @@ PushRuleModel::PushRuleModel(QObject *parent)
: QAbstractListModel(parent)
{
m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
if (Controller::instance().activeConnection()) {
controllerConnectionChanged();
}
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, &PushRuleModel::controllerConnectionChanged);
}
void PushRuleModel::controllerConnectionChanged()
{
if (Controller::instance().activeConnection()) {
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(QStringLiteral("m.push_rules"));
}
}
void PushRuleModel::updateNotificationRules(const QString &type)
@@ -87,7 +74,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
return;
}
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson(QStringLiteral("m.push_rules"));
const QJsonObject ruleDataJson = m_connection->accountDataJson(QStringLiteral("m.push_rules"));
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson[QStringLiteral("global")].toObject());
beginResetModel();
@@ -106,7 +93,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
endResetModel();
}
void PushRuleModel::setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind)
{
for (const auto &rule : rules) {
QString roomId;
@@ -156,8 +143,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
*
* Rooms that the user hasn't joined shouldn't have a rule.
*/
auto connection = Controller::instance().activeConnection();
if (connection->room(ruleId) != nullptr) {
if (m_connection->room(ruleId) != nullptr) {
return PushNotificationSection::Undefined;
}
/**
@@ -171,7 +157,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
if (!testUserId.startsWith(u'@')) {
testUserId.prepend(u'@');
}
if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && connection->user(testUserId) != nullptr) {
if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
return PushNotificationSection::Undefined;
}
// If the rule has push conditions and one is a room ID it is a room only keyword.
@@ -307,8 +293,8 @@ void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction:
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
{
PushNotificationKind::Kind kind = PushNotificationKind::Content;
const QVector<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QVector<Quotient::PushCondition> pushConditions;
const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QList<Quotient::PushCondition> pushConditions;
if (!roomId.isEmpty()) {
kind = PushNotificationKind::Override;
@@ -325,14 +311,14 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
pushConditions.append(keywordCondition);
}
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind),
keyword,
actions,
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QString());
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind),
keyword,
actions,
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QString());
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString();
});
@@ -351,7 +337,7 @@ void PushRuleModel::removeKeyword(const QString &keyword)
}
auto kind = PushNotificationKind::kindString(m_rules[index].kind);
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
});
@@ -359,27 +345,27 @@ void PushRuleModel::removeKeyword(const QString &keyword)
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() {
auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
m_connection->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
}
});
}
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QVector<QVariant> actions;
QList<QVariant> actions;
if (ruleId == QStringLiteral(".m.rule.call")) {
actions = actionToVariant(action, QStringLiteral("ring"));
} else {
actions = actionToVariant(action);
}
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
}
PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVariant> &actions, bool enabled)
PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
{
bool notify = false;
bool isNoisy = false;
@@ -422,16 +408,16 @@ PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVar
}
}
QVector<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) {
Q_ASSERT(false);
return QVector<QVariant>();
return QList<QVariant>();
}
QVector<QVariant> actions;
QList<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append(QStringLiteral("notify"));
@@ -453,4 +439,23 @@ QVector<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action
return actions;
}
NeoChatConnection *PushRuleModel::connection() const
{
return m_connection;
}
void PushRuleModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
if (m_connection) {
connect(m_connection, &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(QStringLiteral("m.push_rules"));
}
}
#include "moc_pushrulemodel.cpp"

View File

@@ -157,6 +157,8 @@ class PushRuleModel : public QAbstractListModel
*/
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
struct Rule {
QString id;
@@ -225,27 +227,31 @@ public:
*/
Q_INVOKABLE void removeKeyword(const QString &keyword);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void defaultStateChanged();
void globalNotificationsEnabledChanged();
void globalNotificationsSetChanged();
void connectionChanged();
private Q_SLOTS:
void controllerConnectionChanged();
void updateNotificationRules(const QString &type);
private:
PushNotificationAction::Action m_defaultKeywordAction;
QList<Rule> m_rules;
NeoChatConnection *m_connection;
void setRules(QVector<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind);
int getRuleIndex(const QString &ruleId) const;
PushNotificationSection::Section getSection(Quotient::PushRule rule);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
PushNotificationAction::Action variantToAction(const QVector<QVariant> &actions, bool enabled);
QVector<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
PushNotificationAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
QList<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
};
Q_DECLARE_METATYPE(PushRuleModel *)

View File

@@ -4,6 +4,12 @@
#include "reactionmodel.h"
#include <QDebug>
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <KLocalizedString>
@@ -29,11 +35,38 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
const auto &reaction = m_reactions.at(index.row());
if (role == TextRole) {
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
const auto reactionText = isEmoji(reaction.reaction)
? QStringLiteral("<span style=\"font-family: 'emoji';\">") + reaction.reaction + QStringLiteral("</span>")
: reaction.reaction;
if (role == TextContentRole) {
if (reaction.authors.count() > 1) {
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
return QStringLiteral("%1 %2").arg(reactionText, QString::number(reaction.authors.count()));
} else {
return reaction.reaction;
return reactionText;
}
}
@@ -64,7 +97,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
"%2 reacted with %3",
reaction.authors.count(),
text,
reaction.reaction);
reactionText);
return text;
}
@@ -101,7 +134,7 @@ void ReactionModel::setReactions(QList<Reaction> reactions)
QHash<int, QByteArray> ReactionModel::roleNames() const
{
return {
{TextRole, "text"},
{TextContentRole, "textContent"},
{ReactionRole, "reaction"},
{ToolTipRole, "toolTip"},
{AuthorsRole, "authors"},

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