Compare commits

...

255 Commits

Author SHA1 Message Date
James Graham
3d738549ae Remove giant emoji tones variable 2024-10-20 11:07:46 +01:00
l10n daemon script
36469c6097 GIT_SILENT Sync po/docbooks with svn 2024-10-20 01:30:26 +00:00
l10n daemon script
0cd9c6a434 GIT_SILENT Sync po/docbooks with svn 2024-10-19 01:31:03 +00:00
l10n daemon script
e285a94273 GIT_SILENT Sync po/docbooks with svn 2024-10-17 01:30:25 +00:00
James Graham
a2ec6d97b1 Update for the latest event content changes to libquotient 2024-10-16 17:32:55 +00:00
l10n daemon script
b42fd3fc51 GIT_SILENT Sync po/docbooks with svn 2024-10-16 01:32:19 +00:00
Tobias Fella
12971bb08b Minor cleanup 2024-10-15 22:01:32 +02:00
James Graham
79deda4f2d Make sure that content is kept alive so fileInfo can be used throughout the function 2024-10-15 16:30:57 +00:00
James Graham
365af2cd6c Mobile Pages
Currently the page experienc on mobile is suboptimal as back gestures do not work and the startup behaviour is not ideal.

This reworks it so that pages are now pushed as a layer on mobile and at startup only a saved space is restored. It is also setup so that on mobile you'll never see a blank room page (like when you select friends or home).
2024-10-15 16:13:02 +00:00
l10n daemon script
cc50e76c0d GIT_SILENT Sync po/docbooks with svn 2024-10-15 01:28:56 +00:00
James Graham
16df22af68 Update for the API changes to RoomMessageEvent in libquotient 0.9 2024-10-14 21:59:56 +01:00
l10n daemon script
53a957fa15 GIT_SILENT Sync po/docbooks with svn 2024-10-14 01:27:49 +00:00
James Graham
69571489fa Mobile DelegateContextMenu
Make sure that the mobile DelegateContextMenu is getting the author data correctly
2024-10-13 14:47:10 +00:00
James Graham
3a66b4d67e Fix mobile ExploreComponent
Make sure that the search filter is removed when another button is pressed.
Make sure that the popup closes when one of the other menus is open.
Make the separator is at the top on NavigationTabBar
2024-10-13 12:41:56 +00:00
l10n daemon script
ba8d2b1281 GIT_SILENT Sync po/docbooks with svn 2024-10-12 01:29:09 +00:00
Tobias Fella
11185c127d Escape display name in verification event 2024-10-11 22:46:38 +02:00
Tobias Fella
0ab3bfd4f3 Fix crash when opening invitation
There seem to be problems with the model not updating correctly when the room is set. This fix is a bit dirty, but seems to work well enough

BUG: 493197
2024-10-11 22:15:38 +02:00
Tobias Fella
b5fdb3d0db Fix transitioning from invitation page to normal room page 2024-10-11 21:11:17 +02:00
Tobias Fella
9ef342e448 Fix emoji detection
The previous code considered "123" to be an emoji
2024-10-11 21:00:51 +02:00
Tobias Fella
0bda65b5ac Make sure that we don't immediately open all new rooms
Some confusing interaction with the code removed here causes us to open all rooms that come in.
We generally don't want that. I also don't understand why we're connecting for new rooms here - the room is already available in this case.
2024-10-11 20:44:15 +02:00
James Graham
005b7a760c Request Notification Permission
If notification permission has not been granted and permission has not previously been asked for request notification permission.
2024-10-11 16:28:59 +00:00
Tobias Fella
ab3c40a709 Don't double-escape subject 2024-10-11 16:12:00 +02:00
Tobias Fella
a809b7f11e Fix avatar size for some uncommon cases 2024-10-11 15:07:46 +02:00
l10n daemon script
702c7d49a8 GIT_SILENT Sync po/docbooks with svn 2024-10-11 01:34:35 +00:00
Tobias Fella
a2afaf40cd Remove some leftover debug logging 2024-10-10 22:06:19 +02:00
Joshua Goins
eb900a5c2c Fix access of notifications manager in UnifiedPush codepath 2024-10-08 07:56:11 -04:00
l10n daemon script
14eadb3b92 GIT_SILENT Sync po/docbooks with svn 2024-10-08 01:29:52 +00:00
l10n daemon script
a5d84bb266 GIT_SILENT made messages (after extraction) 2024-10-08 00:39:44 +00:00
Oliver Beard
5a03ce4e95 qml/RoomDrawer: Add & fix separators
A separator has been added to the drawer view's left side, and the position of the NavigationTabBar has been set so it correctly draws the separator at the top and not the bottom.
2024-10-07 18:43:40 +00:00
l10n daemon script
607db82db0 GIT_SILENT Sync po/docbooks with svn 2024-10-07 01:28:24 +00:00
Heiko Becker
56b302d4c8 GIT_SILENT Update Appstream for new release
(cherry picked from commit 765292e0e9)
2024-10-06 22:02:31 +02:00
James Graham
1237e9d4bd NotificationManager rework
Rework notifications manager to no longer be a singleton, but a component of controller.

The dependency on it for neochat room and connection is also removed.
2024-10-06 17:14:18 +00:00
Carl Schwan
71468e453c RoomGeneralPage: Move avatar setting outside of FormCard 2024-10-06 09:38:37 +00:00
Nicolas Fella
d0a915e81c Add missing QML import name 2024-10-06 09:38:19 +00:00
l10n daemon script
e7751f40fa GIT_SILENT Sync po/docbooks with svn 2024-10-06 01:28:31 +00:00
l10n daemon script
498240c6ec 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"
2024-10-06 01:22:10 +00:00
l10n daemon script
d448cf4c7e GIT_SILENT made messages (after extraction) 2024-10-06 00:39:44 +00:00
James Graham
d7d9f254ff Remove the dependence on RoomManager from NeochatConnection.
At the same time use the roomCreated signal from connection for auto resolving as it's specifically designed for user created rooms.
2024-10-05 18:39:21 +01:00
James Graham
aedba5c650 Strip single <p> tags inside TextHandler rather than actions handler 2024-10-05 17:05:36 +00:00
James Graham
a4e9794b13 MessageComponent
It feels weird to have anything that needs MessageComponent have to depend on all of MessageContentModel and pull in it's dependencies. This moves MessageComponent into its own header.
2024-10-05 15:51:19 +00:00
James Graham
4bd4bd6f22 Rework ActionsHandler
Rework ActionsHandler as static helper functions.

The functions are now invoked from ChatBarCache so there is no need to pass an actions handler object around qml simplifying the code.
2024-10-05 13:44:53 +00:00
l10n daemon script
ac9bfbff78 GIT_SILENT Sync po/docbooks with svn 2024-10-05 01:29:07 +00:00
James Graham
e26392ce94 requestDirectChat
Use Quotient::connection::requestDirectChat directly as it can already handle all the conditions.
2024-10-04 16:52:05 +00:00
l10n daemon script
a314e56425 GIT_SILENT Sync po/docbooks with svn 2024-10-04 01:30:17 +00:00
James Graham
4af27f7609 Remove unneeded includes 2024-10-03 23:22:48 +01:00
James Graham
773017c881 errorOccured
Have controller link to neochatconnection for errorOccured rather than call directly to remove dependency on controller.

For all the same reasons as network/neochat!1926
2024-10-03 20:32:30 +00:00
James Graham
153cbeae8a ShowMessage
Move showMessage to RoomManager and merge warning in. A new Message type enum is created aligned with the Kirgami.MessageType used by Kirigami.Banner to avoid needing to translate from 2 enums. 

showMessage is also sent as a signal from NeoChatRoom (and via the room from ActionsModel), this removes the need for them to have a dependency on Controller (and RoomManager). While not necessarily the cause of Windows crashes the spaghetti dependencies of RoomManager and Controller throughout the code base has made debugging that harder so this aims to simplify that as well.
2024-10-03 18:42:29 +00:00
l10n daemon script
777ea9fbe0 GIT_SILENT Sync po/docbooks with svn 2024-10-03 01:28:58 +00:00
James Graham
d22cc7f40a Use Knotificationpermission 2024-10-02 21:07:58 +00:00
Carl Schwan
c28ca9087c RemoveChildDialog: Don't wrap delegates in FormCard 2024-10-02 17:12:36 +00:00
l10n daemon script
857412d9cc GIT_SILENT Sync po/docbooks with svn 2024-10-02 01:29:32 +00:00
James Graham
4b132460f6 Remove unused include utils.h in neochatroom 2024-10-01 19:31:52 +01:00
James Graham
644f5c0ce1 Permission Manager
Add permission manager from Itinerary so that Android permissions can be checked.

Note at the moment the request permission functions are not hooked up so on Android the permission will need to be manually set on. I'll hook this up later but I wanted to confirm my suspicion on notifications being the current cause of crashes.
2024-10-01 17:29:39 +00:00
l10n daemon script
45439e17c9 GIT_SILENT Sync po/docbooks with svn 2024-10-01 01:29:28 +00:00
Carl Schwan
485e0f0510 Make hover actions position logic more robust 2024-09-30 20:30:36 +00:00
Carl Schwan
2969d20a92 Put hover actions in the RowLayouts
Simplify the code compared to use an ActionToolBar and this should makes
it more reliable.
2024-09-30 20:30:36 +00:00
Soumyadeep Ghosh
7cf97a5c9b snap: update to latest tag 2024-09-28 12:59:32 +05:30
l10n daemon script
f2894ed774 GIT_SILENT Sync po/docbooks with svn 2024-09-27 01:31:05 +00:00
Tobias Fella
3e1044b8fd Adapt to libQuotient changes 2024-09-26 19:55:41 +02:00
l10n daemon script
0c6480c5e0 GIT_SILENT Sync po/docbooks with svn 2024-09-26 01:29:11 +00:00
Luc Schrijvers
232c45979a Disable KRunner, X11, and DBus on Haiku 2024-09-25 13:31:00 +00:00
James Graham
848031c315 Add the notifcation permision for modern android versions 2024-09-25 05:22:26 +00:00
l10n daemon script
bf2389c31c GIT_SILENT Sync po/docbooks with svn 2024-09-25 01:30:30 +00:00
James Graham
d87bba7993 Revert "snap: update for latest tag"
This reverts commit 6e81701d4b
2024-09-24 21:57:18 +00:00
Carl Schwan
9b837a8656 SelectParentDialog: Fix selecting room 2024-09-24 21:08:34 +00:00
Carl Schwan
90061caec3 Fix undefined StyledText error
StyledText is an enum for Text not TextEdit
2024-09-24 19:30:27 +00:00
Soumyadeep Ghosh
6e81701d4b snap: update for latest tag 2024-09-24 17:59:10 +05:30
l10n daemon script
2164c4c3f8 GIT_SILENT Sync po/docbooks with svn 2024-09-24 01:36:38 +00:00
Nate Graham
a0665187c6 Use common key for saving window state data
We typically use "MainWindow" here, not "Main".
2024-09-23 14:37:32 -06:00
l10n daemon script
77e933f620 GIT_SILENT Sync po/docbooks with svn 2024-09-23 01:27:36 +00:00
l10n daemon script
deed9cf6d7 GIT_SILENT Sync po/docbooks with svn 2024-09-22 01:31:14 +00:00
Carl Schwan
fad4e506bc Fix display name in verification message
Previously this would be undefined and i18n doesn't handle this very
well
2024-09-21 13:26:03 +00:00
Carl Schwan
9d6b940b78 Port away from Qt.Plaform.labs
This bring a QtWidget dependency on Android
2024-09-21 12:12:14 +00:00
Carl Schwan
cfb663d399 RoomGeneralPage: Split general info in two card
So that the save button isn't floating in the middle of the card
2024-09-21 12:11:50 +00:00
Carl Schwan
4620c176b6 AppearanceSettingsPage: Hide separator for transparancy setting on Android 2024-09-21 12:11:39 +00:00
Carl Schwan
8c927a59d9 NetworkProxyPage: Add separator between form delegates 2024-09-21 12:11:27 +00:00
Carl Schwan
2fe9bc9846 Fix arrow icons on Android 2024-09-21 12:11:14 +00:00
Carl Schwan
b9615eadb3 Use FormTextAreaDelegate 2024-09-21 12:11:04 +00:00
Volker Krause
e4e5d14d6e Use released KQuickImageEditor 2024-09-21 09:46:33 +00:00
Carl Schwan
75eddd2a6f amdroid: Add missing icon for preferences 2024-09-21 11:45:38 +02:00
l10n daemon script
634407a22d GIT_SILENT Sync po/docbooks with svn 2024-09-21 01:31:02 +00:00
Carl Schwan
42dd2e5413 Ensure floating buttons have correct size on mobile 2024-09-20 19:52:58 +00:00
Carl Schwan
2a6e3c0add Remove global menu support from Android
This pull out QtWidgets which we don't need
2024-09-20 12:25:19 +00:00
Volker Krause
abb81e0d8e Update craft.ini
KF 6.6 for KColorScheme is sufficient meanwhile, but we need a newer
KQuickImageEditor to properly load with Qt6.
2024-09-20 10:49:33 +00:00
l10n daemon script
e9a4b43331 GIT_SILENT Sync po/docbooks with svn 2024-09-20 01:35:20 +00:00
Tobias Fella
08b2c39a61 Make ChooseRoomDialog a SearchPage
Makes it look a bit nicer and more standardised
2024-09-19 19:51:32 +02:00
James Graham
f89cec9c55 Make sure that m_components cannot be accessed out of bounds in closeLinkPreview
https://crash-reports.kde.org/organizations/kde/issues/67352/?project=18&query=is%3Aunresolved+issue.priority%3A%5Bhigh%2C+medium%5D&referrer=issue-stream&statsPeriod=14d&stream_index=5
2024-09-19 17:18:25 +00:00
James Graham
4c49ca2a51 NeochatRoomMember ID fallback
Make sure that when the returned RoomMember in NeochatRoomMember is empty that displayname and similar functions return the member Matrix ID

BUG: 491025
2024-09-19 15:43:02 +00:00
Tobias Fella
52ab6f484b Fix crash in MessageContentModel 2024-09-19 16:45:05 +02:00
Tobias Fella
2a5359e73b Fix NeoChatRoom::directChatRemoteMember
- Don't access member that's not there
- Use NeoChatRoomMember instead of RoomMember

BUG: 492733
2024-09-19 16:04:27 +02:00
l10n daemon script
977ea73237 GIT_SILENT Sync po/docbooks with svn 2024-09-19 01:29:31 +00:00
Tobias Fella
a769b904dc Fix compilation 2024-09-18 14:36:12 +02:00
l10n daemon script
5def5124ef GIT_SILENT Sync po/docbooks with svn 2024-09-18 01:32:34 +00:00
l10n daemon script
40c3cc7f9e GIT_SILENT Sync po/docbooks with svn 2024-09-17 01:31:27 +00:00
l10n daemon script
17c0906044 GIT_SILENT Sync po/docbooks with svn 2024-09-16 01:42:20 +00:00
James Graham
38cfc915f1 Use autoTransform on images
closes network/neochat#674
2024-09-15 10:21:54 +00:00
James Graham
1821d9fc04 Move reply pane to use MessageContentModel
Move reply pane to use MessageContentModel. This means the reply pane component is no longer required.

This commit also limits the size of code and image componets in a reply to keep them from getting too huge.
2024-09-15 08:39:22 +00:00
James Graham
ec6a8dd028 Only save eventId in MessageContentModel
Turns out trying to manage pointers in the model is a bad idea so only save eventId in MessageContentModel, events pointers will now only be obtained temporarily then discarded to avoid both creating additional copies of the event in the model and potential sources of crashes.

This also creates a basic unit test that we can add to going forward.
2024-09-15 08:28:46 +00:00
l10n daemon script
e0c3b7f808 GIT_SILENT Sync po/docbooks with svn 2024-09-15 01:32:10 +00:00
l10n daemon script
155dc4919e 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"
2024-09-15 01:23:53 +00:00
l10n daemon script
a38c53b2be GIT_SILENT Sync po/docbooks with svn 2024-09-14 01:34:50 +00:00
James Graham
67dfc7b32e Fix Eventhandler strings for translation
Change the generic representations of events in event handler to always have a full string to aid translation.

The aggregated list is then converted to be a simple list of single event generic descriptions to avoid string puzzles.

Fixes network/neochat#638

BUG: 466201, BUG: 491024
2024-09-13 17:11:50 +00:00
Tobias Fella
f156551d4f Fix compilation against changes in libQuotient 2024-09-13 15:43:10 +02:00
Tobias Fella
5c04eb85af Fix filtering users in the member list 2024-09-13 14:36:27 +02:00
l10n daemon script
1efa27177a GIT_SILENT Sync po/docbooks with svn 2024-09-13 01:30:57 +00:00
Bart Ribbers
17da652152 Don't open a room by default on mobile
Since the room window is fullscreen on mobile and you can't see the room list,
the first thing you'll be doing is backing out so you can choose the actual room you want to see
2024-09-12 15:17:38 +00:00
Tobias Fella
4ff866ea29 use kcolorscheme master 2024-09-12 13:58:46 +02:00
Tobias Fella
3c9c7abe35 Try fixing android 2024-09-12 13:58:43 +02:00
l10n daemon script
157017126a GIT_SILENT Sync po/docbooks with svn 2024-09-12 01:26:57 +00:00
l10n daemon script
928e4ae5ed 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"
2024-09-12 01:20:59 +00:00
l10n daemon script
2bc790f4cb GIT_SILENT made messages (after extraction) 2024-09-12 00:39:36 +00:00
l10n daemon script
a3148b264c GIT_SILENT Sync po/docbooks with svn 2024-09-11 01:27:19 +00:00
Carl Schwan
16b27700f5 Unify redaction handling
The code in messageeventmodel was calling to EventHandler::getBody who
already handles reaction handlings
2024-09-10 12:08:00 +00:00
Carl Schwan
9ee10b6968 Escape html from redaction message 2024-09-10 12:08:00 +00:00
l10n daemon script
4d0db0b5c2 GIT_SILENT Sync po/docbooks with svn 2024-09-10 01:26:40 +00:00
l10n daemon script
6e49aaf17b GIT_SILENT Sync po/docbooks with svn 2024-09-09 01:28:45 +00:00
Joshua Goins
1092d75f2e Remove vestigial references to window geometry
These weren't removed in d165cd955d
because I forgot.
2024-09-08 07:32:00 +00:00
Joshua Goins
8059c3797d Remove unused lambda capture variables
This removes two compile time warnings.
2024-09-08 07:24:24 +00:00
Joshua Goins
1302d62ad9 SpaceDrawer: Remove seemingly non-existent signal call 2024-09-08 07:23:58 +00:00
Joshua Goins
8eaae4034d Shorten the error message passive notification
The default timeout is a bit long, "short" is 3 seconds shorter than the
default. For  long-term network errors, we have a banner telling you so
anyway. This should hopefully reduce the notification spam when you have
temporary network dropouts.
2024-09-08 07:23:30 +00:00
l10n daemon script
354e3414a1 GIT_SILENT Sync po/docbooks with svn 2024-09-08 01:28:05 +00:00
James Graham
fc24beae6d Use im-kick-user consistently for logout.
I went with im-kick-user as it's fully red at all sizes.

BUG: 491355
2024-09-07 12:41:26 +00:00
Claire Elford
923cc67b55 Add test for missing character before a Matrix ID 2024-09-07 10:19:12 +00:00
Claire Elford
30e24069bc Fix missing character before a Matrix ID
When you send messages like "a @blankeclair:catgirl.cloud b" or
"]#rainversewiki:catgirl.cloud", they would be rendered like
"a@blankeclair:catgirl.cloud b" and "#rainversewiki:catgirl.cloud"
respectively. This commit fixes that by not matching the character before the
MXID in the regex.
2024-09-07 10:19:12 +00:00
Claire Elford
f7533a454c Fix increasing font size of certain emojis
Before this commit, NeoChat has two methods of detecting whether or not a piece
of text was an emoji. One is through a regex, and the other is by using the ICU
library. The two methods are used in different parts of the code.

This commit removes the regex detector and instead uses ICU for all the places
where NeoChat needs to figure out whether or not a string is an emoji. This
fixes increasing the font size for messages that only consist of emoji when
certain emoji are used that the regex did not handle (such as the transgender
symbol and transgender flag emojis).
2024-09-07 09:49:27 +00:00
Claire Elford
ab4e1a86dc Fix parsing self-closing tags with no space (such as <br/>)
If there was no space between the tag name and the slash of a self-closing tag,
the code assumes that the tag name is "br/". This commit adds the slash as a
character to close a tag on, so that "<br/>" is treated as a self-closing "br".

BUG: 487377
2024-09-07 12:50:18 +10:00
l10n daemon script
d28c2ed113 GIT_SILENT Sync po/docbooks with svn 2024-09-07 01:27:17 +00:00
Heiko Becker
3a467328f5 GIT_SILENT Update Appstream for new release
(cherry picked from commit b6dac3bbdf)
2024-09-07 00:48:52 +02:00
Tobias Fella
979d83cb01 Remove calls from tests 2024-09-06 08:21:06 +00:00
Joshua Goins
d165cd955d Use the new KConfig WindowStateSaver
This removes some NeoChat-specific code we have for saving/restoring the
window.
2024-09-06 08:21:06 +00:00
l10n daemon script
6eb770343e GIT_SILENT Sync po/docbooks with svn 2024-09-06 01:34:07 +00:00
James Graham
54be52b855 Fix default permissions settings
Make sure that if default permissions or basic permissons are not present in the power level event that they are set properly when changed rather than in the event section.

Also define some of the commonly used strings

BUG: 491371
2024-09-05 13:48:42 +00:00
Tobias Fella
d201333409 Don't consider events that change membership to be renames
Otherwise, the "Show rename events" flag affects the visibility of events where we don't expect it
2024-09-05 13:27:52 +02:00
Tobias Fella
3db8b4cd17 Ask for a reason when kicking a user 2024-09-05 13:22:23 +02:00
l10n daemon script
0e246a00bc GIT_SILENT Sync po/docbooks with svn 2024-09-05 01:26:38 +00:00
l10n daemon script
e638fa8929 GIT_SILENT Sync po/docbooks with svn 2024-09-04 01:26:36 +00:00
l10n daemon script
cdc982ad91 GIT_SILENT Sync po/docbooks with svn 2024-09-03 01:26:14 +00:00
Joshua Goins
bff69e21ad Add icons for the menus in the message context menu
In Qt6 we can now assign icons to menus, so let's add what's missing.
2024-09-02 17:28:03 +00:00
Joshua Goins
e117cc0cfb Move the "Show User" action to DelegateContextMenu, add it for media
This was missing from the media context menu, and should be added. Since
it's shared between messages and files, it's now a common action.
2024-09-02 17:28:03 +00:00
Joshua Goins
3af1a88e05 Don't show the "Web Search" action in the media context menu
All this does is try to web search for the filename of the image, which
is useless. Let's hide it entirely in this case.
2024-09-02 17:28:03 +00:00
Joshua Goins
aa116a35f5 Web Shortcuts: kcmshell5 does not exist anymore on Plasma 6
This is hardcoded, but it's probably a safe assumption to think most
people running modern NeoChat are using Plasma 6 anyway.
2024-09-02 17:15:37 +00:00
Joshua Goins
ac232d7f55 Change the "configure room" button icon to something more fitting
This button doesn't actually configure anything, you can do plenty of
actions like "mark as read" and such. Since the button isn't solely for
configuration, we should use an overflow menu icon instead.
2024-09-02 17:02:36 +00:00
Joshua Goins
928911e33c Settings: Overhaul the Security page
This improves the organization of this page, which is starting to become
a bit of a mess. The "Hide images and video events" option is moved
here, and the page is rebranded accordingly for "Security & Safety".
Unnecessary headings are removed, and the ignored users button is moved
to the top of the page.

Explanations for the import/keys buttons are added. The key display
is removed as it's not useful for the user (because they don't know what
to do with it) nor developers (because you can't copy it.) We can add
it back somewhere else.

This has the added benefit of making the whole page fit in the default
settings window size too.
2024-09-02 16:13:12 +00:00
Joshua Goins
e3c30f5bb3 Settings: Hide "minimize to system tray" when system tray is disabled
We do this for the state events setting, so let's do it here too.
2024-09-02 16:01:51 +00:00
Tobias Fella
4b51855528 Add name for Avatar in RecommendedSpaceDialog 2024-09-02 17:26:14 +02:00
Tobias Fella
79c27db0a9 Improve InviteUserPage 2024-09-02 16:33:45 +02:00
Tobias Fella
78e42ab352 Make it possible to invite users that were previously in the room by command 2024-09-02 15:54:40 +02:00
Tobias Fella
93909c45ee Refactor dialogs for reporting, banning, and removing messages 2024-09-02 15:18:32 +02:00
Joshua Goins
eda0bf4b23 LocationChooser: Add a "Locate" button to locate yourself
The map centers on London by default, but for the other people living
outside it may find it hard to figure out where they are. This adds a
button that calls into QtPositioning to center the map over where you
are.
2024-09-02 13:09:21 +00:00
Joshua Goins
4f4b10e0b6 LocationChooser: Add a "Re-center" action to find the pinned location
It's somewhat easy to scroll or pan away from the pin you placed, so
let's add an action to re-center the map if you want to find it again.
2024-09-02 13:09:21 +00:00
Joshua Goins
2b374a8bec Add an icon when you only have invites but no direct messages
There's an edge case with the friends icon, where it will display a
blank circle if you only have pending invites but no actual direct
messages. Now an icon is added to make it clear there is pending invites
and it's not a visual bug.
2024-09-02 12:40:54 +00:00
Tobias Fella
7821de4a8d Don't end kick messages without a reason with a ":"
BUG: 492512
2024-09-02 14:39:17 +02:00
Tobias Fella
86b88c851f Close AccountSwitchDialog when logging in a new connection
Otherwise, the dialog is still opened after login has finished
2024-09-02 14:38:33 +02:00
Tobias Fella
1157882f1b Don't register ThreadChatBarModel as a QML_ELEMENT
It's not needed and doesn't work
2024-09-02 13:36:32 +02:00
Tobias Fella
594d1373c9 Cleanup 2024-09-02 13:30:15 +02:00
Tobias Fella
1e6948bbc7 Fix crash during logout 2024-09-02 12:49:38 +02:00
Tobias Fella
41845b97d5 Fix crash on logout 2024-09-02 12:45:11 +02:00
Tobias Fella
bb56f74622 Fix initials for avatar in UserInfo 2024-09-02 10:34:39 +02:00
l10n daemon script
a98f6ac331 GIT_SILENT Sync po/docbooks with svn 2024-09-02 01:26:52 +00:00
Joshua Goins
15d6287995 LocationHelper: Move clamp from zoomToFit to QML
The OSM plugin has a different zoom tolerance than what we're hardcoding
here. This fixes the map looking funky from being too zoomed while
trying to fit multiple location points at once.
2024-09-01 18:48:20 +00:00
Joshua Goins
0242ab72e8 Allow opening locations in your preferred map
Once someone shares a location with you, typically you want to open it
in your preferred mapping application or website. For example, being
sent a location to a restaurant and needing to route it via Google Maps.

Now in NeoChat you can click on the "Open Externally" button on a
location message. On KDE Plasma, the default application can be set
under System Settings. On Android, this URI is handled by Google Maps
and possibly others.
2024-09-01 18:23:49 +00:00
Joshua Goins
4b7cbf37d5 AuthorDelegate: Don't make empty space clickable 2024-09-01 15:55:51 +00:00
Joshua Goins
0ccfe7d991 AuthorDelegate: Add pointing hand cursor to indicate you can tap
It's not immediately obvious that you can press on this static text to
bring up the user's details. This isn't a problem with the avatar - for
example - because it has a pointing hand cursor. Let's do the same here.
2024-09-01 15:55:51 +00:00
Joshua Goins
ab5585cd06 Make the SectionDelegate look just a little bit better
This has insets on it - probably from qqc2-desktop-style - and makes it
look extremely bad when scrolling through messages as the background
size doesn't match. Now the insets are set to zero, except for topInset.
This is done to work around a visual bug where you can see a one-pixel
line right above the SectionDelegate when scrolling.
2024-09-01 11:42:17 -04:00
Joshua Goins
b25a0a7a4e Settings: Hide certain pages when we have no connection
This hides the "Notifications", "Security", "Accounts" and "Devices"
page from the settings if we have no connection. This can now happen
since the user is able to enter the full settings without being logged
in from the welcome page.
2024-09-01 14:21:14 +00:00
Joshua Goins
f574c12adc WelcomePage: Redesign to center the contents, other misc improvements
We now remove the header from the page, and replace it with a separator
(it still lives with an InlineMessage for error handling.) The contents
of this page are now centered, and the maximum width of the buttons are
reduced.

Along with that are two smaller misc improvements. One is that the
duplicate separator underneath "Register" is now gone. Another is that
the full settings page may now be opened from here, allowing users to
access more than proxy settings.
2024-09-01 14:21:14 +00:00
James Graham
05a2f03c18 Make use of the proposed new KColorScheme API
Make use of new API proposed in frameworks/kcolorscheme!12
2024-09-01 14:17:41 +00:00
l10n daemon script
cbc2e65856 GIT_SILENT Sync po/docbooks with svn 2024-09-01 01:26:36 +00:00
l10n daemon script
1df80a7d2a GIT_SILENT Sync po/docbooks with svn 2024-08-31 01:27:27 +00:00
l10n daemon script
050a014df7 GIT_SILENT Sync po/docbooks with svn 2024-08-30 01:35:35 +00:00
l10n daemon script
4775ae09b0 GIT_SILENT Sync po/docbooks with svn 2024-08-29 01:27:27 +00:00
l10n daemon script
8d4c3bb4fc GIT_SILENT Sync po/docbooks with svn 2024-08-28 01:38:01 +00:00
l10n daemon script
439260ff03 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"
2024-08-28 01:25:46 +00:00
James Graham
5b04ad6805 Minor fixes for MessageContentModel and ThreadModel 2024-08-27 15:58:27 +01:00
James Graham
6d77ed1e0e Create an LRU cache for linkpreviewers
Create an LRU cache for linkpreviewers to stop the storage growing continuously as new links are made.
2024-08-27 08:15:03 +00:00
l10n daemon script
cc7f783b50 GIT_SILENT Sync po/docbooks with svn 2024-08-27 01:29:27 +00:00
James Graham
1d7ed1983b Use the ChatBar Component for new thread messages
![image](/uploads/a24dea919e7f165d602991659f517c22/image.png){width=148 height=210}

Note: there is still an issue where after starting a new thread the threaded messages only appear after a restart as the root event needs re-downloading from the server to get the thread info added. My plan is to tackle this next.
2024-08-26 19:13:19 +00:00
Tobias Fella
c0151353c5 Add an option to hide images and videos by default
Implements #658
2024-08-26 17:42:29 +02:00
l10n daemon script
72994248fa GIT_SILENT Sync po/docbooks with svn 2024-08-26 01:35:00 +00:00
l10n daemon script
af0d3d5ee1 GIT_SILENT Sync po/docbooks with svn 2024-08-25 01:26:59 +00:00
James Graham
07dd1d2e91 Make sure that the reply message is hidden if a user is ignored.
This will hide the content when a user ignores and re show it if unignored in the same session.

Note: If the client is restarted the rely will be blanked as the server refuses to send the message. However if unignoring a restart is currently required to get the full timeline back. This can't be trivially fixed as it takes a bit of time for the server to deal with the unblock and allow the message to be downloaded. With no signals available to jump off we'd just have to poll the endpoint which considering this is not going to happen often seems like a bad idea for minimal gain.

Closes network/neochat#657
2024-08-24 16:37:51 +00:00
Tobias Fella
185a88a16e Fix opening ignored users page 2024-08-24 17:32:32 +02:00
Tobias Fella
e3059e636a Fix ignoring invitations 2024-08-24 17:32:15 +02:00
Tobias Fella
aeb566746a Use plaintext for reply author component 2024-08-24 17:30:46 +02:00
Tobias Fella
8da567d9fa Don't run QtKeychain job in a nested event loop
Doing that causes deadlocks and there's no need for it here
2024-08-24 11:21:20 +02:00
Tobias Fella
d99f69cc24 Adapt to libQuotient API change 2024-08-24 11:03:28 +02:00
l10n daemon script
8240962400 GIT_SILENT Sync po/docbooks with svn 2024-08-24 01:27:32 +00:00
Tobias Fella
bd1d4289c0 Ensure that room list does not show rooms without title during startup
There's a brief moment during startup where the model knows about the rooms, but their state
is not loaded, which makes them show up in the room list with ugly fallback titles.
To prevent this, we delay closing the welcome page until the basestate is loaded.
2024-08-23 17:08:46 +02:00
James Graham
5d2139471a MessageEditComponent Updates
Rename MessageEditComponent to ChatBarComponent in preparation for also using it with threads and cleanup.
2024-08-23 14:56:01 +00:00
Tobias Fella
d49a64ac1e Remove unused MatrixImageProvider sources 2024-08-23 16:36:07 +02:00
James Graham
cd867ea581 Rework event handler to be just a series of static helper functions
- Clear out unused functions
- All functions are now static

This is because we pretty much always used it in the form:
```
EventHandler eventHandler(room, event);
eventHandler.function();
```
This simplifies it all to a single call.
2024-08-23 14:30:03 +00:00
James Graham
32fd62c484 Add option to reset all config values to their default
Closes network/neochat#504
2024-08-23 09:20:09 +00:00
l10n daemon script
f2c561bd15 GIT_SILENT Sync po/docbooks with svn 2024-08-23 01:27:31 +00:00
James Graham
80734944d3 Use sections for power levels in the user list
Closes network/neochat#654
2024-08-22 19:34:31 +00:00
James Graham
b3afa9f595 Add delegates to show room upgrades into the timeline model.
The delegates are at the beginning for upgraded rooms and end for predecessors.

Closes: network/neochat#620 and network/neochat#619
2024-08-22 17:21:36 +00:00
l10n daemon script
656558850c GIT_SILENT Sync po/docbooks with svn 2024-08-22 01:30:41 +00:00
Tobias Fella
e41cca9be0 Make room leaving more robust 2024-08-21 20:31:33 +02:00
Laurent Montel
46916b34d4 Remove duplicate headers from headers/cpp 2024-08-21 12:28:57 +00:00
l10n daemon script
4f9ca3e74d GIT_SILENT Sync po/docbooks with svn 2024-08-20 01:36:45 +00:00
l10n daemon script
2bcdd0f52b GIT_SILENT Sync po/docbooks with svn 2024-08-19 01:30:37 +00:00
James Graham
56d790dda9 Thread View
So at the moment this remains behind the feature flag as this only adds a threadmodel and a basic visualisation. There is much more to come to get it ready for full release.
2024-08-18 15:19:03 +00:00
James Graham
149013d2ff Fix pending events not showing when the server event turns up 2024-08-18 15:26:31 +02:00
Tobias Fella
776807580a Re-add pending event indicator
This seems to have been accidentally removed

BUG: 491277
2024-08-18 15:26:30 +02:00
Tobias Fella
75e9eee3a9 Fix pending events all showing the same text
The content models were stored in the hasmap under the same key, since they all don't have a valid event id yet.
Store them under their transaction id instead.

BUG: 491277
2024-08-18 15:26:28 +02:00
l10n daemon script
183615fa7b GIT_SILENT Sync po/docbooks with svn 2024-08-16 01:25:55 +00:00
Heiko Becker
7405fbaa16 GIT_SILENT Update Appstream for new release
(cherry picked from commit 86d85c6ce7)
2024-08-16 00:35:07 +02:00
Andreas Sturmlechner
22743b6d8b Include missing ECMQmlModule
Amends bc67033c00 and e0c3a1c143

No idea why this isn't caught by CI, but it fails for me otherwise.

Signed-off-by: Andreas Sturmlechner <asturm@gentoo.org>
2024-08-15 19:47:13 +00:00
l10n daemon script
b5864f02cb GIT_SILENT Sync po/docbooks with svn 2024-08-15 01:28:27 +00:00
Julius Künzel
373431fb1a Add job to publish to Microsoft Store
This still requires a manual action in the MS Partner Center, but makes the submission easier.
Note that on purpose the job will only be available on release branches, not on master
2024-08-10 10:38:18 +02:00
Julius Künzel
29167fbdf2 Enable appx build for Windows 2024-08-10 10:38:16 +02:00
Tobias Fella
699ac3a8e6 Remove undefined function declarations 2024-08-10 10:35:38 +02:00
l10n daemon script
d7b572faaf 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"
2024-08-10 01:26:10 +00:00
l10n daemon script
a09b4e7f2c GIT_SILENT made messages (after extraction) 2024-08-10 00:41:18 +00:00
l10n daemon script
a794420d3e 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"
2024-08-09 01:21:45 +00:00
l10n daemon script
8b606266c5 GIT_SILENT made messages (after extraction) 2024-08-09 00:39:31 +00:00
l10n daemon script
11e4329d5e GIT_SILENT Sync po/docbooks with svn 2024-08-07 01:28:10 +00:00
James Graham
68dfc6ca81 Show notification counts on the room Avatars when collapsed.
Show notification counts on the room Avatars when collapsed.

BUG: 468520
2024-08-06 20:51:16 +00:00
l10n daemon script
e0c8945431 GIT_SILENT Sync po/docbooks with svn 2024-08-06 01:27:29 +00:00
l10n daemon script
fe60ee00fb GIT_SILENT Sync po/docbooks with svn 2024-08-04 01:27:54 +00:00
l10n daemon script
5990e00577 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"
2024-08-04 01:20:55 +00:00
l10n daemon script
358a699290 GIT_SILENT Sync po/docbooks with svn 2024-08-03 01:28:22 +00:00
l10n daemon script
da6e4378d9 GIT_SILENT Sync po/docbooks with svn 2024-08-02 01:30:14 +00:00
l10n daemon script
5edb0629b3 GIT_SILENT Sync po/docbooks with svn 2024-08-01 01:31:30 +00:00
Tobias Fella
5170854a2c Add UI for importing and exporting megolm keys 2024-07-31 23:05:54 +02:00
James Graham
37d6033df4 Manage MessageContentModels properly so we don't leak memory.
- Manage MessageContentModels properly so we don't leak memory creating new ones every time the role is refreshed.
- Parent and reply MessageContentModels to their message to make sure they get cleaned up when the parent is deleted.
- Make sure ReactionModels are cleaned up on room change to stop that list just growing.
2024-07-31 17:57:11 +00:00
l10n daemon script
ea2f891533 GIT_SILENT Sync po/docbooks with svn 2024-07-30 01:30:47 +00:00
Tobias Fella
42cec7d5ba Show time without timezone in tooltip
Constructing the timezone string is relatively heavy and is done for each delegate, even when the tooltip is never shown
2024-07-29 17:46:27 +02:00
Tobias Fella
2df2e39d43 Show time without seconds in timeline 2024-07-29 17:46:15 +02:00
l10n daemon script
b34525a4d8 GIT_SILENT Sync po/docbooks with svn 2024-07-29 01:29:11 +00:00
l10n daemon script
0a7bd64b0d GIT_SILENT Sync po/docbooks with svn 2024-07-28 01:30:28 +00:00
James Graham
11fd4f88ec Create NeochatRoomMember as a shim for RoomMember
The intention is that NeochatRoomMember can be created passed to QML and then be fully managed by it. It effectively just grabs the current RoomMember, calls the correct function then discards it so that we don't end up trying to access an already deleted state event.
2024-07-27 08:46:56 +00:00
l10n daemon script
4d9452b862 GIT_SILENT Sync po/docbooks with svn 2024-07-27 01:30:45 +00:00
Joshua Goins
060e30e612 Suggest what to do on the empty welcome screen
A simple change, adds a sentence to the "Welcome to NeoChat" message
when it first starts up and you have nothing selected.
2024-07-26 19:32:10 +00:00
Joshua Goins
ade7270add ThreePIdCard: Use Title Case and add an icon for the "Add" button 2024-07-26 19:10:30 +00:00
Joshua Goins
338428b0c0 AccountEditor: Improve strings a little
Changes things to Title Case as necessary, and shortens the QR code
option to be more succinct.
2024-07-26 19:10:30 +00:00
Joshua Goins
54574e4450 AccountEditor: Add placeholder text for "Label"
This makes it much clearer what possible purpose this field could do.
2024-07-26 19:10:30 +00:00
Joshua Goins
ae564451b8 AccountEditor: Add icons to all of these form card buttons
This helps make them stand out apart from the text they are surrounded
by.
2024-07-26 19:10:30 +00:00
Joshua Goins
9a4504ce61 Make the "Unignore this user" button work on the Ignored Users page
This function now takes a QString. This should be fine to use since it's
in the 0.8.2 release which we require.
2024-07-26 19:01:19 +00:00
Joshua Goins
80785a2ff3 Use a Kirigami.PlaceholderMessage in the ignored list
This makes it look cleaner when there's nothing there, and looks
 standard beside other KDE applications. Also removes a duplicate
"Ignored Users" form header that didn't add anything.
2024-07-26 19:01:19 +00:00
Joshua Goins
cb8ed02e82 Fix the emoji page not doing anything
This is yet more stuff broken due to referencing a implicit pageStack
that no longer exists.
2024-07-26 18:55:23 +00:00
Joshua Goins
a64c1b8eab Add a "Show QR code" to the account menu
Making this a couple of clicks away under the account menu should make
it easier to show the QR code without digging through the settings.
2024-07-26 18:44:05 +00:00
Tobias Fella
d43aa169c3 Use let kconfig register the config class 2024-07-26 19:25:55 +02:00
Tobias Fella
8ae7141851 Use cmake function instead of kcfgc 2024-07-26 19:25:00 +02:00
Joshua Goins
d3908db2c9 Fix Carl's avatar in the about data
This is supposed to be a QUrl to catch the correct overload.
2024-07-26 07:41:21 +00:00
Yuri Chornoivan
55de838a55 Fix minor typo 2024-07-26 08:36:41 +03:00
l10n daemon script
4cdfc38b87 GIT_SILENT Sync po/docbooks with svn 2024-07-26 01:26:00 +00:00
Joshua Goins
51197d7c1a Don't flag invite notifications as persistent
These really don't need to be persistent, as they even stick around
when NeoChat is closed. This also spams the user's notification system
usually, if they get lots of invitations at once which don't go away
automatically.
2024-07-25 20:11:50 +00:00
Joshua Goins
2a2a2e0c05 Hint that the user can auto-reject invitations on the invite page
This should help make the new setting more discoverable.
2024-07-25 19:58:58 +00:00
Joshua Goins
07fee30cc0 Allow blocking invites from people you don't share a room with
Matrix currently has a significant moderation loophole, thanks to
invites. Right now, anyone can invite anyone to a room - and clients
like NeoChat will gladly display these rooms to them and even give you
a notification.

However, this creates a pretty easy attack since room names and avatars
are arbitrary and this is a known vector of harassment in the Matrix
community. There's currently no tools to block this server-side, so
let's try to improve the situation where we can.

This adds a new setting to the Security page, wherein it allows you to
block invites from people you don't share a room with. This prevents the
notification from appearing and NeoChat will attempt to leave the room
immediately.

Since this depends on MSC 2666 - a currently unstable feature - the
server may not support it and NeoChat will disable the setting in this
case.
2024-07-25 19:58:58 +00:00
Joshua Goins
83c6ce0ace Don't display notifications for invite rooms
Sometimes a ghost notification will appear, this is sometimes a stray
m.room.invite notification we didn't handle or some other event. Let's
outright reject all notifications from Invite-type rooms to prevent this
from happening altogether.
2024-07-25 15:35:12 -04:00
Joshua Goins
fb7303efa0 Revert "Allow blocking invites from people you don't share a room with"
This reverts commit ef5585d312. This was
supposed to be in an MR.
2024-07-25 15:04:53 -04:00
Joshua Goins
ef5585d312 Allow blocking invites from people you don't share a room with
Matrix currently has a significant moderation loophole, thanks to
invites. Right now, anyone can invite anyone to a room - and clients
like NeoChat will gladly display these rooms to them and even give you
a notification.

However, this creates a pretty easy attack since room names and avatars
are arbitrary and this is a known vector of harassment in the Matrix
community. There's currently no tools to block this server-side, so
let's try to improve the situation where we can.

This adds a new setting to the Security page, wherein it allows you to
block invites from people you don't share a room with. This prevents the
notification from appearing and NeoChat will attempt to leave the room
immediately.

Since this depends on MSC 2666 - a currently unstable feature - the
server may not support it and NeoChat will disable the setting in this
case.
2024-07-25 15:03:22 -04:00
James Graham
11475259a1 Make sure that apostrophes are unescaped when visualising messages
Title

BUG: 488325
2024-07-23 17:27:00 +00:00
l10n daemon script
9df4cd6f13 GIT_SILENT Sync po/docbooks with svn 2024-07-23 01:27:45 +00:00
Albert Astals Cid
c9583964c8 GIT_SILENT Upgrade release service version to 24.11.70. 2024-07-21 12:54:43 +02:00
224 changed files with 61253 additions and 38180 deletions

View File

@@ -2,5 +2,5 @@
; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings]
kde/frameworks/extra-cmake-modules.version=master
kde/applications/neochat.packageAppx=True
libs/qt.qtMajorVersion=6

View File

@@ -10,6 +10,8 @@ include:
- /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml

View File

@@ -8,13 +8,13 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.0")
set(KF_MIN_VERSION "6.6")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -38,6 +38,9 @@ include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
include(ECMQmlModule)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -59,7 +62,6 @@ set_package_properties(Qt6 PROPERTIES
PURPOSE "Basic application components"
)
qt_policy(SET QTP0001 NEW)
if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
@@ -101,7 +103,7 @@ else()
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()

View File

@@ -55,5 +55,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- for Android >= 33 -->
</manifest>

View File

@@ -82,3 +82,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME linkpreviewertest
)
ecm_add_test(
messagecontentmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME messagecontentmodeltest
)

View File

@@ -14,7 +14,6 @@ class ActionsHandlerTest : public QObject
private:
Quotient::Connection *connection = Quotient::Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
ActionsHandler *actionsHandler = new ActionsHandler(this);
private Q_SLOTS:
void nullObject();
@@ -23,20 +22,19 @@ private Q_SLOTS:
void ActionsHandlerTest::nullObject()
{
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(nullptr);
ActionsHandler::handleMessageEvent(nullptr, nullptr);
auto chatBarCache = new ChatBarCache(this);
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(chatBarCache);
ActionsHandler::handleMessageEvent(nullptr, chatBarCache);
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"));
actionsHandler->setRoom(room);
QTest::ignoreMessage(QtWarningMsg, "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(nullptr);
ActionsHandler::handleMessageEvent(room, nullptr);
// The final one should throw no warning so we make sure.
QTest::failOnWarning("ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.");
actionsHandler->handleMessageEvent(chatBarCache);
ActionsHandler::handleMessageEvent(room, chatBarCache);
}
QTEST_GUILESS_MAIN(ActionsHandlerTest)

View File

@@ -51,7 +51,7 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -65,7 +65,7 @@ void ChatBarCacheTest::noRoom()
// 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(), Quotient::RoomMember());
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -81,7 +81,7 @@ void ChatBarCacheTest::badParent()
// 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(), Quotient::RoomMember());
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
@@ -99,7 +99,7 @@ void ChatBarCacheTest::reply()
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationAuthor(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -107,8 +107,13 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(QLatin1String("$153456789:example.org")));
});
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
@@ -116,7 +121,7 @@ void ChatBarCacheTest::edit()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationUser(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationAuthor(), room->member(QLatin1String("@example:example.org")));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -133,7 +138,7 @@ void ChatBarCacheTest::attachment()
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->member(QString()));
QCOMPARE(chatBarCache->relationAuthor(), room->member(QString()));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
}

View File

@@ -29,15 +29,11 @@ private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
EventHandler emptyHandler = EventHandler(nullptr, nullptr);
private Q_SLOTS:
void initTestCase();
void eventId();
void nullEventId();
void author();
void nullAuthor();
void authorDisplayName();
void nullAuthorDisplayName();
void singleLineSidplayName();
@@ -45,7 +41,6 @@ private Q_SLOTS:
void time();
void nullTime();
void timeString();
void nullTimeString();
void highlighted();
void nullHighlighted();
void hidden();
@@ -67,10 +62,6 @@ private Q_SLOTS:
void nullReplyId();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
void nullReplyBody();
void replyMediaInfo();
void nullReplyMediaInfo();
void thread();
void nullThread();
void location();
@@ -85,182 +76,136 @@ void EventHandlerTest::initTestCase()
void EventHandlerTest::eventId()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
QCOMPARE(EventHandler::id(room->messageEvents().at(0).get()), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->member(event->senderId());
EventHandler eventHandler(room, event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor.isLocalMember(), author.id() == room->localMember().id());
QCOMPARE(eventHandlerAuthor.id(), author.id());
QCOMPARE(eventHandlerAuthor.displayName(), author.displayName());
QCOMPARE(eventHandlerAuthor.avatarUrl(), author.avatarUrl());
QCOMPARE(eventHandlerAuthor.avatarMediaId(), author.avatarMediaId());
QCOMPARE(eventHandlerAuthor.color(), author.color());
}
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), RoomMember());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), RoomMember());
QTest::ignoreMessage(QtWarningMsg, "id called with event set to nullptr.");
QCOMPARE(EventHandler::id(nullptr), QString());
}
void EventHandlerTest::authorDisplayName()
{
EventHandler eventHandler(room, room->messageEvents().at(1).get());
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
QCOMPARE(EventHandler::authorDisplayName(room, room->messageEvents().at(1).get()), QStringLiteral("before"));
}
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with room set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(nullptr, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "authorDisplayName called with event set to nullptr.");
QCOMPARE(EventHandler::authorDisplayName(room, nullptr), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
EventHandler eventHandler(room, room->messageEvents().at(11).get());
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, room->messageEvents().at(11).get()),
QStringLiteral("Look at me I put newlines in my display name"));
}
void EventHandlerTest::nullSingleLineDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with room set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(nullptr, nullptr), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
QTest::ignoreMessage(QtWarningMsg, "singleLineAuthorDisplayname called with event set to nullptr.");
QCOMPARE(EventHandler::singleLineAuthorDisplayname(room, nullptr), QString());
}
void EventHandlerTest::time()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
const auto event = room->messageEvents().at(0).get();
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::nullTime()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
QCOMPARE(EventHandler::time(nullptr), QDateTime());
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTime(true), QDateTime());
QCOMPARE(EventHandler::time(room->messageEvents().at(0).get(), true), QDateTime());
}
void EventHandlerTest::timeString()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
const auto event = room->messageEvents().at(0).get();
KFormat format;
QCOMPARE(eventHandler.getTimeString(false),
QCOMPARE(EventHandler::timeString(event, false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true),
QCOMPARE(EventHandler::timeString(event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(EventHandler::timeString(event, false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(EventHandler::timeString(event, true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(EventHandler::timeString(event, false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
QCOMPARE(EventHandler::timeString(event, true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::nullTimeString()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
EventHandler eventHandler(room, 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());
QCOMPARE(EventHandler::timeString(event, QStringLiteral("hh:mm")),
QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toString(QStringLiteral("hh:mm")));
}
void EventHandlerTest::highlighted()
{
EventHandler eventHandlerHighlight(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandlerHighlight.isHighlighted(), true);
EventHandler eventHandlerNoHighlight(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHighlight.isHighlighted(), false);
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(2).get()), true);
QCOMPARE(EventHandler::isHighlighted(room, room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with room set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(nullptr, nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with event set to nullptr.");
QCOMPARE(EventHandler::isHighlighted(room, nullptr), false);
}
void EventHandlerTest::hidden()
{
EventHandler eventHandlerHidden(room, room->messageEvents().at(3).get());
QCOMPARE(eventHandlerHidden.isHidden(), true);
EventHandler eventHandlerNoHidden(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoHidden.isHidden(), false);
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(3).get()), true);
QCOMPARE(EventHandler::isHidden(room, room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with room set to nullptr.");
QCOMPARE(EventHandler::isHidden(nullptr, nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with event set to nullptr.");
QCOMPARE(EventHandler::isHidden(room, nullptr), false);
}
void EventHandlerTest::body()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
const auto event = room->messageEvents().at(0).get();
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
QCOMPARE(EventHandler::richBody(room, event), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(EventHandler::richBody(room, event, true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(EventHandler::plainBody(room, event), QStringLiteral("This is an example\ntext message"));
QCOMPARE(EventHandler::plainBody(room, event, true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "richBody called with room set to nullptr.");
QCOMPARE(EventHandler::richBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "richBody called with event set to nullptr.");
QCOMPARE(EventHandler::richBody(room, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getPlainBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with room set to nullptr.");
QCOMPARE(EventHandler::plainBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "plainBody called with event set to nullptr.");
QCOMPARE(EventHandler::plainBody(room, nullptr), QString());
}
void EventHandlerTest::genericBody_data()
@@ -268,11 +213,13 @@ void EventHandlerTest::genericBody_data()
QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
QTest::newRow("message") << 0 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message");
QTest::newRow("member") << 1
<< QStringLiteral(
"<a href=\"https://matrix.to/#/@example:example.org\">after</a> changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message");
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
QTest::newRow("video") << 4 << QStringLiteral("<a href=\"https://matrix.to/#/@example:example.org\">after</a> sent a message");
}
void EventHandlerTest::genericBody()
@@ -280,54 +227,48 @@ void EventHandlerTest::genericBody()
QFETCH(int, eventNum);
QFETCH(QString, output);
EventHandler eventHandler(room, room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
QCOMPARE(EventHandler::genericBody(room, room->messageEvents().at(eventNum).get()), output);
}
void EventHandlerTest::nullGenericBody()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "genericBody called with room set to nullptr.");
QCOMPARE(EventHandler::genericBody(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "genericBody called with event set to nullptr.");
QCOMPARE(EventHandler::genericBody(room, nullptr), QString());
}
void EventHandlerTest::markdownBody()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(0).get()), QStringLiteral("This is an example\ntext message"));
}
void EventHandlerTest::markdownBodyReply()
{
EventHandler eventHandler(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandler.getMarkdownBody(), QStringLiteral("reply"));
QCOMPARE(EventHandler::markdownBody(room->messageEvents().at(5).get()), QStringLiteral("reply"));
}
void EventHandlerTest::subtitle()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.subtitleText(), QStringLiteral("after: This is an example text message"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.subtitleText(), QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(0).get()), QStringLiteral("after: This is an example text message"));
QCOMPARE(EventHandler::subtitleText(room, room->messageEvents().at(2).get()),
QStringLiteral("after: This is a highlight @bob:kde.org and this is a link https://kde.org"));
}
void EventHandlerTest::nullSubtitle()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with m_event set to nullptr.");
QCOMPARE(noEventHandler.subtitleText(), QString());
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with room set to nullptr.");
QCOMPARE(EventHandler::subtitleText(nullptr, nullptr), QString());
QTest::ignoreMessage(QtWarningMsg, "subtitleText called with event set to nullptr.");
QCOMPARE(EventHandler::subtitleText(room, nullptr), QString());
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
EventHandler eventHandler(room, event);
auto mediaInfo = eventHandler.getMediaInfo();
auto mediaInfo = EventHandler::mediaInfo(room, event);
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
@@ -347,53 +288,42 @@ void EventHandlerTest::mediaInfo()
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with room set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(nullptr, nullptr), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "mediaInfo called with event set to nullptr.");
QCOMPARE(EventHandler::mediaInfo(room, nullptr), QVariantMap());
}
void EventHandlerTest::hasReply()
{
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.hasReply(), true);
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.hasReply(), false);
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(5).get()), true);
QCOMPARE(EventHandler::hasReply(room->messageEvents().at(0).get()), false);
}
void EventHandlerTest::nullHasReply()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
QTest::ignoreMessage(QtWarningMsg, "hasReply called with event set to nullptr.");
QCOMPARE(EventHandler::hasReply(nullptr), false);
}
void EventHandlerTest::replyId()
{
EventHandler eventHandlerReply(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReply.getReplyId(), QStringLiteral("$153456789:example.org"));
EventHandler eventHandlerNoReply(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoReply.getReplyId(), QStringLiteral(""));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(5).get()), QStringLiteral("$153456789:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(0).get()), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
QTest::ignoreMessage(QtWarningMsg, "replyId called with event set to nullptr.");
QCOMPARE(EventHandler::replyId(nullptr), QString());
}
void EventHandlerTest::replyAuthor()
{
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->member(replyEvent->senderId());
EventHandler eventHandler(room, room->messageEvents().at(5).get());
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
auto eventHandlerReplyAuthor = EventHandler::replyAuthor(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandlerReplyAuthor.isLocalMember(), replyAuthor.id() == room->localMember().id());
QCOMPARE(eventHandlerReplyAuthor.id(), replyAuthor.id());
@@ -402,121 +332,58 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandlerReplyAuthor.avatarMediaId(), replyAuthor.avatarMediaId());
QCOMPARE(eventHandlerReplyAuthor.color(), replyAuthor.color());
EventHandler eventHandlerNoAuthor(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoAuthor.getReplyAuthor(), RoomMember());
QCOMPARE(EventHandler::replyAuthor(room, room->messageEvents().at(0).get()), RoomMember());
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), RoomMember());
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with room set to nullptr.");
QCOMPARE(EventHandler::replyAuthor(nullptr, nullptr), RoomMember());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), RoomMember());
}
void EventHandlerTest::replyBody()
{
EventHandler eventHandler(room, room->messageEvents().at(5).get());
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullReplyBody()
{
EventHandler noEventHandler(room, nullptr);
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();
auto replyEvent = room->messageEvents().at(4).get();
EventHandler eventHandler(room, event);
auto mediaInfo = eventHandler.getReplyMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
QCOMPARE(mediaInfo["size"_ls], 62650636);
QCOMPARE(mediaInfo["duration"_ls], 10);
QCOMPARE(mediaInfo["width"_ls], 1920);
QCOMPARE(mediaInfo["height"_ls], 1080);
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
QCOMPARE(thumbnailInfo["size"_ls], 382249);
QCOMPARE(thumbnailInfo["width"_ls], 800);
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullReplyMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "replyAuthor called with event set to nullptr. Returning empty user.");
QCOMPARE(EventHandler::replyAuthor(room, nullptr), RoomMember());
}
void EventHandlerTest::thread()
{
EventHandler eventHandlerNoThread(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandlerNoThread.isThreaded(), false);
QCOMPARE(eventHandlerNoThread.threadRoot(), QString());
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(0).get()), false);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(0).get()), QString());
EventHandler eventHandlerThreadRoot(room, room->messageEvents().at(9).get());
QCOMPARE(eventHandlerThreadRoot.isThreaded(), true);
QCOMPARE(eventHandlerThreadRoot.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadRoot.getReplyId(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(9).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(9).get()), QStringLiteral("$threadroot:example.org"));
EventHandler eventHandlerThreadReply(room, room->messageEvents().at(10).get());
QCOMPARE(eventHandlerThreadReply.isThreaded(), true);
QCOMPARE(eventHandlerThreadReply.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandlerThreadReply.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
QCOMPARE(EventHandler::isThreaded(room->messageEvents().at(10).get()), true);
QCOMPARE(EventHandler::threadRoot(room->messageEvents().at(10).get()), QStringLiteral("$threadroot:example.org"));
QCOMPARE(EventHandler::replyId(room->messageEvents().at(10).get()), 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, "isThreaded called with event set to nullptr.");
QCOMPARE(EventHandler::isThreaded(nullptr), false);
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with event set to nullptr.");
QCOMPARE(EventHandler::threadRoot(nullptr), QString());
}
void EventHandlerTest::location()
{
EventHandler eventHandler(room, room->messageEvents().at(7).get());
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
QCOMPARE(EventHandler::latitude(room->messageEvents().at(7).get()), QStringLiteral("51.7035").toFloat());
QCOMPARE(EventHandler::longitude(room->messageEvents().at(7).get()), QStringLiteral("-1.14394").toFloat());
QCOMPARE(EventHandler::locationAssetType(room->messageEvents().at(7).get()), 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, "latitude called with event set to nullptr.");
QCOMPARE(EventHandler::latitude(nullptr), -100.0);
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLongitude(), -200.0);
QTest::ignoreMessage(QtWarningMsg, "longitude called with event set to nullptr.");
QCOMPARE(EventHandler::longitude(nullptr), -200.0);
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
QTest::ignoreMessage(QtWarningMsg, "locationAssetType called with event set to nullptr.");
QCOMPARE(EventHandler::locationAssetType(nullptr), QString());
}
QTEST_MAIN(EventHandlerTest)

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include "models/messagecontentmodel.h"
#include "testutils.h"
using namespace Quotient;
using namespace Qt::Literals::StringLiterals;
class MessageContentModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
private Q_SLOTS:
void initTestCase();
void missingEvent();
};
void MessageContentModelTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
}
void MessageContentModelTest::missingEvent()
{
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#firstRoom:kde.org"));
auto model1 = MessageContentModel(room, "$153456789:example.org"_L1);
QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), "Loading"_L1);
auto model2 = MessageContentModel(room, "$153456789:example.org"_L1, true);
QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), "Loading reply"_L1);
room->syncNewEvents(QLatin1String("test-min-sync.json"));
QCOMPARE(model1.rowCount(), 2);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Author);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::ComponentTypeRole), MessageComponentType::Text);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::DisplayRole), u"<b>This is an example<br>text message</b>"_s);
}
QTEST_MAIN(MessageContentModelTest)
#include "messagecontentmodeltest.moc"

View File

@@ -12,9 +12,7 @@
#include "enums/messagecomponenttype.h"
#include "models/customemojimodel.h"
#include "models/messagecontentmodel.h"
#include "neochatconnection.h"
#include "utils.h"
#include "testutils.h"
@@ -46,6 +44,7 @@ private Q_SLOTS:
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
@@ -85,11 +84,11 @@ void TextHandlerTest::initTestCase()
void TextHandlerTest::allowedAttributes()
{
const QString testInputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
const QString testOutputString1 = QStringLiteral("<p><span data-mx-spoiler><font color=#FFFFFF>Test</font><span></p>");
const QString testInputString1 = QStringLiteral("<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>");
const QString testOutputString1 = QStringLiteral("<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>");
// Handle urls where the href has either single (') or double (") quotes.
const QString testInputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
const QString testOutputString2 = QStringLiteral("<p><a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a></p>");
const QString testInputString2 = QStringLiteral("<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>");
const QString testOutputString2 = QStringLiteral("<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
@@ -117,7 +116,7 @@ void TextHandlerTest::stripDisallowedTags()
void TextHandlerTest::stripDisallowedAttributes()
{
const QString testInputString = QStringLiteral("<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>");
const QString testOutputString = QStringLiteral("<p>Test</p>");
const QString testOutputString = QStringLiteral("Test");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -144,8 +143,8 @@ void TextHandlerTest::emptyCodeTags()
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
const QString testOutputString = QStringLiteral("<p>This data should just be put in a paragraph.</p>");
const QString testInputString = QStringLiteral("This data should just be left alone.");
const QString testOutputString = QStringLiteral("This data should just be left alone.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -158,8 +157,8 @@ void TextHandlerTest::sendSingleParaMarkup()
const QString testInputString = QStringLiteral(
"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`.");
const QString testOutputString = QStringLiteral(
"<p>Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.</p>");
"Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img "
"src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>.");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -185,7 +184,7 @@ void TextHandlerTest::sendMultipleSectionMarkup()
void TextHandlerTest::sendBadLinks()
{
const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)");
const QString testOutputString = QStringLiteral("<p><a>link</a>, <img alt=\"image\"></p>");
const QString testOutputString = QStringLiteral("<a>link</a>, <img alt=\"image\">");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -222,8 +221,8 @@ void TextHandlerTest::sendCodeClass()
void TextHandlerTest::sendCustomEmoji()
{
const QString testInputString = QStringLiteral(":test:");
const QString testOutputString = QStringLiteral(
"<p><img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" /></p>");
const QString testOutputString =
QStringLiteral("<img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" />");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -236,7 +235,7 @@ void TextHandlerTest::sendCustomEmojiCode_data()
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QString>("testOutputString");
QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral("<p><code>:test:</code></p>");
QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral("<code>:test:</code>");
QTest::newRow("block") << QStringLiteral("```\n:test:\n```") << QStringLiteral("<pre><code>:test:\n</code></pre>");
}
@@ -252,6 +251,19 @@ void TextHandlerTest::sendCustomEmojiCode()
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
const QString testInputString = QStringLiteral("Test...<br/>...ing");
const QString testRichOutputString = QStringLiteral("Test...<br/>...ing");
const QString testPlainOutputString = QStringLiteral("Test...\n...ing");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
@@ -274,6 +286,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
QTest::newRow("unescape") << QStringLiteral("can&#x27;t") << QStringLiteral("can't");
}
void TextHandlerTest::receiveRichInPlainOut()
@@ -360,7 +373,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill()
{
const QString testInputString = QStringLiteral("<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>");
const QString testOutputString = QStringLiteral("<p><b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b></p>");
const QString testOutputString = QStringLiteral("<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -371,7 +384,7 @@ void TextHandlerTest::receiveRichUserPill()
void TextHandlerTest::receiveRichStrikethrough()
{
const QString testInputString = QStringLiteral("<p><del>Test</del></p>");
const QString testOutputString = QStringLiteral("<p><s>Test</s></p>");
const QString testOutputString = QStringLiteral("<s>Test</s>");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
@@ -448,6 +461,9 @@ void TextHandlerTest::receiveRichPlainUrl()
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
QString testInputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b");
QString testOutputStringMxIdWithPrefix = QStringLiteral("a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
@@ -461,6 +477,9 @@ void TextHandlerTest::receiveRichPlainUrl()
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
testTextHandler.setData(testInputStringMxIdWithPrefix);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
}
void TextHandlerTest::receiveRichEdited_data()
@@ -535,7 +554,7 @@ void TextHandlerTest::componentOutput_data()
"someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you "
"defined the id")
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like<br/>"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(

View File

@@ -17,7 +17,6 @@ class WindowControllerTest : public QObject
private Q_SLOTS:
void nullWindow();
void geometry();
void showAndRaise();
void toggle();
@@ -30,32 +29,10 @@ void WindowControllerTest::nullWindow()
auto &instance = WindowController::instance();
QCOMPARE(instance.window(), nullptr);
instance.restoreGeometry();
instance.saveGeometry();
instance.showAndRaiseWindow({});
instance.toggleWindow();
}
void WindowControllerTest::geometry()
{
auto &instance = WindowController::instance();
QWindow window;
window.setGeometry(0, 0, 200, 200);
instance.setWindow(&window);
QCOMPARE(instance.window(), &window);
instance.saveGeometry();
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
QCOMPARE(KWindowConfig::hasSavedWindowSize(windowGroup), true);
window.setGeometry(0, 0, 400, 400);
QCOMPARE(window.geometry(), QRect(0, 0, 400, 400));
instance.restoreGeometry();
QCOMPARE(window.geometry(), QRect(0, 0, 200, 200));
}
void WindowControllerTest::showAndRaise()
{
auto &instance = WindowController::instance();

View File

@@ -25,6 +25,7 @@
<name xml:lang="fi">NeoChat</name>
<name xml:lang="fr">NeoChat</name>
<name xml:lang="gl">NeoChat</name>
<name xml:lang="he">NeoChat</name>
<name xml:lang="hu">NeoChat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name>
@@ -54,6 +55,8 @@
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="cs">Mluvte se svými přáteli na Matrixu</summary>
<summary xml:lang="de">Mit den Freunden auf Matrix unterhalten</summary>
<summary xml:lang="el">Συνομιλήστε με τους φίλους σας στο matrix</summary>
<summary xml:lang="en-GB">Chat with your friends on matrix</summary>
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
@@ -61,6 +64,7 @@
<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="he">התכתבות עם החברים שלך ב־matrix</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
@@ -83,12 +87,16 @@
<p xml:lang="ar">نيوتشات هو تطبيق دردشة يتيح لك الاستفادة الكاملة من شبكة Matrix. فهو يوفر لك طريقة آمنة لإرسال الرسائل النصية ومقاطع الفيديو والملفات الصوتية إلى عائلتك وزملائك وأصدقائك.</p>
<p xml:lang="ca">El NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="ca-valencia">NeoChat és una aplicació de xat que us permet aprofitar plenament la xarxa Matrix. Proporciona una manera segura d'enviar missatges de text, vídeos i arxius d'àudio a la vostra família, companys i amics.</p>
<p xml:lang="de">NeoChat ist eine Anwendung für Unterhaltungen mit allen Vorteilen des Matrix-Netzwerkes. Sie bietet eine sichere Möglichkeit zum Versenden von Nachrichten, Videos und Audiodateien and die Familienmitglieder.</p>
<p xml:lang="el">Το NeoChat είναι μια εφαρμογή συνομιλίας που σας επιτρέπει να εκμεταλλευτείτε πλήρως το δίκτυο Matrix. Σας παρέχει έναν ασφαλή τρόπο να στέλνετε μηνύματα κειμένου, βίντεο και αρχεία ήχου στην οικογένεια, τους συναδέλφους και τους φίλους σας.</p>
<p xml:lang="en-GB">NeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.</p>
<p xml:lang="eo">NeoChat estas babilej-apo, kiu ebligas al vi plene profiti de la Matrix-reto. Ĝi provizas al vi sekuran manieron sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj.</p>
<p xml:lang="es">NeoChat es una aplicación de chat que le permite aprovechar al máximo la red Matrix. Le proporciona un modo seguro de enviar mensajes de texto, vídeos y archivos de sonido a su familia, colegas y amigos.</p>
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
<p xml:lang="fi">NeoChat on keskustelusovellus, jolla Matrix-verkosta saa täyden hyödyn. Se tarjoaa salatun kanavan lähettää perheelle, työkavereille ja ystäville tekstiviestejä sekä video- ja äänitiedostoja.</p>
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
<p xml:lang="gl">NeoChat é unha aplicación de conversa que lle permite usar todas as funcionalidades da rede Matrix. Fornece unha forma segura de enviar mensaxes de texto e ficheiros de vídeo e son a familiares, amizades ou no traballo.</p>
<p xml:lang="he">NeoChat הוא יישום התכתבות שמאפשר לך לנצל את רשת Matrix במלואה. הוא מספק דרך מאובטחת לשליחת הודעות כתובות, סרטונים וקובצי שמע למשפחה, לעמיתים לעבודה ולחברים.</p>
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
@@ -107,6 +115,8 @@
<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 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="de">NeoChat versucht eine vollumfängliche Anwendung für die Spezifikation von Matrix zu sein. Damit wird alles der aktuellen stabilen Spezifikation mit den erwähnenswerten Ausnahmen von VoIP, Diskussionsfäden und ein paar Teilen der Ende-zu-Ende-Verschlüsselung unterstützt. Zudem sind andere kleinere Auslassungen vorhanden, da sich die Matrixspezifikation ständig weiterentwickelt. Nichtsdestotrotz soll letztendlich die gesamte Spezifikation unterstützt werden.</p>
<p xml:lang="el">Το NeoChat στοχεύει να είναι μια πλήρως εξοπλισμένη εφαρμογή για τις προδιαγραφές Matrix. Ως εκ τούτου, υποστηρίζονται όλα τα στοιχεία της τρέχουσας σταθερής προδιαγραφής με τις αξιοσημείωτες εξαιρέσεις του VoIP, των νημάτων και ορισμένων πτυχών της κρυπτογράφησης στα άκρα. Υπάρχουν μερικές άλλες μικρότερες παραλείψεις που οφείλονται στο γεγονός ότι η προδιαγραφή Matrix εξελίσσεται συνεχώς, αλλά ο στόχος παραμένει να παρέχεται τελικά υποστήριξη για ολόκληρη την προδιαγραφή.</p>
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
@@ -114,6 +124,7 @@
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="he">NeoChat מתיימר להיות יישום עתיר יכולות לפי מפרט Matrix. כיוון שזה ייעודו, כל מה שבמפרט היציב עם חריגות משמעותיות כגון VoIP, שרשורים ועוד מגוון היבטים של הצפנה מקצה לקצה נתמכים גם הם. יש מספר השמטות קטן עקב העובדה שהמפרט של Matrix ממשיך להתפתח אך המטרה היא להמשיך לספק תמיכה בסופו של דבר לכל המפרט.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenmente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
@@ -135,6 +146,8 @@
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
<p xml:lang="de">Durch die Weiterentwicklung der Matrix-Spezifikation unterstützt auch NeoChat einige als noch instabil gekennzeichnete Funktionen. Derzeit sind das:</p>
<p xml:lang="el">Λόγω της φύσης της ανάπτυξης των προδιαγραφών Matrix, το NeoChat υποστηρίζει επίσης πολλά ασταθή χαρακτηριστικά. Επί του παρόντος, αυτά είναι:</p>
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
<p xml:lang="eo">Pro la naturo de la Matrix-specifevoluo NeoChat ankaŭ subtenas multajn malstabilajn funkciojn. Nuntempe ĉi tiuj estas:</p>
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
@@ -142,6 +155,7 @@
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="he">מטבע הדברים, הפיתוח של NeoChat תומך במגוון יכולות מפוקפקות כתלות בהתפתחות המפרט הטכני של Matrix. היכולות האלה הן:</p>
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
@@ -165,6 +179,7 @@
<li xml:lang="ar">التصويت - MSC3381</li>
<li xml:lang="ca">Enquestes - MSC3381</li>
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="eo">Enketoj - MSC3381</li>
<li xml:lang="es">Encuestas - MSC3381</li>
@@ -172,6 +187,7 @@
<li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — MSC3381</li>
<li xml:lang="he">סקרים - MSC3381</li>
<li xml:lang="hu">Szavazások - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
@@ -193,6 +209,7 @@
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
@@ -200,6 +217,7 @@
<li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
<li xml:lang="he">חבילות מדבקות - MSC2545</li>
<li xml:lang="hu">Matricacsomagok - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
@@ -222,6 +240,7 @@
<li xml:lang="ar">موقع الأحداث - MSC3488</li>
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="el">Τοποθεσία γεγονότα - MSC3488</li>
<li xml:lang="en-GB">Location Events - MSC3488</li>
<li xml:lang="eo">Lokaj Eventoj - MSC3488</li>
<li xml:lang="es">Eventos de ubicación - MSC3488</li>
@@ -229,6 +248,7 @@
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos — MSC3488</li>
<li xml:lang="he">אירועי מקום - MSC3488</li>
<li xml:lang="hu">Események helyadatai - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
@@ -285,6 +305,8 @@
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="de">Hauptansicht mit Raumliste, Unterhaltung und Raum-Informationen</caption>
<caption xml:lang="el">Κύρια προβολή με λίστα δωματίων, συνομιλία και πληροφορίες δωματίων</caption>
<caption xml:lang="en-GB">Main view with room list, chat, and room information</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
@@ -292,6 +314,7 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -317,12 +340,16 @@
<caption xml:lang="ar">اكتشف مجتمعات جديدة مع فضاءات ماتركس</caption>
<caption xml:lang="ca">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="ca-valencia">Descobriu comunitats noves amb els espais de Matrix</caption>
<caption xml:lang="de">Neue Gemeinschaften mit den Umgebungen von Matrix erkunden</caption>
<caption xml:lang="el">Ανακαλύψτε νέες κοινότητες με το Matrix Spaces</caption>
<caption xml:lang="en-GB">Discover new communities with Matrix Spaces</caption>
<caption xml:lang="eo">Malkovru novajn komunumojn per Matrix Spaces</caption>
<caption xml:lang="es">Descubra nuevas comunidades con los espacios de Matrix</caption>
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
<caption xml:lang="fi">Löydä uusia yhteisöjä Matrix Spacesillä</caption>
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
<caption xml:lang="gl">Descubra novas comunidades dos espazos de Matrix.</caption>
<caption xml:lang="he">אפשר להיחשף לקהילות חדשות דרך Matrix Spaces</caption>
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
@@ -351,6 +378,8 @@
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
<caption xml:lang="de">Hauptansicht mit Raumliste, Unterhaltung und Raum-Informationen</caption>
<caption xml:lang="el">Κύρια προβολή με λίστα δωματίων, συνομιλία και πληροφορίες δωματίων</caption>
<caption xml:lang="en-GB">Main view with room list, chat, and room information</caption>
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
@@ -358,6 +387,7 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -384,6 +414,8 @@
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="cs">Přihlašovací obrazovka</caption>
<caption xml:lang="de">Anmeldebildschirm</caption>
<caption xml:lang="el">Οθόνη εισόδου</caption>
<caption xml:lang="en-GB">Login screen</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
@@ -391,6 +423,7 @@
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="he">מסך כניסה</caption>
<caption xml:lang="hu">Bejelentkező képernyő</caption>
<caption xml:lang="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption>
@@ -415,6 +448,9 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.08.2" date="2024-10-10"/>
<release version="24.08.1" date="2024-09-12"/>
<release version="24.08.0" date="2024-08-22"/>
<release version="24.05.2" date="2024-07-04"/>
<release version="24.05.1" date="2024-06-13"/>
<release version="24.05.0" date="2024-05-23"/>

View File

@@ -18,6 +18,7 @@ Name[eu]=NeoChat
Name[fi]=NeoChat
Name[fr]=NeoChat
Name[gl]=NeoChat
Name[he]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
@@ -59,6 +60,7 @@ GenericName[eu]=Matrix bezeroa
GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix »
GenericName[gl]=Cliente de Matrix
GenericName[he]=לקוח Matrix
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
@@ -99,6 +101,7 @@ Comment[eu]=Matrix protokolorako bezeroa
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[gl]=Cliente para o protocolo Matrix.
Comment[he]=לקוח לפרוטוקול Matrix
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix

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

5850
po/gl/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

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Slovenian "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Uporabniški priročnik za NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>Stran z navodili za NeoChat.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01.11.2022</date>
<releaseinfo
>22,09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Odjemalec za interakcijo s protokolom za matrično sporočanje</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Opis</title>
<para
><command
>neochat</command
> je aplikacija za klepet za matrični protokol, ki deluje na namizju in mobilni napravi. </para>
</refsect1>
<refsect1 id="options"
><title
>Možnosti</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>Uri matrike za uporabnika ali sobo. npr. matrix:u/user:example.org in matrix:r/root:example.org. Tako bo NeoChat poskušal odpreti dano sobo ali pogovor. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Poročanje o napakah</title>
<para
>Napake in zahteve po funkcijah lahko prijavite na <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi? product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Poglej tudi</title>
<simplelist>
<member
>Seznam pogostih vprašanj o Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Avtorske pravice</title>
<para
>Avtorske pravice &copy; 2020-2022 Tobias Fella </para>
<para
>Avtorske pravice &copy; 2020-2022 Carl Schwan </para>
<para
>Licenca: GNU General Public različica 3 ali novejša &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0 .html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})

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

158
snapcraft.yaml Normal file
View File

@@ -0,0 +1,158 @@
# SPDX-FileCopyrightText: 2024 Scarlett Moore <sgmoore@kde.org>
#
# SPDX-License-Identifier: CC0-1.0
---
name: neochat
base: core22
adopt-info: neochat
grade: stable
confinement: strict
apps:
neochat:
extensions:
- kde-neon-6
command: usr/bin/neochat
common-id: org.kde.neochat
desktop: usr/share/applications/org.kde.neochat.desktop
plugs:
- home
- removable-media
- audio-playback
- unity7
- network
- network-bind
- network-manager-observe
- password-manager-service
- accounts-service
compression: lzo
slots:
session-dbus-interface:
interface: dbus
name: org.kde.neochat
bus: session
parts:
olm:
source: https://gitlab.matrix.org/matrix-org/olm.git
source-depth: 1
source-tag: '3.2.12'
plugin: cmake
cmake-parameters:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/usr
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libsecret:
source: https://gitlab.gnome.org/GNOME/libsecret.git
source-tag: '0.21.4'
source-depth: 1
plugin: meson
meson-parameters:
- --prefix=/usr
- -Doptimization=3
- -Ddebug=true
- -Dmanpage=false
- -Dvapi=false
- -Dintrospection=false
- -Dcrypto=disabled
- -Dgtk_doc=false
build-packages:
- meson
- libglib2.0-dev
- libgcrypt20-dev
prime:
- -usr/include
- -usr/lib/*/pkgconfig
qtkeychain:
after: [libsecret]
source: https://github.com/frankosterfeld/qtkeychain.git
source-tag: 0.14.3
source-depth: 1
plugin: cmake
build-environment:
- PKG_CONFIG_PATH: $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TRANSLATIONS=NO
- -DBUILD_WITH_QT6=ON
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
libquotient:
after:
- olm
- qtkeychain
source: https://github.com/quotient-im/libQuotient.git
source-tag: 0.8.2
source-depth: 1
plugin: cmake
build-packages:
- libssl-dev
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
- -DQuotient_ENABLE_E2EE=ON
- -DBUILD_WITH_QT6=ON
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
kquickimageeditor:
source: https://invent.kde.org/libraries/kquickimageeditor.git
source-tag: 'v0.3.0'
source-depth: 1
plugin: cmake
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_WITH_QT6=ON
- -DBUILD_TESTING=OFF
prime:
- -usr/include
- -usr/lib/*/pkgconfig
- -usr/lib/*/cmake
neochat:
after:
- qtkeychain
- libquotient
- kquickimageeditor
parse-info:
- usr/share/metainfo/org.kde.neochat.appdata.xml
source: https://invent.kde.org/network/neochat.git
source-tag: 'v24.08.1'
plugin: cmake
build-packages:
- cmark
- libcmark-dev
- libsqlite3-dev
- libvulkan-dev
- libxkbcommon-dev
- libicu-dev
- libpulse0
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
prime:
- -usr/share/man
deps:
after: [neochat]
plugin: nil
stage-packages:
- libcmark0.30.2
prime:
- usr/lib/*/libcmark.so*

View File

@@ -134,6 +134,8 @@ add_library(neochat STATIC
jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatgetcommonroomsjob.h
mediasizehelper.cpp
mediasizehelper.h
eventhandler.cpp
@@ -188,6 +190,12 @@ add_library(neochat STATIC
threepidbindhelper.h
models/readmarkermodel.cpp
models/readmarkermodel.h
neochatroommember.cpp
neochatroommember.h
models/threadmodel.cpp
models/threadmodel.h
enums/messagetype.h
messagecomponent.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -232,15 +240,10 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EmojiSas.qml
qml/ConfirmDeactivateAccountDialog.qml
qml/VerificationCanceled.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/RoomSearchPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -284,6 +287,8 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -301,8 +306,12 @@ add_subdirectory(devtools)
add_subdirectory(login)
add_subdirectory(chatbar)
if(UNIX)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES
qml/ShareAction.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
)
else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
QT_RESOURCE_ALIAS qml/ShareAction.qml
@@ -380,7 +389,7 @@ if(NOT ANDROID)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11=1)
target_sources(neochat PRIVATE runner.cpp)
@@ -421,7 +430,7 @@ if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PUBLIC NEOCHAT_FLATPAK)
@@ -436,8 +445,11 @@ if(ANDROID)
target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
"arrow-down"
"arrow-up"
"arrow-down-symbolic"
"arrow-up-symbolic"
"arrow-up-double-symbolic"
"arrow-left-symbolic"
"arrow-right-symbolic"
"checkmark"
"help-about"
"im-user"
@@ -446,6 +458,7 @@ if(ANDROID)
"mail-attachment"
"dialog-cancel"
"preferences-desktop-emoticons"
"preferences-security"
"document-open"
"document-save"
"document-send"

View File

@@ -1,70 +1,48 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "actionshandler.h"
#include <Quotient/csapi/joining.h>
#include <Quotient/events/roommemberevent.h>
#include <cmark.h>
#include <KLocalizedString>
#include <QStringBuilder>
#include "chatbarcache.h"
#include "models/actionsmodel.h"
#include "neochatconfig.h"
#include "texthandler.h"
using namespace Quotient;
using namespace Qt::StringLiterals;
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
void ActionsHandler::handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache)
{
}
NeoChatRoom *ActionsHandler::room() const
{
return m_room;
}
void ActionsHandler::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
{
if (!m_room || !chatBarCache) {
if (room == nullptr || chatBarCache == nullptr) {
qWarning() << "ActionsHandler::handleMessageEvent - called with m_room and/or chatBarCache set to nullptr.";
return;
}
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());
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);
const auto handledText = handleMentions(chatBarCache);
const auto result = handleQuickEdit(room, handledText);
if (!result) {
handleMessage(room, handledText, chatBarCache);
}
}
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
QString ActionsHandler::handleMentions(ChatBarCache *chatBarCache)
{
const auto mentions = chatBarCache->mentions();
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
auto handledText = chatBarCache->text();
for (const auto &mention : *mentions) {
if (mention.text.isEmpty() || mention.id.isEmpty()) {
continue;
@@ -78,48 +56,64 @@ QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *ment
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
bool ActionsHandler::handleQuickEdit(NeoChatRoom *room, const QString &handledText)
{
Q_ASSERT(m_room);
if (room == nullptr) {
return false;
}
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
auto match = sed.match(text);
auto match = sed.match(handledText);
if (match.hasMatch()) {
const QString regex = match.captured(1);
const QString replacement = match.captured(2).toHtmlEscaped();
const QString flags = match.captured(3);
for (auto it = m_room->messageEvents().crbegin(); it != m_room->messageEvents().crend(); it++) {
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
if (const auto event = eventCast<const RoomMessageEvent>(&**it)) {
if (event->senderId() == m_room->localMember().id() && event->hasTextContent()) {
if (event->senderId() == room->localMember().id() && event->hasTextContent()) {
QString originalString;
if (event->content()) {
#if Quotient_VERSION_MINOR > 8
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content().get())->body;
#else
originalString = static_cast<const Quotient::EventContent::TextContent *>(event->content())->body;
#endif
} else {
originalString = event->plainBody();
}
if (flags == "/g"_ls) {
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
if (flags == "/g"_L1) {
room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
} else {
m_room->postHtmlMessage(handledText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
event->id());
room->postHtmlMessage(handledText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
event->id());
}
return;
return true;
}
}
}
}
}
return false;
}
void ActionsHandler::handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache)
{
if (room == nullptr) {
return;
}
auto messageType = RoomMessageEvent::MsgType::Text;
if (handledText.startsWith(QLatin1Char('/'))) {
for (const auto &action : ActionsModel::instance().allActions()) {
if (handledText.indexOf(action.prefix) == 1
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache);
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), room, chatBarCache);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
@@ -136,35 +130,11 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha
textHandler.setData(handledText);
handledText = textHandler.handleSendText();
if (handledText.count("<p>"_ls) == 1 && handledText.count("</p>"_ls) == 1) {
handledText.remove("<p>"_ls);
handledText.remove("</p>"_ls);
}
if (handledText.length() == 0) {
return;
}
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
void ActionsHandler::checkEffects(const QString &text)
{
std::optional<QString> effect = std::nullopt;
if (text.contains(QStringLiteral("\u2744"))) {
effect = QLatin1String("snowflake");
} else if (text.contains(QStringLiteral("\u1F386"))) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u2F387"))) {
effect = QLatin1String("fireworks");
} else if (text.contains(QStringLiteral("\u1F389"))) {
effect = QLatin1String("confetti");
} else if (text.contains(QStringLiteral("\u1F38A"))) {
effect = QLatin1String("confetti");
}
if (effect.has_value()) {
Q_EMIT showEffect(*effect);
}
room->postMessage(chatBarCache->text(), handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
#include "moc_actionshandler.cpp"

View File

@@ -1,22 +1,18 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/roommessageevent.h>
#include "chatbarcache.h"
#include "neochatroom.h"
#include <QString>
class ChatBarCache;
class NeoChatRoom;
/**
* @class ActionsHandler
*
* This class handles chat messages ready for posting to a room.
* This class contains functions to handle chat messages ready for posting to a room.
*
* Everything that needs to be done to prepare the message for posting in a room
* including:
@@ -31,36 +27,17 @@ class NeoChatRoom;
*
* @sa ActionsModel, NeoChatRoom
*/
class ActionsHandler : public QObject
class ActionsHandler
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room that messages will be sent to.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
explicit ActionsHandler(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
void roomChanged();
void showEffect(const QString &effect);
public Q_SLOTS:
/**
* @brief Pre-process text and send message event.
*/
void handleMessageEvent(ChatBarCache *chatBarCache);
static void handleMessageEvent(NeoChatRoom *room, ChatBarCache *chatBarCache);
private:
QPointer<NeoChatRoom> m_room;
void checkEffects(const QString &text);
static QString handleMentions(ChatBarCache *chatBarCache);
static bool handleQuickEdit(NeoChatRoom *room, const QString &handledText);
QString handleMentions(QString handledText, QList<Mention> *mentions);
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
static void handleMessage(NeoChatRoom *room, QString handledText, ChatBarCache *chatBarCache);
};

View File

@@ -11,7 +11,6 @@ ecm_add_qml_module(chatbar GENERATE_PLUGIN_SOURCE
CompletionMenu.qml
EmojiDelegate.qml
EmojiGrid.qml
ReplyPane.qml
PieProgressBar.qml
EmojiPicker.qml
EmojiDialog.qml

View File

@@ -53,14 +53,6 @@ QQC2.Control {
}
}
/**
* @brief The ActionsHandler object to use.
*
* This is expected to have the correct room set otherwise messages will be sent
* to the wrong room.
*/
required property ActionsHandler actionsHandler
/**
* @brief The list of actions in the ChatBar.
*
@@ -175,6 +167,7 @@ QQC2.Control {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
Layout.preferredHeight: active ? item.implicitHeight : 0
active: visible
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
@@ -219,7 +212,7 @@ QQC2.Control {
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
if (!repeatTimer.running && NeoChatConfig.typingNotifications) {
var textExists = text.length > 0;
root.currentRoom.sendTypingNotification(textExists);
textExists ? repeatTimer.start() : repeatTimer.stop();
@@ -353,23 +346,40 @@ QQC2.Control {
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
maxWidth: NeoChatConfig.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.width
}
Component {
id: replyPane
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarUrl
text: _private.chatBarCache.relationMessage
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent
replyEventId: _private.chatBarCache.replyId
replyAuthor: _private.chatBarCache.relationAuthor
replyContentModel: _private.chatBarCache.relationEventContentModel
maxContentWidth: paneLoader.item.width
}
QQC2.Button {
id: cancelButton
onCancel: {
_private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
anchors.top: parent.top
anchors.right: parent.right
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
_private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}
@@ -391,11 +401,11 @@ QQC2.Control {
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
function postMessage() {
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
_private.chatBarCache.postMessage();
repeatTimer.stop();
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
_private.chatBarCache.clearRelations();
messageSent();
}

View File

@@ -33,7 +33,7 @@ QQC2.ItemDelegate {
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5
source: "arrow-down"
source: "arrow-down-symbolic"
anchors.bottom: parent.bottom
anchors.right: parent.right
visible: root.showTones

View File

@@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
RowLayout {
id: root
property string userName
property color userColor
property url userAvatar: ""
property var text
signal cancel
Rectangle {
id: verticalBorder
Layout.fillHeight: true
implicitWidth: Kirigami.Units.smallSpacing
color: userColor
}
ColumnLayout {
RowLayout {
KirigamiComponents.Avatar {
id: replyAvatar
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
source: userAvatar
name: userName
color: userColor
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
color: userColor
text: userName
elide: Text.ElideRight
}
}
QQC2.TextArea {
id: textArea
Layout.fillWidth: true
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
text: "<style> a{color:" + Kirigami.Theme.linkColor + ";}.user-pill{}</style>" + replyTextMetrics.elidedText
selectByMouse: true
selectByKeyboard: true
readOnly: true
wrapMode: TextEdit.Wrap
textFormat: TextEdit.RichText
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
TextMetrics {
id: replyTextMetrics
text: root.text
font: textArea.font
elide: Qt.ElideRight
elideWidth: textArea.width * 2 - Kirigami.Units.smallSpacing * 2
}
}
}
QQC2.ToolButton {
id: cancelButton
Layout.alignment: Qt.AlignVCenter
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: {
root.cancel();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}

View File

@@ -5,6 +5,7 @@
#include <Quotient/roommember.h>
#include "actionshandler.h"
#include "chatdocumenthandler.h"
#include "eventhandler.h"
#include "neochatroom.h"
@@ -53,6 +54,7 @@ void ChatBarCache::setReplyId(const QString &replyId)
m_relationType = Reply;
}
m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT attachmentPathChanged();
}
@@ -82,11 +84,12 @@ void ChatBarCache::setEditId(const QString &editId)
m_relationType = Edit;
}
m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT attachmentPathChanged();
}
Quotient::RoomMember ChatBarCache::relationUser() const
Quotient::RoomMember ChatBarCache::relationAuthor() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
@@ -119,12 +122,33 @@ QString ChatBarCache::relationMessage() const
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
EventHandler eventhandler(room, &**event);
return eventhandler.getMarkdownBody();
return EventHandler::markdownBody(&**event);
}
return {};
}
MessageContentModel *ChatBarCache::relationEventContentModel()
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return nullptr;
}
if (m_relationId.isEmpty()) {
return nullptr;
}
if (m_relationContentModel != nullptr) {
return m_relationContentModel;
}
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 nullptr;
}
m_relationContentModel = new MessageContentModel(room, m_relationId, true);
return m_relationContentModel;
}
bool ChatBarCache::isThreaded() const
{
return !m_threadId.isEmpty();
@@ -140,8 +164,8 @@ void ChatBarCache::setThreadId(const QString &threadId)
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
const auto oldThreadId = std::exchange(m_threadId, threadId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
}
QString ChatBarCache::attachmentPath() const
@@ -157,10 +181,22 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
m_attachmentPath = attachmentPath;
m_relationType = None;
const auto oldEventId = std::exchange(m_relationId, QString());
delete m_relationContentModel;
Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
}
void ChatBarCache::clearRelations()
{
const auto oldEventId = std::exchange(m_relationId, QString());
const auto oldThreadId = std::exchange(m_threadId, QString());
m_attachmentPath = QString();
delete m_relationContentModel;
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT attachmentPathChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;
@@ -224,4 +260,15 @@ void ChatBarCache::setSavedText(const QString &savedText)
m_savedText = savedText;
}
void ChatBarCache::postMessage()
{
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;
}
ActionsHandler::handleMessageEvent(room, this);
}
#include "moc_chatbarcache.cpp"

View File

@@ -8,6 +8,8 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "models/messagecontentmodel.h"
class ChatDocumentHandler;
namespace Quotient
@@ -33,7 +35,7 @@ struct Mention {
* 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.
* necessarily have to use the ChatBar component, e.g. ChatBarComponent.
*
* 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
@@ -43,7 +45,7 @@ struct Mention {
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, MessageEditComponent, NeoChatRoom
* @sa ChatBar, ChatBarComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
@@ -100,7 +102,7 @@ class ChatBarCache : public QObject
*
* @sa Quotient::RoomMember
*/
Q_PROPERTY(Quotient::RoomMember relationUser READ relationUser NOTIFY relationIdChanged)
Q_PROPERTY(Quotient::RoomMember relationAuthor READ relationAuthor NOTIFY relationIdChanged)
/**
* @brief The content of the related message.
@@ -109,6 +111,13 @@ class ChatBarCache : public QObject
*/
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief The MessageContentModel for the related message.
*
* Will be nullptr if no related message.
*/
Q_PROPERTY(MessageContentModel *relationEventContentModel READ relationEventContentModel NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is replying in a thread.
*/
@@ -154,9 +163,10 @@ public:
QString editId() const;
void setEditId(const QString &editId);
Quotient::RoomMember relationUser() const;
Quotient::RoomMember relationAuthor() const;
QString relationMessage() const;
MessageContentModel *relationEventContentModel();
bool isThreaded() const;
QString threadId() const;
@@ -165,6 +175,13 @@ public:
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Clear all relations in the cache.
*
* This includes relation ID, thread root ID and attachment path.
*/
Q_INVOKABLE void clearRelations();
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
@@ -185,10 +202,15 @@ public:
*/
void setSavedText(const QString &savedText);
/**
* @brief Post the contents of the cache as a message in the room.
*/
Q_INVOKABLE void postMessage();
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
void threadIdChanged();
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
void attachmentPathChanged();
private:
@@ -199,4 +221,6 @@ private:
QString m_attachmentPath = QString();
QList<Mention> m_mentions;
QString m_savedText;
QPointer<MessageContentModel> m_relationContentModel;
};

View File

@@ -26,23 +26,9 @@ void ColorSchemer::apply(int idx)
c->activateScheme(c->model()->index(idx, 0));
}
void ColorSchemer::apply(const QString &name)
int ColorSchemer::indexForCurrentScheme()
{
c->activateScheme(c->indexForScheme(name));
}
int ColorSchemer::indexForScheme(const QString &name) const
{
auto index = c->indexForScheme(name).row();
if (index == -1) {
index = 0;
}
return index;
}
QString ColorSchemer::nameForIndex(int index) const
{
return c->model()->data(c->model()->index(index, 0), Qt::DisplayRole).toString();
return c->indexForSchemeId(c->activeSchemeId()).row();
}
#include "moc_colorschemer.cpp"

View File

@@ -44,21 +44,11 @@ public:
Q_INVOKABLE void apply(int idx);
/**
* @brief Activates the KColorScheme with the given name.
* @brief Get the row for the current color scheme.
*
* @sa KColorScheme
*/
Q_INVOKABLE void apply(const QString &name);
/**
* @brief Returns the index for the scheme with the given name.
*/
Q_INVOKABLE int indexForScheme(const QString &name) const;
/**
* @brief Returns the name for the scheme with the given index.
*/
Q_INVOKABLE QString nameForIndex(int index) const;
Q_INVOKABLE int indexForCurrentScheme();
private:
KColorSchemeManager *c;

View File

@@ -13,7 +13,6 @@
#include <signal.h>
#include <Quotient/accountregistry.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
@@ -64,7 +63,11 @@ Controller::Controller(QObject *parent)
});
} else {
auto c = new NeoChatConnection(this);
#if Quotient_VERSION_MINOR > 8
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234"));
#else
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
#endif
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
@@ -166,6 +169,10 @@ void Controller::addConnection(NeoChatConnection *c)
dropConnection(c);
});
connect(c, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
connect(c, &NeoChatConnection::syncDone, this, [this, c]() {
m_notificationsManager.handleNotifications(c);
});
connect(c, &NeoChatConnection::showInviteNotification, &m_notificationsManager, &NotificationsManager::postInviteNotification);
c->sync();
@@ -176,6 +183,8 @@ void Controller::dropConnection(NeoChatConnection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
c->disconnect(this);
c->disconnect(&m_notificationsManager);
m_accountRegistry.drop(c);
Q_EMIT connectionDropped(c);
}
@@ -202,15 +211,33 @@ void Controller::invokeLogin()
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
} else {
connect(
connection->allRooms()[0],
&Room::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
}
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
#if Quotient_VERSION_MINOR > 8
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
#else
connection->assumeIdentity(account.userId(), accessToken);
#endif
});
}
}
@@ -230,17 +257,17 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QStrin
switch (job->error()) {
case QKeychain::EntryNotFound:
Q_EMIT errorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
Q_EMIT errorOccured(i18n("Access token wasn't found: Maybe it was deleted?"));
break;
case QKeychain::AccessDeniedByUser:
case QKeychain::AccessDenied:
Q_EMIT errorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
Q_EMIT errorOccured(i18n("Access to keychain was denied: Please allow NeoChat to read the access token"));
break;
case QKeychain::NoBackendAvailable:
Q_EMIT errorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
Q_EMIT errorOccured(i18n("No keychain available: Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
break;
case QKeychain::OtherError:
Q_EMIT errorOccured(i18n("Unable to read access token"), job->errorString());
Q_EMIT errorOccured(i18n("Unable to read access token: %1", job->errorString()));
break;
default:
break;
@@ -251,23 +278,19 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QStrin
return job;
}
bool Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
void Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
{
qDebug() << "Save the access token to the keychain for " << userId;
QKeychain::WritePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(userId);
job.setBinaryData(accessToken);
QEventLoop loop;
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
return false;
}
return true;
auto job = new QKeychain::WritePasswordJob(qAppName());
job->setAutoDelete(true);
job->setKey(userId);
job->setBinaryData(accessToken);
connect(job, &QKeychain::WritePasswordJob::finished, this, [job]() {
if (job->error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job->errorString());
}
});
job->start();
}
bool Controller::supportSystemTray() const
@@ -309,11 +332,18 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
return;
}
if (m_connection != nullptr) {
m_connection->disconnect(this);
m_connection->disconnect(&m_notificationsManager);
}
m_connection = connection;
if (m_connection != nullptr) {
m_connection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(m_connection, m_connection->badgeNotificationCount());
connect(m_connection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
}
Q_EMIT activeConnectionChanged(m_connection);
@@ -328,7 +358,7 @@ void Controller::listenForNotifications()
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
NotificationsManager::instance().postPushNotification(data);
instance().m_notificationsManager.postPushNotification(data);
timer->stop();
});
@@ -340,6 +370,11 @@ void Controller::listenForNotifications()
#endif
}
void Controller::clearInvitationNotification(const QString &roomId)
{
m_notificationsManager.clearInvitationNotification(roomId);
}
void Controller::updateBadgeNotificationCount(NeoChatConnection *connection, int count)
{
if (connection == m_connection) {
@@ -409,11 +444,28 @@ void Controller::removeConnection(const QString &userId)
bool Controller::csSupported() const
{
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 8
return true;
#else
return false;
#endif
}
void Controller::revertToDefaultConfig()
{
const auto config = NeoChatConfig::self();
config->setDefaults();
config->save();
}
bool Controller::isImageShown(const QString &eventId)
{
return m_shownImages.contains(eventId);
}
void Controller::markImageShown(const QString &eventId)
{
m_shownImages.append(eventId);
}
#include "moc_controller.cpp"

View File

@@ -7,6 +7,7 @@
#include <QQmlEngine>
#include "neochatconnection.h"
#include "notificationsmanager.h"
#include <Quotient/accountregistry.h>
class TrayIcon;
@@ -53,16 +54,6 @@ class Controller : public QObject
Q_PROPERTY(bool csSupported READ csSupported CONSTANT)
public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
};
Q_ENUM(MessageType)
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
@@ -86,7 +77,7 @@ public:
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
void saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
[[nodiscard]] bool supportSystemTray() const;
@@ -98,6 +89,13 @@ public:
*/
static void listenForNotifications();
/**
* @brief Clear an existing invite notification for the given room.
*
* Nothing happens if the given room doesn't have an invite notification.
*/
Q_INVOKABLE void clearInvitationNotification(const QString &roomId);
Q_INVOKABLE QString loadFileContent(const QString &path) const;
Quotient::AccountRegistry &accounts();
@@ -108,6 +106,17 @@ public:
bool csSupported() const;
/**
* @brief Revert all configuration values to their default.
*
* The parameters along with their defaults are specified in the config file
* neochatconfig.kcfg.
*/
Q_INVOKABLE void revertToDefaultConfig();
Q_INVOKABLE bool isImageShown(const QString &eventId);
Q_INVOKABLE void markImageShown(const QString &eventId);
private:
explicit Controller(QObject *parent = nullptr);
@@ -116,13 +125,13 @@ private:
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const QString &account);
void loadSettings();
void saveSettings() const;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
QMap<QString, QPointer<NeoChatConnection>> m_connectionsLoading;
QString m_endpoint;
QStringList m_shownImages;
NotificationsManager m_notificationsManager;
private Q_SLOTS:
void invokeLogin();
@@ -130,10 +139,12 @@ private Q_SLOTS:
void updateBadgeNotificationCount(NeoChatConnection *connection, int count);
Q_SIGNALS:
void errorOccured(const QString &error, const QString &detail);
/**
* @brief Request a error message be shown to the user.
*/
void errorOccured(const QString &error);
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void activeConnectionChanged(NeoChatConnection *connection);
void accountsLoadingChanged();
void showMessage(MessageType messageType, const QString &message);
};

View File

@@ -17,25 +17,25 @@ FormCard.FormCardPage {
FormCard.FormCheckDelegate {
text: i18nc("@option:check", "Show hidden events in the timeline")
checked: Config.showAllEvents
checked: NeoChatConfig.showAllEvents
onToggled: Config.showAllEvents = checked
onToggled: NeoChatConfig.showAllEvents = checked
}
FormCard.FormCheckDelegate {
id: roomAccountDataVisibleCheck
text: i18nc("@option:check Enable the matrix 'threads' feature", "Always allow device verification")
description: i18n("Allow the user to start a verification session with devices that were already verified")
checked: Config.alwaysVerifyDevice
checked: NeoChatConfig.alwaysVerifyDevice
onToggled: Config.alwaysVerifyDevice = checked
onToggled: NeoChatConfig.alwaysVerifyDevice = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check", "Show focus in window header")
checked: Config.windowTitleFocus
checked: NeoChatConfig.windowTitleFocus
onToggled: {
Config.windowTitleFocus = checked;
Config.save();
NeoChatConfig.windowTitleFocus = checked;
NeoChatConfig.save();
}
}
}

View File

@@ -18,23 +18,23 @@ FormCard.FormCardPage {
FormCard.FormCheckDelegate {
id: roomAccountDataVisibleCheck
text: i18nc("@option:check Enable the matrix 'threads' feature", "Threads")
checked: Config.threads
checked: NeoChatConfig.threads
onToggled: Config.threads = checked
onToggled: NeoChatConfig.threads = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
checked: Config.secretBackup
checked: NeoChatConfig.secretBackup
onToggled: Config.secretBackup = checked
onToggled: NeoChatConfig.secretBackup = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
checked: Config.phone3PId
checked: NeoChatConfig.phone3PId
onToggled: {
Config.phone3PId = checked
Config.save();
NeoChatConfig.phone3PId = checked
NeoChatConfig.save();
}
}
}

View File

@@ -5,5 +5,5 @@
#include "models/emojimodel.h"
QMultiHash<QString, QVariant> EmojiTones::_tones = {
#include "emojitones_data.h"
// #include "emojitones_data.h"
};

View File

@@ -38,6 +38,8 @@ public:
ReadMarker, /**< The local user read marker. */
Loading, /**< A delegate to tell the user more messages are being loaded. */
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
Predecessor, /**< A delegate to show a room predecessor. */
Successor, /**< A delegate to show a room successor. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -50,7 +50,7 @@ public:
Reply, /**< A component to show a replied-to message. */
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
Edit, /**< A text edit for editing a message. */
ChatBar, /**< A text edit for editing a message. */
Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Other, /**< Anything that cannot be classified as another type. */

31
src/enums/messagetype.h Normal file
View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class MessageType
*
* This class is designed to define the MessageType enumeration.
*/
class MessageType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief The types of messages that can be shown.
*/
enum Type {
Information = 0, /**< Info message, typically highlight color. */
Positive, /**< Positive message, typically green. */
Warning, /**< Warning message, typically amber. */
Error, /**< Error message, typically red. */
};
Q_ENUM(Type);
};

File diff suppressed because it is too large Load Diff

View File

@@ -3,24 +3,22 @@
#pragma once
#include <QObject>
#include <KFormat>
#include <Quotient/eventitem.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/messagecomponenttype.h"
#include <QDateTime>
#include <QString>
#include <Quotient/events/eventcontent.h>
namespace Quotient
{
namespace EventContent
{
class FileInfo;
}
class RoomEvent;
class RoomMember;
class RoomMessageEvent;
}
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
/**
* @class EventHandler
@@ -38,40 +36,19 @@ class ReactionModel;
*/
class EventHandler
{
Q_GADGET
public:
EventHandler(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the event.
* @brief Return the ID of the event.
*
* Returns the transaction ID if the Matrix ID is empty, which may be the case
* for a pending event.
*/
QString getId() const;
/**
* @brief The MessageComponentType to use to visualise the main event content.
*/
MessageComponentType::Type messageComponentType() const;
/**
* @brief Get the author of the event in context of the room.
*
* An empty Quotient::RoomMember will be returned if the EventHandler hasn't had
* the room or event initialised.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a Quotient::RoomMember object for the user.
*
* @sa Quotient::RoomMember
*/
Quotient::RoomMember getAuthor(bool isPending = false) const;
static QString id(const Quotient::RoomEvent *event);
/**
* @brief Get the display name of the event author.
*
* This method is separate from getAuthor() and special in that it will return
* This method is special in that it will return
* the old display name of the author if the current event is one that caused it
* to change. This allows for scenarios where the UI wishes to notify that a
* user's display name has changed and what it changed from.
@@ -79,7 +56,7 @@ public:
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString getAuthorDisplayName(bool isPending = false) const;
static QString authorDisplayName(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
/**
* @brief Get the display name of the event author but with any newlines removed.
@@ -90,12 +67,12 @@ public:
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString singleLineAuthorDisplayname(bool isPending = false) const;
static QString singleLineAuthorDisplayname(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
/**
* @brief Return a QDateTime object for the event timestamp.
*/
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
static QDateTime time(const Quotient::RoomEvent *event, bool isPending = false, QDateTime lastUpdated = {});
/**
* @brief Return a QString for the event timestamp.
@@ -111,14 +88,32 @@ public:
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
static QString timeString(const Quotient::RoomEvent *event,
bool relative,
QLocale::FormatType format = QLocale::ShortFormat,
bool isPending = false,
QDateTime lastUpdated = {});
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param format the format to use as a string.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending = false, const QDateTime &lastUpdated = {});
/**
* @brief Whether the event should be highlighted in the timeline.
*
* @note Messages in direct chats are never highlighted.
*/
bool isHighlighted();
static bool isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Whether the event should be hidden in the timeline.
@@ -127,7 +122,7 @@ public:
* user has hidden all state events or if the sender has been ignored by the local
* user.
*/
bool isHidden();
static bool isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief The input format of the body in the message.
@@ -159,7 +154,7 @@ public:
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getRichBody(bool stripNewlines = false) const;
static QString richBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines = false);
/**
* @brief Output a string for the message content ready for display in a plain text field.
@@ -175,14 +170,14 @@ public:
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getPlainBody(bool stripNewlines = false) const;
static QString plainBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines = false);
/**
* @brief Output the original body for the message content, useful for editing the original message.
*
* The event type must be a room message event.
*/
QString getMarkdownBody() const;
static QString markdownBody(const Quotient::RoomEvent *event);
/**
* @brief Output a generic string for the message content ready for display.
@@ -195,9 +190,9 @@ public:
* E.g. For a message the text will be:
* "sent a message"
*
* @sa getRichBody(), getPlainBody()
* @sa richBody(), plainBody()
*/
QString getGenericBody() const;
static QString genericBody(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Output a string for the event to be used as a RoomList subtitle.
@@ -205,7 +200,7 @@ public:
* The output includes the username followed by the plain message, all with no
* line breaks.
*/
QString subtitleText() const;
static QString subtitleText(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Return the media info for the event.
@@ -223,22 +218,21 @@ public:
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
* - isSticker - Whether the image is a sticker or not
*/
QVariantMap getMediaInfo() const;
static QVariantMap mediaInfo(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Whether the event is a reply to another in the timeline.
*
* @param showFallbacks whether message that have is_falling_back set true should
* show the fallback reply. Leave true for non-threaded
* timelines.
*/
bool hasReply() const;
static bool hasReply(const Quotient::RoomEvent *event, bool showFallbacks = true);
/**
* @brief Return the Matrix ID of the event replied to.
*/
QString getReplyId() const;
/**
* @brief The MessageComponentType to use to visualise the reply content.
*/
MessageComponentType::Type replyMessageComponentType() const;
static QString replyId(const Quotient::RoomEvent *event);
/**
* @brief Get the author of the event replied to in context of the room.
@@ -253,73 +247,21 @@ public:
*
* @sa Quotient::RoomMember
*/
Quotient::RoomMember getReplyAuthor() const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a rich text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content of the event replied to ready
* for display in a plain text field.
*
* The output string is dependant upon the event type and the desired output format.
*
* For most messages this is the body content of the message. For media messages
* this will be the caption and for state events it will be a string specific
* to that event with some dynamic details about the event added.
*
* E.g. For a room topic state event the text will be:
* "set the topic to: <new topic text>"
*
* @param stripNewlines whether the output should have new lines in it.
*/
QString getReplyPlainBody(bool stripNewlines = false) const;
/**
* @brief Return the media info for the event replied to.
*
* An empty QVariantMap will be returned for any event that doesn't have any
* media info.
*
* @return This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
* - mimeIcon - The MIME icon name (should be image-xxx).
* - size - The file size in bytes.
* - width - The width in pixels of the audio media.
* - height - The height in pixels of the audio media.
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
* - isSticker - Whether the image is a sticker or not
*/
QVariantMap getReplyMediaInfo() const;
static Quotient::RoomMember replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event);
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
bool isThreaded() const;
static bool isThreaded(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
QString threadRoot() const;
static QString threadRoot(const Quotient::RoomEvent *event);
/**
* @brief Return the latitude for the event.
@@ -327,7 +269,7 @@ public:
* Returns -100.0 if the event doesn't have a location (latitudes are in the
* range -90deg to +90deg so -100 is out of range).
*/
float getLatitude() const;
static float latitude(const Quotient::RoomEvent *event);
/**
* @brief Return the longitude for the event.
@@ -335,23 +277,26 @@ public:
* Returns -200.0 if the event doesn't have a location (latitudes are in the
* range -180deg to +180deg so -200 is out of range).
*/
float getLongitude() const;
static float longitude(const Quotient::RoomEvent *event);
/**
* @brief Return the type of location marker for the event.
*/
QString getLocationAssetType() const;
static QString locationAssetType(const Quotient::RoomEvent *event);
private:
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
static QString getBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines);
static QString getMessageBody(const NeoChatRoom *room, const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines);
KFormat m_format;
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
QVariantMap
getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false, bool isSticker = false) const;
static QVariantMap getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event);
QVariantMap static getMediaInfoFromFileInfo(const NeoChatRoom *room,
#if Quotient_VERSION_MINOR > 8
const Quotient::EventContent::FileContentBase *fileContent,
#else
const Quotient::EventContent::TypedBase *fileContent,
#endif
const QString &eventId,
bool isThumbnail = false,
bool isSticker = false);
static QVariantMap getMediaInfoFromTumbnail(const NeoChatRoom *room, const Quotient::EventContent::Thumbnail &thumbnail, const QString &eventId);
};

View File

@@ -10,22 +10,13 @@
#include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h>
#if Quotient_VERSION_MINOR > 8
#include <Quotient/keyimport.h>
#endif
#include "controller.h"
#include "neochatconfig.h"
struct ForeignConfig {
Q_GADGET
QML_FOREIGN(NeoChatConfig)
QML_NAMED_ELEMENT(Config)
QML_SINGLETON
public:
static NeoChatConfig *create(QQmlEngine *, QJSEngine *)
{
QQmlEngine::setObjectOwnership(NeoChatConfig::self(), QQmlEngine::CppOwnership);
return NeoChatConfig::self();
}
};
struct ForeignAccountRegistry {
Q_GADGET
QML_FOREIGN(Quotient::AccountRegistry)
@@ -52,8 +43,11 @@ struct ForeignSSSSHandler {
QML_NAMED_ELEMENT(SSSSHandler)
};
struct RoomMemberForeign {
#if Quotient_VERSION_MINOR > 8
struct ForeignKeyImport {
Q_GADGET
QML_FOREIGN(Quotient::RoomMember)
QML_NAMED_ELEMENT(RoomMember)
QML_SINGLETON
QML_FOREIGN(Quotient::KeyImport)
QML_NAMED_ELEMENT(KeyImport)
};
#endif

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatgetcommonroomsjob.h"
using namespace Quotient;
NeochatGetCommonRoomsJob::NeochatGetCommonRoomsJob(const QString &userId)
: BaseJob(HttpVerb::Get,
QStringLiteral("GetCommonRoomsJob"),
QStringLiteral("/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms").toLatin1(),
QUrlQuery({{QStringLiteral("user_id"), userId}}))
{
}

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
// TODO: Upstream to libQuotient
class NeochatGetCommonRoomsJob : public Quotient::BaseJob
{
public:
explicit NeochatGetCommonRoomsJob(const QString &userId);
};

View File

@@ -40,7 +40,7 @@ float LocationHelper::zoomToFit(const QRectF &r, float mapWidth, float mapHeight
const auto zy = std::log2((mapHeight / (p2.y() - p1.y())));
const auto z = std::min(zx, zy);
return std::clamp(z, 5.0, 18.0);
return z;
}
#include "moc_locationhelper.cpp"

View File

@@ -78,16 +78,14 @@ void LoginHelper::init()
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken());
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
m_connection = nullptr;
});
connect(m_connection, &Connection::networkError, this, [this](QString error, const QString &, int, int) {
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error)));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -95,14 +93,14 @@ void LoginHelper::init()
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
Q_EMIT loginErrorOccured(i18n("Login Failed: %1", error));
}
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::resolveError, this, [](QString error) {
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
connect(m_connection, &Connection::resolveError, this, [this](QString error) {
Q_EMIT m_connection->errorOccured(i18n("Network Error: %1", std::move(error)));
});
connect(

View File

@@ -130,7 +130,7 @@ Q_SIGNALS:
void loginFlowsChanged();
void ssoUrlChanged();
void connected();
void errorOccured(const QString &message);
void loginErrorOccured(const QString &message);
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();

View File

@@ -11,7 +11,7 @@ import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
import org.kde.neochat.settings
FormCard.FormCardPage {
Kirigami.Page {
id: root
property bool showExisting: false
@@ -23,202 +23,237 @@ FormCard.FormCardPage {
signal connectionChosen
title: i18n("Welcome")
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
header: QQC2.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
contentItem: ColumnLayout {
spacing: 0
Kirigami.Heading {
id: welcomeMessage
text: i18n("Welcome to NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
background: null
contentItem: RowLayout {
spacing: 0
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "edit-delete-remove"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
}
contentItem: Item {
ColumnLayout {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
FormCard.FormCard {
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
spacing: 0
Connections {
id: stepConnections
target: currentStep
Kirigami.Icon {
source: "org.kde.neochat"
Layout.alignment: Qt.AlignHCenter
implicitWidth: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
implicitHeight: Math.round(Kirigami.Units.iconSizes.huge * 1.5)
}
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
Kirigami.Heading {
id: welcomeMessage
text: i18n("NeoChat")
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormHeader {
id: existingAccountsHeader
title: i18nc("@title", "Continue with an existing account")
visible: (loadedAccounts.count > 0 || loadingAccounts.count > 0) && root._showExisting
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
visible: existingAccountsHeader.visible
maximumWidth: Kirigami.Units.gridUnit * 20
Repeater {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
text: model.userId
onClicked: {
Controller.activeConnection = model.connection;
root.connectionChosen();
}
}
}
Repeater {
id: loadingAccounts
model: Controller.accountsLoading
delegate: FormCard.AbstractFormDelegate {
id: loadingDelegate
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
background: null
contentItem: RowLayout {
spacing: 0
function onCloseDialog(): void {
root.closeDialog();
QQC2.Label {
Layout.fillWidth: true
text: i18nc("As in 'this account is still loading'", "%1 (loading)", modelData)
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
color: Kirigami.Theme.disabledTextColor
Accessible.ignored: true // base class sets this text on root already
}
QQC2.ToolButton {
text: i18nc("@action:button", "Log out of this account")
icon.name: "im-kick-user"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
enabled: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
}
FormCard.FormArrow {
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
direction: Qt.RightArrow
visible: root.background.visible
}
}
}
onCountChanged: {
if (loadingAccounts.count === 0 && loadedAccounts.count === 1 && showExisting) {
Controller.activeConnection = AccountRegistry.data(AccountRegistry.index(0, 0), 257);
root.connectionChosen();
}
}
}
}
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
FormCard.FormHeader {
title: i18nc("@title", "Log in or Create a New Account")
maximumWidth: Kirigami.Units.gridUnit * 20
}
FormCard.FormCard {
maximumWidth: Kirigami.Units.gridUnit * 20
Loader {
id: module
Layout.fillWidth: true
sourceComponent: Qt.createComponent('org.kde.neochat.login', root.initialStep)
Connections {
id: stepConnections
target: currentStep
function onProcessed(nextStep: string): void {
module.source = nextStep + ".qml";
root.currentStepString = nextStep;
headerMessage.text = "";
headerMessage.visible = false;
if (!module.item.noControls) {
module.item.forceActiveFocus();
} else {
continueButton.forceActiveFocus();
}
}
function onShowMessage(message: string): void {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
function onClearError(): void {
headerMessage.text = "";
headerMessage.visible = false;
}
function onCloseDialog(): void {
root.closeDialog();
}
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
Connections {
target: Registration
function onNextStepChanged() {
if (Registration.nextStep === "m.login.recaptcha") {
stepConnections.onProcessed("Captcha");
}
if (Registration.nextStep === "m.login.terms") {
stepConnections.onProcessed("Terms");
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
}
}
}
if (Registration.nextStep === "m.login.email.identity") {
stepConnections.onProcessed("Email");
}
if (Registration.nextStep === "loading") {
stepConnections.onProcessed("Loading");
Connections {
target: LoginHelper
function onLoginErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
}
}
}
}
Connections {
target: LoginHelper
function onErrorOccured(message) {
headerMessage.text = message;
headerMessage.visible = message.length > 0;
headerMessage.type = Kirigami.MessageType.Error;
FormCard.FormDelegateSeparator {
below: continueButton
visible: root.currentStep.nextAction
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right-symbolic"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left-symbolic"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
}
}
}
FormCard.FormDelegateSeparator {
below: continueButton
}
FormCard.FormButtonDelegate {
id: continueButton
text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue")
visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right"
enabled: root.currentStep.nextAction ? root.currentStep.nextAction.enabled : false
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Go back")
visible: root.currentStep.previousAction
onClicked: root.currentStep.previousAction.trigger()
icon.name: "arrow-left"
enabled: root.currentStep.previousAction ? root.currentStep.previousAction.enabled : false
}
}
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Open proxy settings")
icon.name: "settings-configure"
onClicked: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat.settings", "NetworkProxyPage"), {}, {
title: i18nc("@title:window", "Proxy Settings")
});
FormCard.FormCard {
Layout.topMargin: Kirigami.Units.largeSpacing * 2
maximumWidth: Kirigami.Units.gridUnit * 20
FormCard.FormButtonDelegate {
text: i18nc("@action:button", "Settings")
icon.name: "settings-configure"
onClicked: NeoChatSettingsView.open()
}
}
}
}

View File

@@ -48,7 +48,6 @@
#include "colorschemer.h"
#include "controller.h"
#include "logger.h"
#include "neochatconfig.h"
#include "roommanager.h"
#include "sharehandler.h"
#include "windowcontroller.h"
@@ -148,7 +147,7 @@ int main(int argc, char *argv[])
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QStringLiteral("https://carlschwan.eu/avatar.png"));
QUrl(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"));
@@ -185,9 +184,6 @@ int main(int argc, char *argv[])
#endif
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
@@ -307,7 +303,6 @@ int main(int argc, char *argv[])
QWindow *window = windowFromEngine(&engine);
WindowController::instance().setWindow(window);
WindowController::instance().restoreGeometry();
return app.exec();
}

View File

@@ -1,124 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#include "matriximageprovider.h"
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QThread>
#include <KLocalizedString>
#include "neochatconnection.h"
using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id))
, requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
requestedSize.setHeight(100);
requestedSize.setWidth(100);
}
if (mediaId.count(QLatin1Char('/')) != 1) {
if (mediaId.startsWith(QLatin1Char('/'))) {
mediaId = mediaId.mid(1);
} else {
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
Q_EMIT finished();
return;
}
}
mediaId = mediaId.split(QLatin1Char('?'))[0];
QImage cachedImage;
if (cachedImage.load(localFile)) {
image = cachedImage;
errorStr.clear();
Q_EMIT finished();
return;
}
if (!m_connection) {
qWarning() << "Current connection is null";
return;
}
// Execute a request on the main thread asynchronously
moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
if (!m_connection) {
return;
}
// Runs in the main thread, not QML thread
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);
}
void ThumbnailResponse::prepareResult()
{
Q_ASSERT(QThread::currentThread() == job->thread());
Q_ASSERT(job->error() != BaseJob::Pending);
{
QWriteLocker _(&lock);
if (job->error() == BaseJob::Success) {
image = job->thumbnail();
QString localPath = QFileInfo(localFile).absolutePath();
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
}
image.save(localFile);
errorStr.clear();
} else if (job->error() == BaseJob::Abandoned) {
errorStr = i18n("Image request has been cancelled");
// qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
} else {
errorStr = job->errorString();
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr;
}
job = nullptr;
}
Q_EMIT finished();
}
QQuickTextureFactory *ThumbnailResponse::textureFactory() const
{
QReadLocker _(&lock);
return QQuickTextureFactory::textureFactoryForImage(image);
}
QString ThumbnailResponse::errorString() const
{
QReadLocker _(&lock);
return errorStr;
}
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new ThumbnailResponse(id, requestedSize, m_connection);
}
#include "moc_matriximageprovider.cpp"

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QQuickAsyncImageProvider>
#include <Quotient/jobs/mediathumbnailjob.h>
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
* A QQuickImageResponse for an mxc image.
*
* @sa QQuickImageResponse
*/
class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
void startRequest();
void prepareResult();
private:
QString mediaId;
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
QPointer<NeoChatConnection> m_connection;
QImage image;
QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory *textureFactory() const override;
QString errorString() const override;
};
/**
* @class MatrixImageProvider
*
* A QQuickAsyncImageProvider for mxc images.
*
* @sa QQuickAsyncImageProvider
*/
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 = new MatrixImageProvider;
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:
QPointer<NeoChatConnection> m_connection;
MatrixImageProvider() = default;
};

22
src/messagecomponent.h Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include "enums/messagecomponenttype.h"
struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other;
QString content;
QVariantMap attributes;
int operator==(const MessageComponent &right) const
{
return type == right.type && content == right.content && attributes == right.attributes;
}
bool isEmpty() const
{
return type == MessageComponentType::Other;
}
};

View File

@@ -4,7 +4,7 @@
#include "actionsmodel.h"
#include "chatbarcache.h"
#include "controller.h"
#include "enums/messagetype.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "roommanager.h"
@@ -24,15 +24,14 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18n("Leaving this room."));
Q_EMIT room->showMessage(MessageType::Information, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
} else {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto leaving = room->connection()->room(text);
@@ -40,10 +39,10 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
leaving = room->connection()->roomByAlias(text);
}
if (leaving) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("Leaving room <roomname>.", "Leaving room %1.", text));
room->connection()->leaveRoom(leaving);
} else {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Room <roomname> not found", "Room %1 not found.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("Room <roomname> not found", "Room %1 not found.", text));
}
}
return QString();
@@ -51,7 +50,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen."));
Q_EMIT room->showMessage(MessageType::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text, room);
}
@@ -193,31 +192,29 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
const RoomMemberEvent *roomMemberEvent = room->currentState().get<RoomMemberEvent>(text);
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Invite) {
Q_EMIT Controller::instance().showMessage(Controller::Info,
i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
Q_EMIT room->showMessage(MessageType::Information,
i18nc("<user> is already invited to this room.", "%1 is already invited to this room.", text));
return QString();
}
if (roomMemberEvent && roomMemberEvent->membership() == Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is banned from this room.", "%1 is banned from this room.", text));
return QString();
}
if (room->localMember().id() == text) {
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room."));
Q_EMIT room->showMessage(MessageType::Positive, i18n("You are already in this room."));
return QString();
}
if (room->members().contains(room->member(text))) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
if (room->joinedMemberIds().contains(text)) {
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString();
}
room->inviteToRoom(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive,
i18nc("<username> was invited into this room", "%1 was invited into this room", text));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was invited into this room", "%1 was invited into this room", text));
return QString();
},
false,
@@ -231,9 +228,8 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
@@ -241,7 +237,7 @@ QList<ActionsModel::Action> actions{
RoomManager::instance().resolveResource(targetRoom->id());
return QString();
}
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("Joining room <roomname>.", "Joining room %1.", text));
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
@@ -258,9 +254,8 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(roomName);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
auto targetRoom = text.startsWith(QLatin1Char('!')) ? room->connection()->room(text) : room->connection()->roomByAlias(text);
@@ -268,7 +263,7 @@ QList<ActionsModel::Action> actions{
RoomManager::instance().resolveResource(targetRoom->id());
return QString();
}
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = dynamic_cast<NeoChatConnection *>(room->connection());
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
if (parts.length() >= 2) {
@@ -289,16 +284,15 @@ QList<ActionsModel::Action> actions{
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
Q_EMIT room->showMessage(MessageType::Error,
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 (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString();
}
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("Joining room <roomname>.", "Joining room %1.", text));
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
@@ -327,7 +321,7 @@ QList<ActionsModel::Action> actions{
QStringLiteral("nick"),
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("No new nickname provided, no changes will happen."));
Q_EMIT room->showMessage(MessageType::Error, i18n("No new nickname provided, no changes will happen."));
} else {
room->connection()->user()->rename(text);
}
@@ -361,16 +355,15 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
if (room->connection()->ignoredUsers().contains(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("<username> is already ignored.", "%1 is already ignored.", text));
return QString();
}
room->connection()->addToIgnoredUsers(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> is now ignored", "%1 is now ignored.", text));
return QString();
},
@@ -386,16 +379,15 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
if (!room->connection()->ignoredUsers().contains(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("<username> is not ignored.", "%1 is not ignored.", text));
return QString();
}
room->connection()->removeFromIgnoredUsers(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> is no longer ignored.", "%1 is no longer ignored.", text));
return QString();
},
false,
@@ -431,14 +423,13 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
auto state = room->currentState().get<RoomMemberEvent>(parts[0]);
if (state && state->membership() == Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info,
i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
Q_EMIT room->showMessage(MessageType::Information,
i18nc("<user> is already banned from this room.", "%1 is already banned from this room.", text));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
@@ -446,18 +437,17 @@ QList<ActionsModel::Action> actions{
return QString();
}
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to ban users from this room."));
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to ban users from this room."));
return QString();
}
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
Q_EMIT room->showMessage(
MessageType::Error,
i18nc("You are not allowed to ban <username> from this room.", "You are not allowed to ban %1 from this room.", parts[0]));
return QString();
}
room->ban(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT Controller::instance().showMessage(Controller::Positive,
i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was banned from this room.", "%1 was banned from this room.", parts[0]));
return QString();
},
false,
@@ -472,8 +462,7 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
Q_EMIT room->showMessage(MessageType::Error, i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", text));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
@@ -481,18 +470,16 @@ QList<ActionsModel::Action> actions{
return QString();
}
if (plEvent->ban() > plEvent->powerLevelForUser(room->localMember().id())) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to unban users from this room."));
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to unban users from this room."));
return QString();
}
auto state = room->currentState().get<RoomMemberEvent>(text);
if (state && state->membership() != Membership::Ban) {
Q_EMIT Controller::instance().showMessage(Controller::Info,
i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
Q_EMIT room->showMessage(MessageType::Information, i18nc("<user> is not banned from this room.", "%1 is not banned from this room.", text));
return QString();
}
room->unban(text);
Q_EMIT Controller::instance().showMessage(Controller::Positive,
i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was unbanned from this room.", "%1 was unbanned from this room.", text));
return QString();
},
@@ -509,16 +496,16 @@ QList<ActionsModel::Action> actions{
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(parts[0]);
if (!regexMatch.hasMatch()) {
Q_EMIT Controller::instance().showMessage(Controller::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
Q_EMIT room->showMessage(MessageType::Error,
i18nc("'<text>' does not look like a matrix id.", "'%1' does not look like a matrix id.", parts[0]));
return QString();
}
if (parts[0] == room->localMember().id()) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You cannot kick yourself from the room."));
Q_EMIT room->showMessage(MessageType::Error, i18n("You cannot kick yourself from the room."));
return QString();
}
if (!room->isMember(parts[0])) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
Q_EMIT room->showMessage(MessageType::Error, i18nc("<username> is not in this room", "%1 is not in this room.", parts[0]));
return QString();
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
@@ -527,18 +514,17 @@ QList<ActionsModel::Action> actions{
}
auto kick = plEvent->kick();
if (plEvent->powerLevelForUser(room->localMember().id()) < kick) {
Q_EMIT Controller::instance().showMessage(Controller::Error, i18n("You are not allowed to kick users from this room."));
Q_EMIT room->showMessage(MessageType::Error, i18n("You are not allowed to kick users from this room."));
return QString();
}
if (plEvent->powerLevelForUser(room->localMember().id()) <= plEvent->powerLevelForUser(parts[0])) {
Q_EMIT Controller::instance().showMessage(
Controller::Error,
Q_EMIT room->showMessage(
MessageType::Error,
i18nc("You are not allowed to kick <username> from this room", "You are not allowed to kick %1 from this room.", parts[0]));
return QString();
}
room->kickMember(parts[0], parts.size() > 1 ? parts.mid(1).join(QLatin1Char(' ')) : QString());
Q_EMIT Controller::instance().showMessage(Controller::Positive,
i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
Q_EMIT room->showMessage(MessageType::Positive, i18nc("<username> was kicked from this room.", "%1 was kicked from this room.", parts[0]));
return QString();
},
false,

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentmodel.h"
#include "eventhandler.h"
#include "neochatconfig.h"
#include <QImageReader>
@@ -9,9 +10,10 @@
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/qt_connection_util.h>
#include <KLocalizedString>
#include <Quotient/qt_connection_util.h>
#include <Kirigami/Platform/PlatformTheme>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
@@ -19,10 +21,7 @@
#endif
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "filetype.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -30,20 +29,8 @@
using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(event != nullptr ? event->id() : QString())
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
, m_event(loadEvent<RoomEvent>(event->fullJson()))
, m_isPending(isPending)
, m_isReply(isReply)
{
initializeModel();
}
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(eventId)
, m_isPending(isPending)
@@ -55,78 +42,79 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event
void MessageContentModel::initializeModel()
{
Q_ASSERT(m_room != nullptr);
// Allow making a model for an event that is being downloaded but will appear later
// e.g. a reply, but we need an ID to know when it has arrived.
Q_ASSERT(!m_eventId.isEmpty());
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
Q_EMIT eventUpdated();
updateReplyModel();
resetContent();
return true;
}
}
return false;
});
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent);
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == serverEvent->id()) {
if (m_room != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
beginResetModel();
m_isPending = false;
m_event = loadEvent<RoomEvent>(serverEvent->fullJson());
Q_EMIT eventUpdated();
m_eventId = serverEvent->id();
initializeEvent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
if (m_room != nullptr) {
for (int i = fromIndex; i <= toIndex; i++) {
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
}
}
}
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_room != nullptr) {
if (m_eventId == newEvent->id()) {
beginResetModel();
m_event = loadEvent<RoomEvent>(newEvent->fullJson());
Q_EMIT eventUpdated();
initializeEvent();
resetContent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && m_event != nullptr && eventId == m_eventId) {
if (m_room != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
if (eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_eventId || newEventId == m_eventId)) {
if (oldEventId == m_eventId || newEventId == m_eventId) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (oldThreadId == m_eventId || newThreadId == m_eventId) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
@@ -134,26 +122,78 @@ void MessageContentModel::initializeModel()
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
if (m_event != nullptr) {
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
updateReplyModel();
}
resetModel();
});
initializeEvent();
updateReplyModel();
resetModel();
}
void MessageContentModel::initializeEvent()
{
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
Q_EMIT eventUnavailable();
return;
}
if (m_eventSenderObject == nullptr) {
auto senderId = event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
}
void MessageContentModel::getEvent()
{
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = false;
initializeEvent();
updateReplyModel();
resetModel();
return true;
}
}
return false;
});
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = true;
resetModel();
return true;
}
}
return false;
});
m_room->downloadEventFromServer(m_eventId);
}
bool MessageContentModel::showAuthor() const
{
return m_showAuthor;
@@ -166,7 +206,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
}
m_showAuthor = showAuthor;
if (m_event != nullptr) {
if (m_room->connection()->isIgnored(m_eventSenderId)) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -193,25 +233,49 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {};
}
EventHandler eventHandler(m_room, m_event.get());
const auto component = m_components[index.row()];
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
if (role == DisplayRole) {
if (m_isReply) {
return i18n("Loading reply");
} else {
return i18n("Loading");
}
}
if (role == ComponentTypeRole) {
return component.type;
}
return {};
}
if (role == DisplayRole) {
if (component.type == MessageComponentType::Loading && m_isReply) {
return i18n("Loading reply");
if (m_notFound || m_room->connection()->isIgnored(m_eventSenderId)) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QString disabledTextColor;
if (theme != nullptr) {
disabledTextColor = theme->disabledTextColor().name();
} else {
disabledTextColor = QStringLiteral("#000000");
}
return QString(QStringLiteral("<span style=\"color:%1\">").arg(disabledTextColor)
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
+ QStringLiteral("</span>"));
}
if (m_event == nullptr) {
return QString();
}
if (m_event->isRedacted()) {
auto reason = m_event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", m_event->redactedBecause()->reason());
if (component.type == MessageComponentType::Loading) {
if (m_isReply) {
return i18n("Loading reply");
} else {
return i18n("Loading");
}
}
if (!component.content.isEmpty()) {
return component.content;
}
return eventHandler.getRichBody();
return EventHandler::richBody(m_room, event);
}
if (role == ComponentTypeRole) {
return component.type;
@@ -220,56 +284,53 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes;
}
if (role == EventIdRole) {
return eventHandler.getId();
return EventHandler::id(event);
}
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId();
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
return event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTime(m_isPending, lastUpdated);
return EventHandler::time(event, m_isPending, lastUpdated);
}
if (role == TimeStringRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId();
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
return event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTimeString(false, QLocale::ShortFormat, m_isPending, lastUpdated);
return EventHandler::timeString(event, QStringLiteral("hh:mm"), m_isPending, lastUpdated);
}
if (role == AuthorRole) {
return QVariant::fromValue(eventHandler.getAuthor(m_isPending));
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
return EventHandler::mediaInfo(m_room, event);
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get()));
return QVariant::fromValue(m_room->cachedFileTransferInfo(event));
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
return EventHandler::latitude(event);
}
if (role == LongitudeRole) {
return eventHandler.getLongitude();
return EventHandler::longitude(event);
}
if (role == AssetRole) {
return eventHandler.getLocationAssetType();
return EventHandler::locationAssetType(event);
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
}
if (role == ReplyEventIdRole) {
return eventHandler.getReplyId();
return EventHandler::replyId(event);
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(eventHandler.getReplyAuthor());
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event));
}
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
@@ -282,6 +343,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == ChatBarCacheRole) {
if (m_room->threadCache()->threadId() == m_eventId) {
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
}
return {};
}
@@ -309,20 +376,28 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[IsReplyRole] = "isReply";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
void MessageContentModel::resetModel()
{
const auto event = m_room->getEvent(m_eventId);
beginResetModel();
m_components.clear();
if (m_event == nullptr) {
if (m_room->connection()->isIgnored(m_eventSenderId) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
}
if (event == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel();
return;
@@ -336,16 +411,14 @@ void MessageContentModel::resetModel()
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing)
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
{
Q_ASSERT(m_event != nullptr);
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
beginRemoveRows({}, startRow, rowCount() - 1);
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing);
const auto newComponents = messageContentComponents(isEditing, isThreading);
if (newComponents.size() == 0) {
return;
}
@@ -354,17 +427,22 @@ void MessageContentModel::resetContent(bool isEditing)
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
return {};
}
QList<MessageComponent> newComponents;
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
if (eventCast<const Quotient::RoomMessageEvent>(event)
&& eventCast<const Quotient::RoomMessageEvent>(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
if (m_event->isRedacted()) {
if (event->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
@@ -374,37 +452,43 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
EventHandler eventHandler(m_room, m_event.get());
newComponents.append(componentsForType(eventHandler.messageComponentType()));
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event)));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && !EventHandler::isThreaded(event)) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
return newComponents;
}
void MessageContentModel::updateReplyModel()
{
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr || m_isReply) {
return;
}
EventHandler eventHandler(m_room, m_event.get());
if (!eventHandler.hasReply()) {
if (!EventHandler::hasReply(event) || (EventHandler::isThreaded(event) && NeoChatConfig::self()->threads())) {
if (m_replyModel) {
delete m_replyModel;
}
return;
}
const auto replyEvent = m_room->findInTimeline(eventHandler.getReplyId());
if (replyEvent == m_room->historyEdge()) {
m_replyModel = new MessageContentModel(m_room, eventHandler.getReplyId(), true);
} else {
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true);
if (m_replyModel != nullptr) {
return;
}
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event), true, false, this);
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
});
@@ -412,28 +496,45 @@ void MessageContentModel::updateReplyModel()
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
{
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
return {};
}
switch (type) {
case MessageComponentType::Text: {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
return TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
}
case MessageComponentType::File: {
QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, QString(), {}};
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (m_emptyItinerary) {
if (!m_isReply) {
auto fileTransferInfo = m_room->cachedFileTransferInfo(m_event.get());
auto fileTransferInfo = m_room->cachedFileTransferInfo(event);
#ifndef Q_OS_ANDROID
Q_ASSERT(event->content() != nullptr && event->content()->fileInfo() != nullptr);
const QMimeType mimeType = event->content()->fileInfo()->mimeType;
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->hasFileContent());
#if Quotient_VERSION_MINOR > 8
const QMimeType mimeType = roomMessageEvent->fileContent()->mimeType;
#else
const QMimeType mimeType = roomMessageEvent->content()->fileInfo()->mimeType;
#endif
if (mimeType.name() == QStringLiteral("text/plain") || mimeType.parentMimeTypes().contains(QStringLiteral("text/plain"))) {
QString originalName = event->content()->fileInfo()->originalName;
#if Quotient_VERSION_MINOR > 8
QString originalName = roomMessageEvent->fileContent()->originalName;
#else
QString originalName = roomMessageEvent->content()->fileInfo()->originalName;
#endif
if (originalName.isEmpty()) {
originalName = event->plainBody();
originalName = roomMessageEvent->plainBody();
}
KSyntaxHighlighting::Repository repository;
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName);
@@ -462,19 +563,27 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
} else {
updateItineraryModel();
}
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: {
if (!m_event->is<StickerEvent>()) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
if (!event->is<StickerEvent>()) {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
QList<MessageComponent> components;
components += MessageComponent{type, QString(), {}};
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
}
@@ -492,7 +601,7 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
if (linkPreviewer->loaded()) {
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_ls, link}}};
} else {
connect(linkPreviewer, &LinkPreviewer::loadedChanged, [this, link]() {
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
for (auto &component : m_components) {
@@ -533,6 +642,11 @@ QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageCompon
void MessageContentModel::closeLinkPreview(int row)
{
if (row < 0 || row > m_components.size()) {
qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
return;
}
if (m_components[row].type == MessageComponentType::LinkPreview || m_components[row].type == MessageComponentType::LinkPreviewLoad) {
beginResetModel();
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
@@ -544,13 +658,14 @@ void MessageContentModel::closeLinkPreview(int row)
void MessageContentModel::updateItineraryModel()
{
if (m_room == nullptr || m_event == nullptr) {
const auto event = m_room->getEvent(m_eventId);
if (m_room == nullptr || event == nullptr) {
return;
}
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
auto filePath = m_room->cachedFileTransferInfo(m_event.get()).localPath;
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (roomMessageEvent->hasFileContent()) {
auto filePath = m_room->cachedFileTransferInfo(event).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;

View File

@@ -6,27 +6,13 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include <Quotient/room.h>
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other;
QString content;
QVariantMap attributes;
int operator==(const MessageComponent &right) const
{
return type == right.type && content == right.content && attributes == right.attributes;
}
bool isEmpty() const
{
return type == MessageComponentType::Other;
}
};
#include "messagecomponent.h"
#include "neochatroommember.h"
/**
* @class MessageContentModel
@@ -64,17 +50,20 @@ public:
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
explicit MessageContentModel(NeoChatRoom *room,
const QString &eventId,
bool isReply = false,
bool isPending = false,
MessageContentModel *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
@@ -109,24 +98,28 @@ public:
Q_SIGNALS:
void showAuthorChanged();
void eventUnavailable();
void eventUpdated();
private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QString m_eventSenderId;
Quotient::RoomEventPtr m_event;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
bool m_notFound = false;
void initializeModel();
void initializeEvent();
void getEvent();
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();

View File

@@ -17,16 +17,14 @@
#include <QGuiApplication>
#include <QTimeZone>
#include <KFormat>
#include <KLocalizedString>
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
#include "models/messagefiltermodel.h"
#include "models/reactionmodel.h"
#include "readmarkermodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -69,6 +67,11 @@ MessageEventModel::MessageEventModel(QObject *parent)
connect(this, &MessageEventModel::modelReset, this, [this]() {
resetting = false;
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
beginResetModel();
endResetModel();
});
}
NeoChatRoom *MessageEventModel::room() const
@@ -86,10 +89,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_readMarkerModels.clear();
m_currentRoom->disconnect(this);
m_currentRoom = nullptr;
endResetModel();
// Don't clear the member objects until the model has been fully reset and all
// refs cleared.
m_memberObjects.clear();
m_contentModels.clear();
m_reactionModels.clear();
m_readMarkerModels.clear();
}
beginResetModel();
@@ -150,8 +159,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
refreshLastUserEvents(i);
}
});
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] {
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
m_initialized = true;
createEventObjects(event);
beginInsertRows({}, 0, 0);
});
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
@@ -214,22 +224,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
beginResetModel();
endResetModel();
});
connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
auto event = it->event();
if (event->senderId() == member.id()) {
refreshEventRoles(event->id(), {AuthorRole});
}
}
});
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
auto event = it->event();
if (event->senderId() == member.id()) {
refreshEventRoles(event->id(), {AuthorRole});
}
}
});
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id();
} else {
@@ -397,6 +391,8 @@ void MessageEventModel::fetchMore(const QModelIndex &parent)
}
}
static NeochatRoomMember *emptyNeochatRoomMember = new NeochatRoomMember;
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
{
if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
@@ -416,7 +412,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return DelegateType::ReadMarker;
case TimeRole: {
const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime();
const KFormat format;
static const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
}
case SpecialMarksRole:
@@ -437,31 +433,25 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
const auto &evt = isPending ? **pendingIt : **timelineIt;
EventHandler eventHandler(m_currentRoom, &evt);
if (role == Qt::DisplayRole) {
if (evt.isRedacted()) {
auto reason = evt.redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return eventHandler.getRichBody();
return EventHandler::richBody(m_currentRoom, &evt);
}
if (role == ContentModelRole) {
if (!evt.isStateEvent() && !evt.id().isEmpty()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
QString modelId;
if (!evt.id().isEmpty() && m_contentModels.contains(evt.id())) {
modelId = evt.id();
} else if (!evt.transactionId().isEmpty() && m_contentModels.contains(evt.transactionId())) {
modelId = evt.transactionId();
}
if (evt.isStateEvent()) {
if (evt.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_currentRoom, &evt));
}
if (!modelId.isEmpty()) {
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
}
return {};
}
if (role == GenericDisplayRole) {
return eventHandler.getGenericBody();
return EventHandler::genericBody(m_currentRoom, &evt);
}
if (role == DelegateTypeRole) {
@@ -469,11 +459,22 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AuthorRole) {
return QVariant::fromValue(eventHandler.getAuthor(isPending));
QString mId;
if (isPending) {
mId = m_currentRoom->localMember().id();
} else {
mId = evt.senderId();
}
if (!m_memberObjects.contains(mId)) {
return QVariant::fromValue<NeochatRoomMember *>(emptyNeochatRoomMember);
}
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(mId).get());
}
if (role == HighlightRole) {
return eventHandler.isHighlighted();
return EventHandler::isHighlighted(m_currentRoom, &evt);
}
if (role == SpecialMarksRole) {
@@ -486,7 +487,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return pendingIt->deliveryStatus();
}
if (eventHandler.isHidden()) {
if (EventHandler::isHidden(m_currentRoom, &evt)) {
return EventStatus::Hidden;
}
if (EventHandler::isThreaded(&evt) && EventHandler::threadRoot(&evt) != EventHandler::id(&evt) && NeoChatConfig::threads()) {
return EventStatus::Hidden;
}
@@ -494,7 +499,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == EventIdRole) {
return eventHandler.getId();
return EventHandler::id(&evt);
}
if (role == ProgressInfoRole) {
@@ -510,20 +515,20 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == TimeRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTime(isPending, lastUpdated);
return EventHandler::time(&evt, isPending, lastUpdated);
}
if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
return EventHandler::timeString(&evt, true, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == IsThreadedRole) {
return eventHandler.isThreaded();
return EventHandler::isThreaded(&evt);
}
if (role == ThreadRootRole) {
return eventHandler.threadRoot();
return EventHandler::threadRoot(&evt);
}
if (role == ShowSectionRole) {
@@ -576,7 +581,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AuthorDisplayNameRole) {
return eventHandler.getAuthorDisplayName(isPending);
return EventHandler::authorDisplayName(m_currentRoom, &evt, isPending);
}
if (role == IsRedactedRole) {
@@ -588,11 +593,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
return EventHandler::mediaInfo(m_currentRoom, &evt);
}
if (role == IsEditableRole) {
return eventHandler.messageComponentType() == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id();
return MessageComponentType::typeForEvent(evt) == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id();
}
return {};
@@ -619,6 +624,29 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
}
auto eventId = event->id();
auto senderId = event->senderId();
if (eventId.isEmpty()) {
eventId = event->transactionId();
}
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_currentRoom->localMember().id();
}
if (!m_memberObjects.contains(senderId)) {
m_memberObjects[senderId] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_currentRoom, senderId));
}
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, eventId));
}
}
if (EventHandler::isThreaded(event) && !m_threadModels.contains(EventHandler::threadRoot(event))) {
m_threadModels[EventHandler::threadRoot(event)] = QSharedPointer<ThreadModel>(new ThreadModel(EventHandler::threadRoot(event), m_currentRoom));
}
// ReadMarkerModel handles updates to add and remove markers, we only need to
// handle adding and removing whole models here.
@@ -679,4 +707,9 @@ bool MessageEventModel::event(QEvent *event)
return QObject::event(event);
}
ThreadModel *MessageEventModel::threadModelForRootId(const QString &threadRootId) const
{
return m_threadModels[threadRootId].data();
}
#include "moc_messageeventmodel.cpp"

View File

@@ -3,14 +3,16 @@
#pragma once
#include <KFormat>
#include <QAbstractListModel>
#include <QQmlEngine>
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include "pollhandler.h"
#include "readmarkermodel.h"
#include "threadmodel.h"
class ReactionModel;
@@ -53,8 +55,8 @@ public:
ContentModelRole, /**< The MessageContentModel for the event. */
IsThreadedRole,
ThreadRootRole,
IsThreadedRole, /**< Whether the message is in a thread. */
ThreadRootRole, /**< The Matrix ID of the thread root message, if any . */
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -103,6 +105,8 @@ public:
*/
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const;
protected:
bool event(QEvent *event) override;
@@ -113,9 +117,11 @@ private:
int rowBelowInserted = -1;
bool resetting = false;
bool movingEvent = false;
KFormat m_format;
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;

View File

@@ -4,12 +4,12 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include <QVariant>
#include "enums/delegatetype.h"
#include "messagecontentmodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "timelinemodel.h"
#include "neochatroommember.h"
using namespace Quotient;
@@ -133,14 +133,11 @@ bool MessageFilterModel::showAuthor(QModelIndex index) const
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
QString aggregateString;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
aggregateString += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
aggregateString += ", "_ls;
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAuthor)) {
uniqueAuthors.append(nextAuthor);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
@@ -148,46 +145,12 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
break;
}
}
parts.sort(); // Sort them so that all identical events can be collected.
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
chunks.last() += i18ncp("%1: What's being done; %2: How often it is done.", " %1", " %1 %2 times", part, count);
}
chunks.removeDuplicates();
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
QString chunksText;
chunksText += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", ");
chunksText += chunks.takeFirst();
}
chunksText +=
uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
chunksText += chunks.takeFirst();
}
return i18nc(
"userText (%1) is either a Matrix username if a single user sent all the states or n users if they were sent by multiple users."
"chunksText (%2) is a list of comma separated actions for each of the state events in the group.",
"<style>a {text-decoration: none;}</style>%1 %2",
userText,
chunksText);
} else {
return {};
aggregateString = aggregateString.trimmed();
if (aggregateString.endsWith(u',')) {
aggregateString.removeLast();
}
return aggregateString;
}
QVariantList MessageFilterModel::stateEventsList(int sourceRow) const

View File

@@ -121,12 +121,11 @@ void NotificationsModel::loadData()
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
const auto &roomEvent = eventCast<const RoomEvent>(notification.event.get());
EventHandler eventHandler(dynamic_cast<NeoChatRoom *>(room), roomEvent);
beginInsertRows({}, m_notifications.length(), m_notifications.length());
m_notifications += Notification{
.roomId = notification.roomId,
.text = room->member(authorId).htmlSafeDisplayName() + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": "))
+ eventHandler.getPlainBody(true),
+ EventHandler::plainBody(dynamic_cast<NeoChatRoom *>(room), roomEvent, true),
.authorName = room->member(authorId).htmlSafeDisplayName(),
.authorAvatar = authorAvatar,
.eventId = roomEvent->id(),

View File

@@ -2,20 +2,40 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "permissionsmodel.h"
#include "powerlevel.h"
#include <Quotient/events/roompowerlevelsevent.h>
#include <KLazyLocalizedString>
#include "powerlevel.h"
using namespace Qt::Literals::StringLiterals;
namespace
{
constexpr auto UsersDefaultKey = "users_default"_L1;
constexpr auto StateDefaultKey = "state_default"_L1;
constexpr auto EventsDefaultKey = "events_default"_L1;
constexpr auto InviteKey = "invite"_L1;
constexpr auto KickKey = "kick"_L1;
constexpr auto BanKey = "ban"_L1;
constexpr auto RedactKey = "redact"_L1;
static const QStringList defaultPermissions = {
QStringLiteral("users_default"),
QStringLiteral("state_default"),
QStringLiteral("events_default"),
QStringLiteral("invite"),
QStringLiteral("kick"),
QStringLiteral("ban"),
QStringLiteral("redact"),
UsersDefaultKey,
StateDefaultKey,
EventsDefaultKey,
};
static const QStringList basicPermissions = {
InviteKey,
KickKey,
BanKey,
RedactKey,
};
static const QStringList knownPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
QStringLiteral("m.room.power_levels"),
@@ -33,14 +53,14 @@ static const QStringList defaultPermissions = {
};
// Alternate name text for default permissions.
static const QHash<QString, KLazyLocalizedString> defaultPermissionNames = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "Default user power level")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "Default power level to set the room state")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "Default power level to send messages")},
{QStringLiteral("invite"), kli18nc("Room permission type", "Invite users")},
{QStringLiteral("kick"), kli18nc("Room permission type", "Kick users")},
{QStringLiteral("ban"), kli18nc("Room permission type", "Ban users")},
{QStringLiteral("redact"), kli18nc("Room permission type", "Remove messages sent by other users")},
static const QHash<QString, KLazyLocalizedString> permissionNames = {
{UsersDefaultKey, kli18nc("Room permission type", "Default user power level")},
{StateDefaultKey, kli18nc("Room permission type", "Default power level to set the room state")},
{EventsDefaultKey, kli18nc("Room permission type", "Default power level to send messages")},
{InviteKey, kli18nc("Room permission type", "Invite users")},
{KickKey, kli18nc("Room permission type", "Kick users")},
{BanKey, kli18nc("Room permission type", "Ban users")},
{RedactKey, kli18nc("Room permission type", "Remove messages sent by other users")},
{QStringLiteral("m.reaction"), kli18nc("Room permission type", "Send reactions")},
{QStringLiteral("m.room.redaction"), kli18nc("Room permission type", "Remove their own messages")},
{QStringLiteral("m.room.power_levels"), kli18nc("Room permission type", "Change user permissions")},
@@ -58,10 +78,10 @@ static const QHash<QString, KLazyLocalizedString> defaultPermissionNames = {
};
// Subtitles for the default values.
static const QHash<QString, KLazyLocalizedString> defaultSubtitles = {
{QStringLiteral("users_default"), kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{QStringLiteral("state_default"), kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{QStringLiteral("events_default"), kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
static const QHash<QString, KLazyLocalizedString> permissionSubtitles = {
{UsersDefaultKey, kli18nc("Room permission type", "This is the power level for all new users when joining the room")},
{StateDefaultKey, kli18nc("Room permission type", "This is used for all state events that do not have their own entry here")},
{EventsDefaultKey, kli18nc("Room permission type", "This is used for all message events that do not have their own entry here")},
};
// Permissions that should use the event default.
@@ -70,6 +90,7 @@ static const QStringList eventPermissions = {
QStringLiteral("m.reaction"),
QStringLiteral("m.room.redaction"),
};
};
PermissionsModel::PermissionsModel(QObject *parent)
: QAbstractListModel(parent)
@@ -109,6 +130,8 @@ void PermissionsModel::initializeModel()
}
m_permissions.append(defaultPermissions);
m_permissions.append(basicPermissions);
m_permissions.append(knownPermissions);
for (const auto &event : currentPowerLevelEvent->events().keys()) {
if (!m_permissions.contains(event)) {
@@ -131,17 +154,17 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
const auto permission = m_permissions.value(index.row());
if (role == NameRole) {
if (defaultPermissionNames.keys().contains(permission)) {
return defaultPermissionNames.value(permission).toString();
if (permissionNames.keys().contains(permission)) {
return permissionNames.value(permission).toString();
}
return permission;
}
if (role == SubtitleRole) {
if (permission.startsWith(QLatin1String("m.")) && defaultPermissionNames.keys().contains(permission)) {
if (knownPermissions.contains(permission) && permissionNames.keys().contains(permission)) {
return permission;
}
if (defaultSubtitles.contains(permission)) {
return defaultSubtitles.value(permission).toString();
if (permissionSubtitles.contains(permission)) {
return permissionSubtitles.value(permission).toString();
}
return QString();
}
@@ -166,11 +189,10 @@ QVariant PermissionsModel::data(const QModelIndex &index, int role) const
return QString();
}
if (role == IsDefaultValueRole) {
return permission.contains(QLatin1String("default"));
return defaultPermissions.contains(permission);
}
if (role == IsBasicPermissionRole) {
return permission == QStringLiteral("invite") || permission == QStringLiteral("kick") || permission == QStringLiteral("ban")
|| permission == QStringLiteral("redact");
return basicPermissions.contains(permission);
}
return {};
}
@@ -201,19 +223,19 @@ std::optional<int> PermissionsModel::powerLevel(const QString &permission) const
}
if (const auto currentPowerLevelEvent = m_room->currentState().get<Quotient::RoomPowerLevelsEvent>()) {
if (permission == QStringLiteral("ban")) {
if (permission == BanKey) {
return currentPowerLevelEvent->ban();
} else if (permission == QStringLiteral("kick")) {
} else if (permission == KickKey) {
return currentPowerLevelEvent->kick();
} else if (permission == QStringLiteral("invite")) {
} else if (permission == InviteKey) {
return currentPowerLevelEvent->invite();
} else if (permission == QStringLiteral("redact")) {
} else if (permission == RedactKey) {
return currentPowerLevelEvent->redact();
} else if (permission == QStringLiteral("users_default")) {
} else if (permission == UsersDefaultKey) {
return currentPowerLevelEvent->usersDefault();
} else if (permission == QStringLiteral("state_default")) {
} else if (permission == StateDefaultKey) {
return currentPowerLevelEvent->stateDefault();
} else if (permission == QStringLiteral("events_default")) {
} else if (permission == EventsDefaultKey) {
return currentPowerLevelEvent->eventsDefault();
} else if (eventPermissions.contains(permission)) {
return currentPowerLevelEvent->powerLevelForEvent(permission);
@@ -241,13 +263,16 @@ void PermissionsModel::setPowerLevel(const QString &permission, const int &newPo
auto powerLevelContent = currentPowerLevelEvent->contentJson();
if (powerLevelContent.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
// Deal with the case where a default or basic permission is missing from the event content erroneously.
} else if (defaultPermissions.contains(permission) || basicPermissions.contains(permission)) {
powerLevelContent[permission] = clampPowerLevel;
} else {
auto eventPowerLevels = powerLevelContent[QLatin1String("events")].toObject();
eventPowerLevels[permission] = clampPowerLevel;
powerLevelContent[QLatin1String("events")] = eventPowerLevels;
}
m_room->setState<Quotient::RoomPowerLevelsEvent>(powerLevelContent);
m_room->setState<Quotient::RoomPowerLevelsEvent>(Quotient::fromJson<Quotient::PowerLevelsEventContent>(powerLevelContent));
}
}

View File

@@ -11,7 +11,6 @@
#include <Quotient/jobs/basejob.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include <KLazyLocalizedString>
@@ -312,8 +311,12 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
pushConditions.append(keywordCondition);
}
#if Quotient_VERSION_MINOR > 8
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(PushRuleKind::kindString(kind),
#else
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushRuleKind::kindString(kind),
#endif
keyword,
actions,
QString(),
@@ -338,7 +341,11 @@ void PushRuleModel::removeKeyword(const QString &keyword)
}
auto kind = PushRuleKind::kindString(m_rules[index].kind);
#if Quotient_VERSION_MINOR > 8
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(kind, m_rules[index].id);
#else
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
#endif
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();
});
@@ -346,10 +353,18 @@ void PushRuleModel::removeKeyword(const QString &keyword)
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
#if Quotient_VERSION_MINOR > 8
auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(kind, ruleId);
#else
auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
#endif
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
if (job->enabled() != enabled) {
#if Quotient_VERSION_MINOR > 8
m_connection->callApi<Quotient::SetPushRuleEnabledJob>(kind, ruleId, enabled);
#else
m_connection->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
#endif
}
});
}
@@ -363,7 +378,11 @@ void PushRuleModel::setNotificationRuleActions(const QString &kind, const QStrin
actions = actionToVariant(action);
}
#if Quotient_VERSION_MINOR > 8
m_connection->callApi<Quotient::SetPushRuleActionsJob>(kind, ruleId, actions);
#else
m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
#endif
}
PushRuleAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)

View File

@@ -2,21 +2,13 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "reactionmodel.h"
#include "neochatroom.h"
#include "utils.h"
#include <QDebug>
#include <QFont>
#ifdef HAVE_ICU
#include <QTextBoundaryFinder>
#include <QTextCharFormat>
#include <unicode/uchar.h>
#include <unicode/urename.h>
#endif
#include <KLocalizedString>
#include <Quotient/roommember.h>
ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
@@ -157,30 +149,6 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
};
}
bool 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)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
}
QString ReactionModel::reactionText(QString text) const
{
text = text.toHtmlEscaped();
@@ -194,7 +162,7 @@ QString ReactionModel::reactionText(QString text) const
.arg(m_room->connection()->makeMediaUrl(QUrl(text)).toString(), QString::number(size));
}
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
return Utils::isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}
#include "moc_reactionmodel.cpp"

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