Compare commits

...

192 Commits

Author SHA1 Message Date
Laurent Montel
a244293f9f Fix port deprecated [=] operator in c++20 2023-08-30 10:52:20 +02:00
Tobias Fella
50ad18d095 Merge branch 'master' into kf6 2023-08-29 22:57:12 +02:00
Tobias Fella
9fcfad7058 Fix supportStreaming in VideoDelegate 2023-08-29 20:48:02 +00:00
Tobias Fella
cfcc1756dd Implement deactivating accounts 2023-08-29 22:38:32 +02:00
Tobias Fella
e15bec2295 Introduce NeoChatConnection
Previously, some functions that conceptually belong to the connection needed to be in the Controller, since we didn't have a place to put them.
This fixes that by extending the Connection class in a similar way as we extend the Room class.
2023-08-29 20:14:44 +00:00
Tobias Fella
ca53d163eb Merge branch 'master' into kf6 2023-08-29 19:56:46 +02:00
James Graham
9071cf827f Make sure that the event after a state event is also a state event before filtering it out. 2023-08-29 17:41:20 +01:00
Yuri Chornoivan
c7deaaba84 Fix minor typo 2023-08-29 09:52:45 +03:00
l10n daemon script
414035de8b GIT_SILENT Sync po/docbooks with svn 2023-08-29 01:44:33 +00:00
James Graham
3bac752463 Merge branch 'master' into kf6 2023-08-28 14:04:17 +01:00
James Graham
442612d31d Merge the functionality of CollapseStateProxyModel into MessageFilterModel
Merge the functionality of CollapseStateProxyModel into MessageFilterModel there is no need for a whole separate model the filters can be combined trivialy.
2023-08-28 10:31:18 +00:00
Tobias Fella
2ba3d970e1 Merge branch 'master' into kf6 2023-08-28 12:05:45 +02:00
Tobias Fella
59164d3bb2 Implement account registration
Implements #7
2023-08-28 10:05:09 +00:00
Tobias Fella
cc60dde62d Deselect space when leaving it
BUG: 473271
2023-08-28 11:47:39 +02:00
Tobias Fella
a19502ca40 Fix KF6 crash in emoji completion 2023-08-28 09:46:47 +00:00
l10n daemon script
cbed8148a3 GIT_SILENT Sync po/docbooks with svn 2023-08-28 02:09:20 +00:00
Tobias Fella
1b2919587a Fix opening CreateSpaceDialog in Qt6 2023-08-28 00:24:42 +02:00
Tobias Fella
e5420973f5 Merge branch 'master' into kf6 2023-08-27 21:49:53 +02:00
James Graham
50e8b9ebf6 Visit User
Move to using visit user in all cases.
2023-08-27 19:05:55 +00:00
Nicolas Fella
f5ad2ad162 Fix enter key naviation in login flow 2023-08-27 19:32:41 +02:00
Tobias Fella
931d20fecd Merge branch 'master' into kf6 2023-08-27 19:18:07 +02:00
Tobias Fella
d11d6c74b3 Fix opening settings
BUG: 473789
2023-08-27 17:17:12 +00:00
James Graham
725c6c30ce Get KF6 Working
Minimal set of changes so that a compiled version of NeoChat on KF6 will run and at least be functional.
2023-08-27 16:17:56 +00:00
James Graham
63ed69a5d4 Use updated AlbumMaximizeDelegate
Autoload is being removed so don't use it.

Requires libraries/kirigami-addons!146
2023-08-27 16:02:07 +00:00
Tobias Fella
a8aa775575 Implement message forwarding
Implements #338
2023-08-27 09:55:31 +00:00
Tobias Fella
6305359b3c Use round instead of ceil in delegatesizehelper
Fixes #592
2023-08-27 09:54:57 +00:00
Tobias Fella
7e859364af Don't show "no canonical alias" message in GroupChatDrawerHeader
The word "canonical" is confusing to average people and there isn't really a point in showing the message.
2023-08-27 09:54:41 +00:00
Tobias Fella
8abd0db012 Remove redundant NeoChatRoom::htmlSafeDisplayName 2023-08-27 09:54:28 +00:00
Tobias Fella
dbc10685f0 Port login to FormCard 2023-08-27 09:54:12 +00:00
l10n daemon script
96582a12bc GIT_SILENT Sync po/docbooks with svn 2023-08-27 01:50:22 +00:00
l10n daemon script
0ac61854bd GIT_SILENT made messages (after extraction) 2023-08-27 00:46:25 +00:00
Tobias Fella
68298f038d Fix opening report sheet 2023-08-26 19:54:45 +00:00
Tobias Fella
405fd5841a Fix opening ban sheet 2023-08-26 19:54:17 +00:00
Tobias Fella
5da9bba844 Force plain text user display name in user detail sheet 2023-08-26 19:54:06 +00:00
Tobias Fella
84373712ef Fix opening about page 2023-08-26 21:51:40 +02:00
James Graham
50f4f96341 Fix maximize component menu
Should be plainText not plainMessage
2023-08-26 19:29:11 +00:00
Tobias Fella
1b27b1a4e2 Make sure we don't load link previews when they're not enabled
Fixes #591
2023-08-26 19:28:29 +00:00
Tobias Fella
33811a4c49 Remove unused kitemmodels import 2023-08-26 21:25:09 +02:00
James Graham
43715486e5 Room Drawer Mobile
Push the room drawer as a page rather than a drawer when on mobile or the window is thin.

This also lays some foundations for having multiple different pages in the drawer by separating the room information into it's own component which could be switched out to show other things like highlights or media.
2023-08-26 08:42:39 +00:00
l10n daemon script
5c72bd4ab7 GIT_SILENT Sync po/docbooks with svn 2023-08-26 01:47:31 +00:00
James Graham
092f1be99b Further mark messages read fixes
Add a timer to lockout the scroll trigger for marking messages as read in the first second after changing room. This is because the scrollview can't differentiate between mouse scrolling and the multiple changes in scrollview that happen as the room is changed over.
2023-08-25 22:27:20 +00:00
Carl Schwan
550d55cb1a Add release note for last two version 2023-08-25 14:25:34 +02:00
l10n daemon script
e9edb61245 GIT_SILENT Sync po/docbooks with svn 2023-08-25 01:47:19 +00:00
Tobias Fella
89aae665b1 Fix reaction string 2023-08-24 17:38:17 +00:00
Carl Schwan
1e3c3dd1f4 Remove debuging value
Otherwise like of connectivity never disapear
2023-08-24 16:10:01 +02:00
Ingo Klöcker
6f4d2c0216 Add Windows-specific screenshots as screenshots with custom attribute
Instead of using translated custom values (which are not accepted by
AppStream validation) this adds the Windows-specific screenshots as
normal screenshots with custom attribute x-kde-os="windows". AppStream
validation accepts custom attributes (in fact, it ignores any attributes
other than "type").

Upstream appstream tooling will treat those screenshots as any other
extra screenshots (screenshots without type="default").
2023-08-23 15:31:47 +02:00
l10n daemon script
803cd2b4e4 GIT_SILENT Sync po/docbooks with svn 2023-08-23 01:47:24 +00:00
Carl Schwan
9f3012061d ReactionDelegate: Don't bind twice the reaction repeater model
We already are using an alias for this
2023-08-22 21:49:30 +00:00
Tobias Leupold
c1604a9c4f Trayicon tweaks
This fixes two minor inconveniences:

- When closing the chat window and re-showing it from the systray icon, the geometry was not properly restored. The window was always shown in the middle of the screen. Now, one gets the window back with it's actual last position and size.
- It is now possible to not only show the window from the systray icon, but also to close it. This is the way other chat programs do it (Kopete back in the day, Konversation, Quassel IRC etc.)
2023-08-22 20:58:04 +00:00
l10n daemon script
3c7fcee244 GIT_SILENT Sync po/docbooks with svn 2023-08-22 01:47:22 +00:00
Tobias Fella
d33a50a00d Fix json for replies 2023-08-21 19:10:15 +02:00
Tobias Fella
ed033a1c5e Raise compiler settings level to 5.105 2023-08-21 13:02:14 +02:00
l10n daemon script
2b961703ae GIT_SILENT Sync po/docbooks with svn 2023-08-21 01:46:37 +00:00
l10n daemon script
0539665779 GIT_SILENT made messages (after extraction) 2023-08-21 00:46:26 +00:00
James Graham
df127b88e6 Fix Image Replies
Make sure that the image container has it's implicit width/height set so that the correct size can always be calculated. This was breaking images that were a reply to another message (not sure if that's legal because we don't allow but other clients do because I found one).
2023-08-20 18:16:51 +00:00
Joshua Goins
27c4b57f0f Fix link to 'made for matrix' bade 2023-08-20 11:30:05 -04:00
Joshua Goins
f875a23e83 Only close the settings window if there's no accounts left 2023-08-20 15:08:57 +00:00
Joshua Goins
7f5cfbf21c Move the sign out button outside the editor, and close the dialog 2023-08-20 15:08:57 +00:00
Joshua Goins
f7c7643c1c Emit the accepted signal in the logout confirmation dialog 2023-08-20 15:08:57 +00:00
Joshua Goins
692edf52f1 Allow customizing the connection for the logout confirm dialog 2023-08-20 15:08:57 +00:00
Joshua Goins
e48cfaa41f Check if the room is valid before trying to refresh users 2023-08-20 15:08:57 +00:00
Joshua Goins
ba116460d5 Check if there's an active connection before updating notification rules 2023-08-20 15:08:57 +00:00
Joshua Goins
c71672dab3 Remove used logout dialog in main.qml 2023-08-20 15:08:57 +00:00
l10n daemon script
932ef72311 GIT_SILENT Sync po/docbooks with svn 2023-08-20 01:46:49 +00:00
l10n daemon script
7e53a2234f GIT_SILENT made messages (after extraction) 2023-08-20 00:46:36 +00:00
Joshua Goins
a0499e5140 Set sourceSize in for images, improving memory usage and smoothing
In some cases (where we don't get a thumbnail) we load the entire image,
which might be large. Even when we get a thumbnail, the smoothing we
apply doesn't get the best results and the only other option is
mipmapping which is more memory intensive - it stores progressively
smaller images but keeps all of them in memory.

Now the ImageDelegate sets the sourceSize, which helps reduce the memory
footprint (since Qt will throw away whatever parts of the image we don't
need) and makes the images look smoother when scaled. However,
AnimatedImages do not support this so this change introduces a new
property to tell if the image could possibly be animated. It will
erroneously pick up formats that could support animation (e.g. WebP) but
other image formats will still benefit so it's a net positive. The
ImageDelegate will load the correct image component depending on this
property.
2023-08-19 18:20:49 +00:00
Joshua Goins
093ef0a18c Use mxc urls for completion room avatars 2023-08-19 11:49:03 -04:00
Joshua Goins
36bf862ab9 Change the completion proxy model to filter out invalid entries
When auto-completing rooms, there may be some that do not have
canonical addresses (e.g. 1-to-1 chats). These can't be linked anyway,
so skip them and any other unlinkable entries.
2023-08-19 15:45:51 +00:00
Joshua Goins
6d8c1d0780 Fix avatar URLs for rooms
Room avatar URLs don't specify the new mxc:// format the user avatars
use, so we need to carve out a workaround for them.
2023-08-19 15:45:51 +00:00
Joshua Goins
7fe85066a4 Move attachment dialog to ChatBar, where it can actually be used
ChatBar calls to an attachDialog that never comes, so let's have them
move in together.
2023-08-19 15:23:04 +00:00
Tobias Fella
1fefa228e6 Don't check isJobPending in explore page 2023-08-19 14:08:45 +00:00
Joshua Goins
4104e10d95 Add another message when no rooms are found 2023-08-19 14:08:45 +00:00
Joshua Goins
05f3c3ee0a Make the explore page header way prettier, like it is in KCMs 2023-08-19 14:08:45 +00:00
Joshua Goins
0a1c489401 Make the explore page search loading process more obvious
Adds progress bars, lots of them! Now it's easier to tell if NeoChat is
trying it's best to load the public room list.
2023-08-19 14:08:45 +00:00
l10n daemon script
e53d63ad8b GIT_SILENT Sync po/docbooks with svn 2023-08-18 01:44:42 +00:00
l10n daemon script
78541b32f0 GIT_SILENT Sync po/docbooks with svn 2023-08-17 01:46:48 +00:00
Carl Schwan
3bd93996c0 Don't inherit color in TimelineContainer 2023-08-16 23:44:22 +02:00
Heiko Becker
61968aa475 GIT_SILENT Update Appstream for new release
(cherry picked from commit 91b5573016)
2023-08-16 23:33:49 +02:00
Joshua Goins
24d0082048 Use view-list-details instead of expand icon in room drawer 2023-08-16 20:07:46 +00:00
Joshua Goins
2980af11b0 Destroy the correct component in space list context menu 2023-08-16 20:07:46 +00:00
Joshua Goins
91d9406c38 Prevent the TapHandler in AvatarTabButton from triggering clicked()
This caused right-clicks on this component (for example, in the space
drawer) to cause clicked() to be called on the delegate.
2023-08-16 20:07:46 +00:00
Joshua Goins
280d1e38e2 Add icons to the space list context menu items 2023-08-16 20:07:46 +00:00
Joshua Goins
7b520da4b4 Prevent the chat bar from pasting a local file URL if we save the bitmap
There's no point in also putting the file:// URL, so catch the event if
saving the bitmap from the clipboard is successful.
2023-08-16 19:57:22 +00:00
Joshua Goins
419b4cea98 Ensure that the notification avatar is drawn with no pen 2023-08-16 19:46:59 +00:00
Joshua Goins
38824f30ac Further notification avatar improvements
The correct render hint is actually SmoothPixmapTransform, as
Antialiasing only matters for primitives. Also, render a white
background for potentially transparent avatars.
2023-08-16 19:46:59 +00:00
Joshua Goins
9c4d8ef823 Fix the account switcher width so it doesn't spill off the side 2023-08-16 19:39:14 +00:00
Joshua Goins
fe3bf3a638 Pick a better shadow and clean up emoji popup backgrounds
These shadows are now closer to how other popups look.
2023-08-16 19:31:07 +00:00
Joshua Goins
d678a446e2 Don't clip the emoji popups as they destroy shadows 2023-08-16 19:31:07 +00:00
Nicolas Fella
6df60a39b0 Port to KStatusNotifierItem framework 2023-08-16 11:43:56 +00:00
l10n daemon script
6f9fa76ab7 GIT_SILENT Sync po/docbooks with svn 2023-08-16 01:47:00 +00:00
Carl Schwan
6e8b0f001f Add network cache 2023-08-15 15:33:46 +00:00
l10n daemon script
0bfad95d8b GIT_SILENT Sync po/docbooks with svn 2023-08-15 01:48:13 +00:00
Tobias Fella
1b43846196 Remove obsolete quotientMinorVersion property 2023-08-14 18:05:14 +00:00
Tobias Fella
4ea183b139 Improve tooltips in chatbar 2023-08-14 07:54:47 +00:00
Tobias Fella
22fd24549b Wrap room display name in Room Drawer 2023-08-13 20:02:58 +02:00
l10n daemon script
af793fb865 GIT_SILENT Sync po/docbooks with svn 2023-08-13 01:45:51 +00:00
l10n daemon script
13414b5834 GIT_SILENT made messages (after extraction) 2023-08-13 00:46:24 +00:00
l10n daemon script
65a1df4a75 GIT_SILENT Sync po/docbooks with svn 2023-08-12 01:49:08 +00:00
l10n daemon script
d15157703a GIT_SILENT made messages (after extraction) 2023-08-12 00:46:19 +00:00
l10n daemon script
2d21330a36 GIT_SILENT Sync po/docbooks with svn 2023-08-11 01:45:25 +00:00
l10n daemon script
fbc4611d02 GIT_SILENT Sync po/docbooks with svn 2023-08-10 01:47:01 +00:00
l10n daemon script
e257865b19 GIT_SILENT made messages (after extraction) 2023-08-10 00:46:30 +00:00
James Graham
018154f555 No emoji chatbar button on mobile
Hide the emoji button in the `chatbar` and don't instantiate the dialog component on mobile. This isn't needed on mobile as the virtual keyboard can add emojis. Should also save a small amount of memory on mobile from the dialog which is normally always instantiated.
2023-08-09 18:10:53 +00:00
l10n daemon script
80a5daa1f7 GIT_SILENT Sync po/docbooks with svn 2023-08-09 01:48:44 +00:00
Carl Schwan
bf5ce049d9 Port to new settings component 2023-08-08 21:40:05 +00:00
Carl Schwan
58213ee3e6 Display big emoji correctly
We already check with a regex if the message only contains emoji so it is safe
to hardcode the font family as emoji for these messages.
2023-08-08 12:07:16 +02:00
l10n daemon script
2d1862a637 GIT_SILENT Sync po/docbooks with svn 2023-08-08 01:49:39 +00:00
Tobias Fella
67453d9fb8 Remove ifdefs for E2EE 2023-08-07 21:15:11 +00:00
Tobias Fella
3ab04583ae Force plain text in room delegate 2023-08-07 22:58:11 +02:00
l10n daemon script
794683b5d2 GIT_SILENT Sync po/docbooks with svn 2023-08-07 02:08:13 +00:00
l10n daemon script
9f7f5409bd SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2023-08-07 01:51:19 +00:00
l10n daemon script
7d7b9d7ae0 GIT_SILENT made messages (after extraction) 2023-08-07 00:48:50 +00:00
James Graham
c7c892c51c Fix message long press
Add a tap handler so that long press events in the text of a message can be captured. This should make mobile menus work on text messages again.
2023-08-06 17:08:46 +00:00
l10n daemon script
97903a7cb0 GIT_SILENT Sync po/docbooks with svn 2023-08-06 01:50:23 +00:00
l10n daemon script
0c8f488ac4 GIT_SILENT made messages (after extraction) 2023-08-06 00:46:42 +00:00
Tobias Fella
88368cefaa Remove leftover debug statement 2023-08-06 00:51:28 +02:00
Tobias Fella
ab4bb41979 Fix last active time in devices page 2023-08-05 20:32:47 +02:00
James Graham
c2398b19dc Fix mark all read on room change
Fix it so that the view is correctly positioned at the bottom when the room is changed and make sure this is not seen as a scroll event (i.e. messages are not automatically marked as read but the timer has to time out first and all visible on screen).
2023-08-05 17:38:21 +00:00
James Graham
4b3dac025f Declare currentRoom property
Make sure `currentRoom` is a property for every qml component where it's used in `chatbar`.
2023-08-05 16:50:06 +00:00
l10n daemon script
9eeff06328 GIT_SILENT Sync po/docbooks with svn 2023-08-05 01:46:52 +00:00
l10n daemon script
657a8291d0 GIT_SILENT made messages (after extraction) 2023-08-05 00:46:28 +00:00
James Graham
bc4431462a Chatbox id for all property refs
Change the chatbox id to root and use for all references to it properties. Using root.currentRoom is needed for the chatbar to be visible in KF6
2023-08-04 13:56:26 +00:00
l10n daemon script
fdd8a62873 GIT_SILENT Sync po/docbooks with svn 2023-08-04 01:47:18 +00:00
l10n daemon script
c164228894 GIT_SILENT made messages (after extraction) 2023-08-04 00:46:08 +00:00
Carl Schwan
7d418f0883 Shorten appstream description 2023-08-03 16:17:58 +00:00
Tobias Fella
e90aecbead Port UserDetailDialog to Kirigami.Dialog 2023-08-03 17:26:46 +02:00
l10n daemon script
221c27fbbe GIT_SILENT Sync po/docbooks with svn 2023-08-03 02:09:50 +00:00
l10n daemon script
1907404124 GIT_SILENT Sync po/docbooks with svn 2023-08-02 01:54:58 +00:00
James Graham
81e953dd8c use QTEST_MAIN 2023-08-01 15:57:58 +00:00
James Graham
9311052e39 Move polishcolor to a utils namespace and rename getusercolor 2023-08-01 15:57:58 +00:00
James Graham
648fff20f2 Updates based on comments 2023-08-01 15:57:58 +00:00
James Graham
980211f36a Remove unneeded include 2023-08-01 15:57:58 +00:00
James Graham
25e0e3fa8e Remove NeoChatUser and just use Quotient::User this simplifies things and avoids casting from one to the other which address santizer didn't like 2023-08-01 15:57:58 +00:00
l10n daemon script
403c1ed391 GIT_SILENT Sync po/docbooks with svn 2023-08-01 01:58:12 +00:00
l10n daemon script
c2f76612cf GIT_SILENT made messages (after extraction) 2023-08-01 00:46:09 +00:00
Tobias Fella
2a651b8946 Use pageStack.pushDialogLayer for devtools and inviteuser 2023-07-31 20:44:50 +02:00
Volker Krause
00c0d1c276 Add heading indicator for live locations
Heading isn't covered by any MSC so far, so that is using Itinerary's
vendor prefix for now.
2023-07-31 18:06:52 +02:00
Carl Schwan
0162d91b60 Ensure e2ee is enabled 2023-07-31 07:09:23 +00:00
Carl Schwan
5f41b7b4c1 Fix opening account menu on right click 2023-07-31 07:02:00 +00:00
l10n daemon script
d9691bac7c GIT_SILENT Sync po/docbooks with svn 2023-07-31 02:10:40 +00:00
Tobias Fella
719b916837 Add accessible name to RoomDelegate 2023-07-30 22:57:46 +02:00
Tobias Fella
f9514fe925 Add accessible name to chatbar 2023-07-30 22:57:43 +02:00
Tobias Fella
399b9e6944 Add text and tooltip to section collapse/expand button 2023-07-30 22:29:21 +02:00
l10n daemon script
00cf5c8a5b GIT_SILENT Sync po/docbooks with svn 2023-07-30 01:49:49 +00:00
l10n daemon script
02f26424e2 GIT_SILENT made messages (after extraction) 2023-07-30 00:45:54 +00:00
Tobias Fella
3bd639775a Fix room list icon size in compact room list
BUG: 472682
2023-07-29 19:00:58 +00:00
Tobias Fella
163c1ef407 Html-escape display names in state delegates 2023-07-29 15:42:53 +02:00
Tobias Fella
9997b1074a Make "Close to system tray" string more correct
BUG: 472051
2023-07-29 09:01:14 +00:00
l10n daemon script
f52a25717b GIT_SILENT Sync po/docbooks with svn 2023-07-29 01:46:50 +00:00
l10n daemon script
5746c24b73 GIT_SILENT Sync po/docbooks with svn 2023-07-28 01:55:04 +00:00
l10n daemon script
dca1b22521 GIT_SILENT made messages (after extraction) 2023-07-28 00:47:00 +00:00
l10n daemon script
b15f1461d8 GIT_SILENT Sync po/docbooks with svn 2023-07-27 01:47:22 +00:00
Joshua Goins
24fd7a0f5b Use prettier notification images
Avatars are shown rounded in the main interface, so they should look the
same in the notification tray too. On top of that, if the room is a
group then show that group's icon when applicable in the bottom right.
2023-07-26 08:31:58 +00:00
Joshua Goins
0ac3932303 Add an obvious "upload" button in the account editor
It's not obvious at first glance that you can click on the avatar in the
account editor to upload an image. This adds a new button when there is
no image uploaded.
2023-07-26 08:27:55 +00:00
Joshua Goins
807a9ee779 Fix errors preventing the avatar from being shown when uploaded 2023-07-26 08:27:55 +00:00
Joshua Goins
4f32a168cd Fix settings button tooltip 2023-07-26 02:04:24 -04:00
l10n daemon script
45d2da56c7 GIT_SILENT Sync po/docbooks with svn 2023-07-26 01:49:31 +00:00
l10n daemon script
d794e26d62 GIT_SILENT made messages (after extraction) 2023-07-26 00:46:56 +00:00
l10n daemon script
42cc42642d GIT_SILENT Sync po/docbooks with svn 2023-07-25 01:47:33 +00:00
l10n daemon script
ffd6205bd1 GIT_SILENT Sync po/docbooks with svn 2023-07-24 09:05:17 +00:00
Tobias Fella
9ba6b2f0a5 Fix clearing user search field text when switching rooms 2023-07-24 08:45:51 +00:00
Carl Schwan
f8d3e25f8e Move no connection info message to the top 2023-07-24 08:45:10 +00:00
Carl Schwan
75cd77facb Refactor completion menu
- Use new Avatar
- Use required property in delegate
2023-07-24 08:44:49 +00:00
l10n daemon script
8546d76a54 GIT_SILENT made messages (after extraction) 2023-07-24 07:34:02 +00:00
Tobias Fella
00b7f68a03 Fix nullptr check 2023-07-23 23:10:52 +02:00
Carl Schwan
b486cb905c Add back some spacing around the avatar in the timeline 2023-07-22 18:41:09 +00:00
Carl Schwan
cdd5c9f484 Fix accessibility of StartChatPage and port to new components 2023-07-22 18:41:09 +00:00
Carl Schwan
3e6c426397 Port Avatar usage in Settings KirigamiAddons 2023-07-22 18:41:09 +00:00
Carl Schwan
847f41e1ad Port most Avatar usage to new KirigamiAddons Avatar 2023-07-22 18:41:09 +00:00
Carl Schwan
ed5bb270ff Discard wrong avatar urls 2023-07-22 17:51:03 +00:00
Carl Schwan
c8b4da2b96 Scroll to top of RoomDrawer on loading of header 2023-07-22 17:50:32 +00:00
Tobias Fella
ba5445e135 Stop finding QtKeychain
Has been upstreamed to libQuotient
2023-07-22 16:14:56 +02:00
l10n daemon script
1cca39e105 GIT_SILENT Sync po/docbooks with svn 2023-07-22 01:57:06 +00:00
Carl Schwan
dbf67b984e Fix reply error message when the reply is not loaded yet 2023-07-21 11:24:10 +02:00
l10n daemon script
c9126cf38e GIT_SILENT Sync po/docbooks with svn 2023-07-21 02:04:06 +00:00
Tobias Fella
13988da4fc Add login appium test 2023-07-20 07:14:23 +00:00
Carl Schwan
0847839abc Remove hover effect on timeline events 2023-07-20 08:40:15 +02:00
Carl Schwan
6b55e502a0 Remove duplicated code in QuickSwitcher for RoomDelegate 2023-07-20 08:39:59 +02:00
Carl Schwan
8f81629ac1 Redesign room list
Use KirigamiAddons.Delegated.RoundedItemDelegate
2023-07-20 08:39:57 +02:00
Carl Schwan
7f459cb90f Redesign timeline
Remove border von bubbles and only hilight the bubble on hover
2023-07-20 08:38:51 +02:00
l10n daemon script
420e195313 GIT_SILENT Sync po/docbooks with svn 2023-07-20 01:59:01 +00:00
Carl Schwan
3263a69880 Port to KirigamAddons.Banner 2023-07-19 22:46:36 +02:00
l10n daemon script
b060881f06 GIT_SILENT Sync po/docbooks with svn 2023-07-19 02:41:03 +00:00
Tobias Fella
701e786c1f Require passing tests on windows 2023-07-18 23:23:33 +02:00
Tobias Fella
646c8ba8fe Fix opening user mentions 2023-07-18 05:54:15 +00:00
Tobias Fella
9b31fdea10 Cleanup CMake after depending on libQuotient 0.7 2023-07-18 05:26:49 +00:00
l10n daemon script
ce5dfdee16 GIT_SILENT Sync po/docbooks with svn 2023-07-18 01:55:04 +00:00
Tobias Fella
918e805718 Fix image loading in room avatars 2023-07-17 11:25:52 +00:00
Albert Astals Cid
7debf47833 GIT_SILENT Upgrade release service version to 23.11.70. 2023-07-17 12:53:16 +02:00
Tobias Fella
2c142c36e6 Remove cmake policy 2023-07-17 11:57:19 +02:00
l10n daemon script
3279142498 GIT_SILENT Sync po/docbooks with svn 2023-07-17 01:57:57 +00:00
l10n daemon script
0e08a1aa7e GIT_SILENT made messages (after extraction) 2023-07-17 00:49:09 +00:00
211 changed files with 39809 additions and 31164 deletions

View File

@@ -57,6 +57,11 @@ Dependencies:
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux/Qt6', 'Linux/Qt5']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]

View File

@@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
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}")
@@ -27,7 +27,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -77,7 +77,7 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -93,11 +93,9 @@ set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
if(QT_MAJOR_VERSION STREQUAL "6")
find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} REQUIRED)
endif()
if(ANDROID)
find_package(OpenSSL)
@@ -130,6 +128,11 @@ set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
PURPOSE "Talk with matrix server"
)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
find_package(cmark)
set_package_properties(cmark PROPERTIES
TYPE REQUIRED
@@ -160,10 +163,6 @@ set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL
)
if(NOT Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
@@ -179,9 +178,11 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING AND Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
if (BUILD_TESTING)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
add_subdirectory(appiumtests)
endif()
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)

View File

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

View File

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

View File

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

View File

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

44
appiumtests/logintest.py Executable file
View File

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

View File

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

View File

@@ -46,43 +46,26 @@
<name xml:lang="uk">NeoChat</name>
<name xml:lang="x-test">xxNeoChatxx</name>
<name xml:lang="zh-CN">NeoChat</name>
<summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
<summary xml:lang="ca">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
<summary xml:lang="eo">Kliento por matrix, la malcentra komunikprotokolo</summary>
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada</summary>
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
<summary xml:lang="ie">Un cliente de Matrix, li protocol de communication decentralisat</summary>
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
<summary xml:lang="nn">Ein klient for Matrix  protokollen for desentralisert kommunikasjon</summary>
<summary xml:lang="pa">ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ</summary>
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
<summary xml:lang="ru">Клиент для Matrix — децентрализованного коммуникационного протокола</summary>
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
<summary>Chat with your friends on matrix</summary>
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="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="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<description>
<p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami
to provide a convergent experience across multiple platforms.</p>
@@ -92,17 +75,20 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
<p xml:lang="eo">NeoChat estas kliento por Matrix, la malcentra komunikoprotokolo por tuja mesaĝado. Ĝi ebligas al vi sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj. Ĝi uzas KDE-framojn kaj precipe Kirigami por disponigi konverĝan sperton tra pluraj platformoj.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
<p xml:lang="eu">NeoChat «Matrix»erako, bat-bateko mezularitzarako komunikazio deszentralizatuko protokolorako, bezero bat da. Zure sendiari, kide eta lagunei testu mezuak, bideo eta audio fitxategiak bidaltzeko aukera ematen dizu. «KDE Frameworks» eta bereziki «Kirigami» erabiltzen ditu plataforma anitzen artean esperientzia konbergente bat eskaintzeko.</p>
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxería instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
<p xml:lang="ko">NeoChat은 분산형 인스턴트 메시징 통신 프로토콜인 Matrix 클라이언트입니다. 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다. KDE 프레임워크와 Kirigami를 사용하여 다양한 플랫폼에서 일관적인 사용자 경험을 제공합니다.</p>
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
<p xml:lang="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</p>
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
<p xml:lang="sv">NeoChat är en klient för Matrix, det decentraliserade kommunikationsprotokollet för direktmeddelanden. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner. Den använder KDE Ramverk, i synnerhet Kirigami, för att tillhandahålla en konvergent upplevelse på flera plattformar.</p>
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.xx</p>
@@ -111,18 +97,22 @@ to provide a convergent experience across multiple platforms.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="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>
<p xml:lang="eu">«NeoChat»ek «Matrix» zehaztapenaren ezaugarri guztiak eskaintzen dituen aplikazio bat izan nahi du. Beraz, egungo zehaztapen egonkorrean dagoen guztiaren euskarria du, VoIP, hariak eta muturren artean zifratzeko salbuespen nabarmenekin. Badira beste ez-betetze txikiago batzuk, «Matrix»en zehaztapena etengabe eboluzioan dagoelako, baina azken helburua zehaztapen osoaren euskarria ematea izaten jarraitzen du.</p>
<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="ia">NeoChat aspira a esser un application plenemente 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>
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
@@ -131,18 +121,23 @@ to provide a convergent experience across multiple platforms.</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="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>
<p xml:lang="eu">«Matrix» zehaztapenaren garapenaren izaeragatik, «Neo¢hat»ek ezaugarri ezegonkor ugari ere onartzen ditu. Gaur egun hauek:</p>
<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="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>
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
<p xml:lang="ko">Matrix 표준 개발의 특징으로 인하여 NeoChat은 일부 실험적인 기능을 지원합니다. 현재 지원하는 기능은 다음과 같습니다.</p>
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
<p xml:lang="sv">På grund av sättet Matrix-specifikationens utvecklas, stöder NeoChat också ett stor antal instabila funktioner. För närvarande är de:</p>
<p xml:lang="ta">மேட்ரிக்ஸு நெறிமுறை வரையறுக்கப்படும் வித‍த்தின் காரணமாக, பல நிலையற்ற அம்சங்களையும் நியோச்சாட் ஆதரிக்கிறது. தற்போது ஆதரிக்கப்படுபவை:</p>
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
@@ -152,17 +147,21 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ca">Enquestes - MSC3381</li>
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="eo">Enketoj - MSC3381</li>
<li xml:lang="es">Encuestas - MSC3381</li>
<li xml:lang="eu">Polls - MSC3381</li>
<li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas - MSC3381</li>
<li xml:lang="gl">Enquisas MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
<li xml:lang="ka">Polls - MSC3381</li>
<li xml:lang="ko">투표 - MSC3381</li>
<li xml:lang="nl">Polls - MSC3381</li>
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
<li xml:lang="tr">Anketler - MSC3381</li>
<li xml:lang="uk">Опитування - MSC3381</li>
@@ -172,17 +171,21 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - 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>
<li xml:lang="eu">Eranskailu paketeak - MSC2545</li>
<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="gl">Paquetes de adhesivos MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
<li xml:lang="ko">스티커 팩 - MSC2545</li>
<li xml:lang="nl">Sticker Packs - MSC2545</li>
<li xml:lang="nn">Klistremerke-pakkar  MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li>
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
@@ -192,17 +195,21 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - 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>
<li xml:lang="eu">Kokaleku-gertaerak - MSC3488</li>
<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="gl">Localización de eventos MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
<li xml:lang="nn">Posisjonshendingar  MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li>
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
@@ -261,32 +268,6 @@ to provide a convergent experience across multiple platforms.</p>
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::windows_store::screenshots::1::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</value>
<value key="KDE::windows_store::screenshots::1::caption">Main view with room list, chat, and room information</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</value>
<value key="KDE::windows_store::screenshots::1::caption" xml:lang="x-test">xxMain view with room list, chat, and room informationxx</value>
<value key="KDE::windows_store::screenshots::2::image">https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</value>
<value key="KDE::windows_store::screenshots::2::caption">Login screen</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ca-valencia">Pantalla d'inici de sessió</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="es">Pantalla de inicio de sesión</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="fr">Écran de connexion</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="ka">შესვლის ეკრანი</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="nl">Aanmeldscherm</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="pt">Ecrã de autenticação</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="sl">Prijavni zaslon</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="tr">Oturum açma ekranı</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="uk">Вікно входу</value>
<value key="KDE::windows_store::screenshots::2::caption" xml:lang="x-test">xxLogin screenxx</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -296,15 +277,77 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
<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="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>
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</caption>
<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="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
<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="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
<caption xml:lang="ko">로그인 화면</caption>
<caption xml:lang="nl">Aanmeldscherm</caption>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
<caption xml:lang="sv">Inloggningsfönster</caption>
<caption xml:lang="ta">நுழைவுத் திரை</caption>
<caption xml:lang="tr">Oturum açma ekranı</caption>
<caption xml:lang="uk">Вікно входу</caption>
<caption xml:lang="x-test">xxLogin screenxx</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>Apart from a visual overhaul, NeoChat can now display location events and also a map with the location of all the users currently broadcasting their location using Itineray's Matrix integration. Great for locating where your friends are.</p>
</description>
</release>
<release version="23.04.3" date="2023-07-06"/>
<release version="23.04.2" date="2023-06-08"/>
<release version="23.04.1" date="2023-05-11"/>
<release version="23.04.0" date="2023-04-20">
<url>https://kde.org/announcements/gear/23.04.0/#neochathttpsappskdeorgneochat</url>
<description>
<p>NeoChat improves its design with tweaks that provide a more compact layout and a simpler menu which works better for the collapsed room list.</p>
<p>We have also improved the video controls, added a new command /knock &lt;room-id&gt; to send a knock event to a room, and you can now edit a prior message inline, within the chat pane.</p>
<p>Other usability improvements include an overhaul of the keyboard navigation and shortcuts like Ctrl+PgUp/PgDn that allow you to skip from room to room.</p>
</description>
<artifacts>
<artifact type="binary" platform="x86_64-windows-msvc">
<location>https://download.kde.org/stable/release-service/23.04.0/windows/neochat-23.04.0-512-windows-cl-msvc2019-x86_64.exe</location>

View File

@@ -93,7 +93,7 @@ Comment[es]=Cliente para el protocolo Matrix
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[gl]=Cliente para o protocolo 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

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 % Swedish "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>NeoChat användarmanual</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat manualsida.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>2022-11-01</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Klient för att interagera med meddelandeprotokollet matrix</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Beskrivning</title>
<para
><command
>neochat</command
> är ett chattprogram för protokollet matrix som fungerar både på skrivbord och mobil. </para>
</refsect1>
<refsect1 id="options"
><title
>Väljare</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>Matrix webbadress för en användare eller ett rum, t.ex. matrix:u/användare:exempel.org. Det gör att NeoChat försöker öppna det angivna rummet eller konversationen. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Rapportera fel</title>
<para
>Rapportera fel och funktionsönskemål på <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
>Se också</title>
<simplelist>
<member
>En lista över vanliga frågor om 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
>Copyright</title>
<para
>Copyright &copy; 2020-2022 Tobias Fella </para>
<para
>Copyright &copy; 2020-2022 Carl Schwan </para>
<para
>Licens: GNU General Public Version 3 eller senare &lt;<ulink url="https://wwwgnuorg/licenses/gpl-html"
>http://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
configure_file(qml/Page/RoomList/RoomDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Page/RoomList/RoomDelegate.qml)
configure_file(qml/Component/QuickSwitcher.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/QuickSwitcher.qml)
configure_file(qml/Dialog/UserDetailDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/UserDetailDialog.qml)
configure_file(qml/Dialog/PowerLevelDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/PowerLevelDialog.qml)
configure_file(qml/Component/Timeline/AudioDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/AudioDelegate.qml)
configure_file(qml/Component/Timeline/VideoDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/VideoDelegate.qml)
@@ -44,8 +43,6 @@ add_library(neochat STATIC
roommanager.h
neochatroom.cpp
neochatroom.h
neochatuser.cpp
neochatuser.h
models/userlistmodel.cpp
models/userlistmodel.h
models/userfiltermodel.cpp
@@ -77,8 +74,6 @@ add_library(neochat STATIC
blurhash.h
blurhashimageprovider.cpp
blurhashimageprovider.h
models/collapsestateproxymodel.cpp
models/collapsestateproxymodel.h
models/mediamessagefiltermodel.cpp
models/mediamessagefiltermodel.h
urlhelper.cpp
@@ -127,6 +122,10 @@ add_library(neochat STATIC
locationhelper.h
events/pollevent.cpp
pollhandler.cpp
utils.h
registration.cpp
neochatconnection.cpp
neochatconnection.h
)
ecm_qt_declare_logging_category(neochat
@@ -143,6 +142,11 @@ add_executable(neochat-app
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
)
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
endif()
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
@@ -157,6 +161,9 @@ if(NOT ANDROID)
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
if(QT_MAJOR_VERSION STREQUAL "6")
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
endif()
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
@@ -175,7 +182,8 @@ else()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)

View File

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

View File

@@ -218,21 +218,21 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
void ChatDocumentHandler::complete(int index)
{
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(name % " ");
cursor.insertText(name + QStringLiteral(" "));
cursor.setPosition(at);
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, name, 0, 0, id});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('/'));
QTextCursor cursor(document()->textDocument());
@@ -240,20 +240,20 @@ void ChatDocumentHandler::complete(int index)
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(QStringLiteral("/%1 ").arg(command));
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
auto text = getText();
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
QTextCursor cursor(document()->textDocument());
cursor.setPosition(at);
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
cursor.insertText(alias % " ");
cursor.insertText(alias + QStringLiteral(" "));
cursor.setPosition(at);
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
cursor.setKeepPositionOnInsert(true);
pushMention({cursor, alias, 0, 0, alias});
m_highlighter->rehighlight();
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
auto text = getText();
auto at = text.lastIndexOf(QLatin1Char(':'));
QTextCursor cursor(document()->textDocument());

View File

@@ -104,12 +104,12 @@ class ChatDocumentHandler : public QObject
/**
* @brief The color to highlight user mentions.
*/
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
/**
* @brief The color to highlight spelling errors.
*/
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ Login::Login(QObject *parent)
void Login::init()
{
m_homeserverReachable = false;
m_connection = new Connection();
m_connection = new NeoChatConnection();
m_matrixId = QString();
m_password = QString();
m_deviceName = QStringLiteral("NeoChat %1 %2 %3 %4")
@@ -33,12 +33,12 @@ void Login::init()
connect(this, &Login::matrixIdChanged, this, [this]() {
setHomeserverReachable(false);
QRegularExpression validator("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$");
QRegularExpression validator(QStringLiteral("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$"));
if (!validator.match(m_matrixId).hasMatch()) {
return;
}
if (m_matrixId == "@") {
if (m_matrixId == QLatin1Char('@')) {
return;
}
@@ -51,7 +51,7 @@ void Login::init()
m_testing = true;
Q_EMIT testingChanged();
if (!m_connection) {
m_connection = new Connection();
m_connection = new NeoChatConnection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -87,7 +87,11 @@ void Login::init()
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, this, [this](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
if (error == QStringLiteral("Invalid username or password")) {
setInvalidPassword(true);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
}
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
@@ -120,8 +124,8 @@ QString Login::matrixId() const
void Login::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if (!m_matrixId.startsWith('@')) {
m_matrixId.prepend('@');
if (!m_matrixId.startsWith(QLatin1Char('@'))) {
m_matrixId.prepend(QLatin1Char('@'));
}
Q_EMIT matrixIdChanged();
}
@@ -133,6 +137,7 @@ QString Login::password() const
void Login::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
@@ -155,7 +160,7 @@ void Login::login()
// Some servers do not have a .well_known file. So we login via the username part from the mxid,
// rather than with the full mxid, as that would lead to an invalid user.
auto username = m_matrixId.mid(1, m_matrixId.indexOf(":") - 1);
auto username = m_matrixId.mid(1, m_matrixId.indexOf(QLatin1Char(':')) - 1);
m_connection->loginWithPassword(username, m_password, m_deviceName, QString());
}
@@ -199,4 +204,15 @@ bool Login::isLoggedIn() const
return m_isLoggedIn;
}
void Login::setInvalidPassword(bool invalid)
{
m_invalidPassword = invalid;
Q_EMIT isInvalidPasswordChanged();
}
bool Login::isInvalidPassword() const
{
return m_invalidPassword;
}
#include "moc_login.cpp"

View File

@@ -6,10 +6,7 @@
#include <QObject>
#include <QUrl>
namespace Quotient
{
class Connection;
}
class NeoChatConnection;
/**
* @class Login
@@ -73,6 +70,11 @@ class Login : public QObject
*/
Q_PROPERTY(bool isLoggedIn READ isLoggedIn NOTIFY isLoggedInChanged)
/**
* @brief Whether the password (or the username) is invalid.
*/
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit Login(QObject *parent = nullptr);
@@ -100,6 +102,9 @@ public:
bool isLoggedIn() const;
bool isInvalidPassword() const;
void setInvalidPassword(bool invalid);
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
@@ -116,6 +121,7 @@ Q_SIGNALS:
void testingChanged();
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
private:
void setHomeserverReachable(bool reachable);
@@ -126,9 +132,10 @@ private:
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
Quotient::Connection *m_connection = nullptr;
NeoChatConnection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing = false;
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
};

View File

@@ -3,7 +3,9 @@
#include <QCommandLineParser>
#include <QIcon>
#include <QNetworkDiskCache>
#include <QNetworkProxyFactory>
#include <QObject>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQmlNetworkAccessManagerFactory>
@@ -16,6 +18,10 @@
#include <QApplication>
#endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
@@ -28,10 +34,11 @@
#include "neochat-version.h"
#include <Quotient/keyverificationsession.h>
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include <Quotient/room.h>
#include <Quotient/user.h>
#include <Quotient/util.h>
#include "actionshandler.h"
@@ -47,7 +54,6 @@
#include "login.h"
#include "matriximageprovider.h"
#include "models/accountemoticonmodel.h"
#include "models/collapsestateproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
@@ -74,8 +80,8 @@
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
#include "roommanager.h"
@@ -88,12 +94,12 @@
#endif
#include "models/completionmodel.h"
#include "models/statemodel.h"
#include "neochatuser.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -105,7 +111,17 @@ class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
QNetworkAccessManager *create(QObject *) override
{
return NetworkAccessManager::instance();
auto nam = NetworkAccessManager::instance();
nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/hsts/"));
nam->setStrictTransportSecurityEnabled(true);
auto namDiskCache = new QNetworkDiskCache(nam);
namDiskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/nam/"));
nam->setCache(namDiskCache);
return nam;
}
};
@@ -128,11 +144,15 @@ int main(int argc, char *argv[])
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
#endif
#ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze"));
#else
QIcon::setFallbackThemeName("breeze");
QIcon::setFallbackThemeName("breeze"_ls);
QApplication app(argc, argv);
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
@@ -154,7 +174,7 @@ int main(int argc, char *argv[])
#endif
KLocalizedString::setApplicationDomain("neochat");
QGuiApplication::setOrganizationName("KDE");
QGuiApplication::setOrganizationName("KDE"_ls);
KAboutData about(QStringLiteral("neochat"),
i18n("NeoChat"),
@@ -191,7 +211,8 @@ int main(int argc, char *argv[])
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
QFile::copy(QStringLiteral("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"),
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
#endif
Clipboard clipboard;
@@ -221,6 +242,7 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
@@ -228,7 +250,6 @@ int main(int argc, char *argv[])
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
@@ -253,34 +274,32 @@ int main(int argc, char *argv[])
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM");
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM");
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM");
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM");
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
qmlRegisterUncreatableType<NeoChatUser>("org.kde.neochat", 1, 0, "NeoChatUser", {});
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"_ls);
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"_ls);
qmlRegisterUncreatableType<User>("org.kde.neochat", 1, 0, "User", {});
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<Connection *>("Connection*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<User *>("User*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
#ifdef Quotient_E2EE_ENABLED
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
#endif
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(KAboutData::applicationData());
});
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"_ls), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "LocationHelper", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(LocationHelper());
});
@@ -323,11 +342,18 @@ int main(int argc, char *argv[])
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
if (parser.isSet("ignore-ssl-errors"_ls)) {
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
reply->ignoreSslErrors();
});
}
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
@@ -342,7 +368,7 @@ int main(int argc, char *argv[])
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner", &runner, QDBusConnection::ExportScriptableContents);
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
#endif
QWindow *window = windowFromEngine(&engine);

View File

@@ -25,18 +25,24 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, errorStr("Image request hasn't started")
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
requestedSize.setHeight(100);
requestedSize.setWidth(100);
}
if (mediaId.count('/') != 1) {
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
Q_EMIT finished();
return;
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;

View File

@@ -34,7 +34,7 @@ private Q_SLOTS:
void prepareResult();
private:
const QString mediaId;
QString mediaId;
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;

View File

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

View File

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

View File

@@ -1,177 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include <KLocalizedString>
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|| (source_row < sourceModel()->rowCount() - 1
&& sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::DelegateType::State) // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool(); // If it's a new day, show it
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
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)
!= MessageEventModel::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
)) {
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();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = "<style>a {text-decoration: none;}</style>"; // There can be links in the event text so make sure all are styled.
// 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\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()["id"].toString(),
uniqueAuthors[0].toMap()["color"].toString(),
uniqueAuthors[0].toMap()["displayName"].toString());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{"author", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{"authorDisplayName", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
return stateEvents;
}
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString CollapseStateProxyModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_collapsestateproxymodel.cpp"

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "messageeventmodel.h"
#include <QSortFilterProxyModel>
/**
* @class CollapseStateProxyModel
*
* This model aggregates multiple sequential state events into a single entry.
*
* Events are only aggregated if they happened on the same day.
*
* @sa MessageEventModel
*/
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -53,54 +53,55 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
}
auto filterIndex = m_filterModel->index(index.row(), 0);
if (m_autoCompletionType == User) {
if (role == Text) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, UserListModel::DisplayNameRole);
}
if (role == Subtitle) {
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, UserListModel::UserIdRole);
}
if (role == Icon) {
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, UserListModel::AvatarRole);
}
}
if (m_autoCompletionType == Command) {
if (role == Text) {
return m_filterModel->data(filterIndex, ActionsModel::Prefix).toString() + QStringLiteral(" ")
+ m_filterModel->data(filterIndex, ActionsModel::Parameters).toString();
if (role == DisplayNameRole) {
return QStringLiteral("%1 %2").arg(m_filterModel->data(filterIndex, ActionsModel::Prefix).toString(),
m_filterModel->data(filterIndex, ActionsModel::Parameters).toString());
}
if (role == Subtitle) {
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, ActionsModel::Description);
}
if (role == Icon) {
if (role == IconNameRole) {
return QStringLiteral("invalid");
}
if (role == ReplacedText) {
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, ActionsModel::Prefix);
}
}
if (m_autoCompletionType == Room) {
if (role == Text) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, RoomListModel::DisplayNameRole);
}
if (role == Subtitle) {
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
if (role == Icon) {
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole);
if (role == IconNameRole) {
return QStringLiteral("mxc://%1")
.arg(m_roomListModel->connection()->makeMediaUrl(m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toUrl()).toString());
}
}
if (m_autoCompletionType == Emoji) {
if (role == Text) {
if (role == DisplayNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
}
if (role == Icon) {
if (role == IconNameRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
}
if (role == ReplacedText) {
if (role == ReplacedTextRole) {
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
}
if (role == Subtitle) {
if (role == SubtitleRole) {
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
}
}
@@ -111,10 +112,10 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> CompletionModel::roleNames() const
{
return {
{Text, "text"},
{Subtitle, "subtitle"},
{Icon, "icon"},
{ReplacedText, "replacedText"},
{DisplayNameRole, "displayName"},
{SubtitleRole, "subtitle"},
{IconNameRole, "iconName"},
{ReplacedTextRole, "replacedText"},
};
}
@@ -144,7 +145,7 @@ void CompletionModel::updateCompletion()
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();
} else if (text().startsWith(QLatin1Char(':')) && !text()[1].isUpper()
} else if (text().startsWith(QLatin1Char(':')) && text().size() > 1 && !text()[1].isUpper()
&& (m_fullText.indexOf(QLatin1Char(':'), 1) == -1
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);

View File

@@ -40,12 +40,12 @@ class CompletionModel : public QAbstractListModel
*
* @sa AutoCompletionType
*/
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged);
Q_PROPERTY(AutoCompletionType autoCompletionType READ autoCompletionType NOTIFY autoCompletionTypeChanged)
/**
* @brief The RoomListModel to be used for room completions.
*/
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged);
Q_PROPERTY(RoomListModel *roomListModel READ roomListModel WRITE setRoomListModel NOTIFY roomListModelChanged)
public:
/**
@@ -64,10 +64,10 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
Text = Qt::DisplayRole, /**< The main text to show. */
Subtitle, /**< The subtitle text to show. */
Icon, /**< The icon to show. */
ReplacedText, /**< The text to replace the input text with for the completion. */
DisplayNameRole = Qt::DisplayRole, /**< The main text to show. */
SubtitleRole, /**< The subtitle text to show. */
IconNameRole, /**< The icon to show. */
ReplacedTextRole, /**< The text to replace the input text with for the completion. */
};
Q_ENUM(Roles)

View File

@@ -11,6 +11,11 @@ bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &so
if (m_filterText.isEmpty()) {
return false;
}
if (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().isEmpty()) {
return false;
}
return (sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString().startsWith(m_filterText, Qt::CaseInsensitive)
&& !m_fullText.startsWith(sourceModel()->data(sourceModel()->index(sourceRow, 0), filterRole()).toString()))
|| (m_secondaryFilterRole != -1

View File

@@ -21,14 +21,14 @@ void CustomEmojiModel::fetchEmojis()
return;
}
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
if (data == nullptr) {
return;
}
QJsonObject emojis = data->contentJson()["images"].toObject();
QJsonObject emojis = data->contentJson()["images"_ls].toObject();
// TODO: Remove with stable migration
const auto legacyEmojis = data->contentJson()["emoticons"].toObject();
const auto legacyEmojis = data->contentJson()["emoticons"_ls].toObject();
for (const auto &emoji : legacyEmojis.keys()) {
if (!emojis.contains(emoji)) {
emojis[emoji] = legacyEmojis[emoji];
@@ -41,9 +41,9 @@ void CustomEmojiModel::fetchEmojis()
for (const auto &emoji : emojis.keys()) {
const auto &data = emojis[emoji];
const auto e = emoji.startsWith(":") ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
const auto e = emoji.startsWith(":"_ls) ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
m_emojis << CustomEmoji{e, data.toObject()["url"].toString(), QRegularExpression(e)};
m_emojis << CustomEmoji{e, data.toObject()["url"_ls].toString(), QRegularExpression(e)};
}
endResetModel();
@@ -56,19 +56,19 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"].toObject();
auto emojiData = json["images"_ls].toObject();
QString url;
url = job->contentUri().toString();
QImage image(location.toLocalFile());
QJsonObject imageInfo;
imageInfo["w"] = image.width();
imageInfo["h"] = image.height();
imageInfo["mimetype"] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
imageInfo["size"] = image.sizeInBytes();
imageInfo["w"_ls] = image.width();
imageInfo["h"_ls] = image.height();
imageInfo["mimetype"_ls] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
imageInfo["size"_ls] = image.sizeInBytes();
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
{QStringLiteral("url"), url},
@@ -77,8 +77,8 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{"usage"_ls, "emoticon"_ls},
});
json["images"] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
json["images"_ls] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
});
}
@@ -86,30 +86,30 @@ void CustomEmojiModel::removeEmoji(const QString &name)
{
using namespace Quotient;
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes");
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
Q_ASSERT(data);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
auto emojiData = json["images"].toObject();
auto emojiData = json["images"_ls].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["images"] = emojiData;
json["images"_ls] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["images"] = emojiData;
json["images"_ls] = emojiData;
}
emojiData = json["emoticons"].toObject();
emojiData = json["emoticons"_ls].toObject();
if (emojiData.contains(name)) {
emojiData.remove(name);
json["emoticons"] = emojiData;
json["emoticons"_ls] = emojiData;
}
if (emojiData.contains(_name)) {
emojiData.remove(_name);
json["emoticons"] = emojiData;
json["emoticons"_ls] = emojiData;
}
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes", json);
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
}
CustomEmojiModel::CustomEmojiModel(QObject *parent)
@@ -174,7 +174,7 @@ QHash<int, QByteArray> CustomEmojiModel::roleNames() const
QString CustomEmojiModel::preprocessText(const QString &text)
{
auto parts = text.split("```");
auto parts = text.split("```"_ls);
bool skip = true;
for (auto &part : parts) {
skip = !skip;
@@ -187,7 +187,7 @@ QString CustomEmojiModel::preprocessText(const QString &text)
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
}
}
return parts.join("```");
return parts.join("```"_ls);
}
QVariantList CustomEmojiModel::filterModel(const QString &filter)

View File

@@ -5,6 +5,9 @@
#include "controller.h"
#include <QDateTime>
#include <QLocale>
#include <KLocalizedString>
#include <Quotient/csapi/device_management.h>
@@ -52,6 +55,12 @@ QVariant DevicesModel::data(const QModelIndex &index, int role) const
} else {
return false;
}
case TimestampString:
if (device.lastSeenTs) {
return QDateTime::fromMSecsSinceEpoch(*device.lastSeenTs).toString(QLocale().dateTimeFormat(QLocale::ShortFormat));
} else {
return false;
}
case Type:
if (device.deviceId == m_connection->deviceId()) {
return This;
@@ -81,6 +90,7 @@ QHash<int, QByteArray> DevicesModel::roleNames() const
{DisplayName, "displayName"},
{LastIp, "lastIp"},
{LastTimestamp, "lastTimestamp"},
{TimestampString, "timestamp"},
{Type, "type"},
};
}
@@ -103,11 +113,11 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
if (job->error() != BaseJob::Success) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
authData["password"] = password;
authData["type"] = "m.login.password";
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier;
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls;
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, Controller::instance().activeConnection()->user()->id()}};
authData["identifier"_ls] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess);
} else {

View File

@@ -40,6 +40,7 @@ public:
DisplayName, /**< Display name set by the user for this device. */
LastIp, /**< The IP address where this device was last seen. */
LastTimestamp, /**< The timestamp when this devices was last seen. */
TimestampString, /**< String for the timestamp when this devices was last seen. */
Type, /**< The category to sort this device into. */
};
Q_ENUM(Roles)

View File

@@ -8,7 +8,7 @@
class DevicesProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged);
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged)
public:
DevicesProxyModel(QObject *parent = nullptr);

View File

@@ -63,7 +63,7 @@ QHash<int, QByteArray> EmojiModel::roleNames() const
QVariantList EmojiModel::history() const
{
return m_settings.value("Editor/emojis", QVariantList()).toList();
return m_settings.value(QStringLiteral("Editor/emojis"), QVariantList()).toList();
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
@@ -105,7 +105,7 @@ void EmojiModel::emojiUsed(const QVariant &modelData)
}
list.push_front(modelData);
m_settings.setValue("Editor/emojis", list);
m_settings.setValue(QStringLiteral("Editor/emojis"), list);
Q_EMIT historyChanged();
}
@@ -133,8 +133,8 @@ QVariantList EmojiModel::emojis(Category category) const
QVariantList EmojiModel::tones(const QString &baseEmoji) const
{
if (baseEmoji.endsWith("tone")) {
return EmojiTones::_tones.values(baseEmoji.split(":")[0]);
if (baseEmoji.endsWith(QStringLiteral("tone"))) {
return EmojiTones::_tones.values(baseEmoji.split(QStringLiteral(":"))[0]);
}
return EmojiTones::_tones.values(baseEmoji);
}
@@ -145,54 +145,54 @@ QVariantList EmojiModel::categories() const
{
return QVariantList{
{QVariantMap{
{"category", EmojiModel::HistoryNoCustom},
{"name", i18nc("Previously used emojis", "History")},
{"emoji", QStringLiteral("⌛️")},
{QStringLiteral("category"), EmojiModel::HistoryNoCustom},
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
}},
{QVariantMap{
{"category", EmojiModel::Smileys},
{"name", i18nc("'Smileys' is a category of emoji", "Smileys")},
{"emoji", QStringLiteral("😏")},
{QStringLiteral("category"), EmojiModel::Smileys},
{QStringLiteral("name"), i18nc("'Smileys' is a category of emoji", "Smileys")},
{QStringLiteral("emoji"), QStringLiteral("😏")},
}},
{QVariantMap{
{"category", EmojiModel::People},
{"name", i18nc("'People' is a category of emoji", "People")},
{"emoji", QStringLiteral("🙋‍♂️")},
{QStringLiteral("category"), EmojiModel::People},
{QStringLiteral("name"), i18nc("'People' is a category of emoji", "People")},
{QStringLiteral("emoji"), QStringLiteral("🙋‍♂️")},
}},
{QVariantMap{
{"category", EmojiModel::Nature},
{"name", i18nc("'Nature' is a category of emoji", "Nature")},
{"emoji", QStringLiteral("🌲")},
{QStringLiteral("category"), EmojiModel::Nature},
{QStringLiteral("name"), i18nc("'Nature' is a category of emoji", "Nature")},
{QStringLiteral("emoji"), QStringLiteral("🌲")},
}},
{QVariantMap{
{"category", EmojiModel::Food},
{"name", i18nc("'Food' is a category of emoji", "Food")},
{"emoji", QStringLiteral("🍛")},
{QStringLiteral("category"), EmojiModel::Food},
{QStringLiteral("name"), i18nc("'Food' is a category of emoji", "Food")},
{QStringLiteral("emoji"), QStringLiteral("🍛")},
}},
{QVariantMap{
{"category", EmojiModel::Activities},
{"name", i18nc("'Activities' is a category of emoji", "Activities")},
{"emoji", QStringLiteral("🚁")},
{QStringLiteral("category"), EmojiModel::Activities},
{QStringLiteral("name"), i18nc("'Activities' is a category of emoji", "Activities")},
{QStringLiteral("emoji"), QStringLiteral("🚁")},
}},
{QVariantMap{
{"category", EmojiModel::Travel},
{"name", i18nc("'Travel' is a category of emoji", "Travel")},
{"emoji", QStringLiteral("🚅")},
{QStringLiteral("category"), EmojiModel::Travel},
{QStringLiteral("name"), i18nc("'Travel' is a category of emoji", "Travel")},
{QStringLiteral("emoji"), QStringLiteral("🚅")},
}},
{QVariantMap{
{"category", EmojiModel::Objects},
{"name", i18nc("'Objects' is a category of emoji", "Objects")},
{"emoji", QStringLiteral("💡")},
{QStringLiteral("category"), EmojiModel::Objects},
{QStringLiteral("name"), i18nc("'Objects' is a category of emoji", "Objects")},
{QStringLiteral("emoji"), QStringLiteral("💡")},
}},
{QVariantMap{
{"category", EmojiModel::Symbols},
{"name", i18nc("'Symbols' is a category of emoji", "Symbols")},
{"emoji", QStringLiteral("🔣")},
{QStringLiteral("category"), EmojiModel::Symbols},
{QStringLiteral("name"), i18nc("'Symbols' is a category of emoji", "Symbols")},
{QStringLiteral("emoji"), QStringLiteral("🔣")},
}},
{QVariantMap{
{"category", EmojiModel::Flags},
{"name", i18nc("'Flags' is a category of emoji", "Flags")},
{"emoji", QStringLiteral("🏁")},
{QStringLiteral("category"), EmojiModel::Flags},
{QStringLiteral("name"), i18nc("'Flags' is a category of emoji", "Flags")},
{QStringLiteral("emoji"), QStringLiteral("🏁")},
}},
};
}
@@ -203,15 +203,15 @@ QVariantList EmojiModel::categoriesWithCustom() const
cats.removeAt(0);
cats.insert(0,
QVariantMap{
{"category", EmojiModel::History},
{"name", i18nc("Previously used emojis", "History")},
{"emoji", QStringLiteral("⌛️")},
{QStringLiteral("category"), EmojiModel::History},
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
});
cats.insert(1,
QVariantMap{
{"category", EmojiModel::Custom},
{"name", i18nc("'Custom' is a category of emoji", "Custom")},
{"emoji", QStringLiteral("🖼️")},
{QStringLiteral("category"), EmojiModel::Custom},
{QStringLiteral("name"), i18nc("'Custom' is a category of emoji", "Custom")},
{QStringLiteral("emoji"), QStringLiteral("🖼️")},
});
;
return cats;

View File

@@ -33,7 +33,7 @@ struct Emoji {
{
arch >> object.unicode;
arch >> object.shortName;
object.isCustom = object.unicode.startsWith("image://");
object.isCustom = object.unicode.startsWith(QStringLiteral("image://"));
return arch;
}

View File

@@ -82,8 +82,9 @@ void ImagePacksModel::reloadImages()
// Load emoticons from the account data
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
json["pack"] = QJsonObject{
{"display_name", m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
json["pack"_ls] = QJsonObject{
{"display_name"_ls,
m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
};
const auto &content = ImagePackEventContent(json);
if (!content.images.isEmpty()) {
@@ -104,8 +105,8 @@ void ImagePacksModel::reloadImages()
for (const auto &packKey : packs.keys()) {
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
const auto packContent = pack->content();
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|| (packContent.pack->usage->contains("sticker") && showStickers()))
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers()))
&& !packContent.images.isEmpty()) {
m_events += packContent;
}
@@ -115,12 +116,12 @@ void ImagePacksModel::reloadImages()
}
// Load emoticons from the current room
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes");
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
for (const auto &event : events) {
auto packContent = eventCast<const ImagePackEvent>(event)->content();
if (packContent.pack.has_value()) {
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon") && showEmoticons())
|| (packContent.pack->usage->contains("sticker") && showStickers())) {
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers())) {
m_events += packContent;
}
}

View File

@@ -8,6 +8,8 @@
#include <QDebug>
#include <cmath>
using namespace Quotient;
bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs)
@@ -76,7 +78,7 @@ QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
return longitude.toFloat();
}
case AssetRole:
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"].toString();
return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
case AuthorRole:
return m_room->getUser(data.senderId);
case IsLiveRole: {
@@ -88,6 +90,11 @@ QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const
const auto timeout = data.beaconInfo.value("timeout"_ls).toDouble(600000);
return lastTs + timeout >= QDateTime::currentDateTime().toMSecsSinceEpoch();
}
case HeadingRole: {
bool success = false;
const auto heading = data.beacon["org.matrix.msc3488.location"_ls].toObject()["org.kde.itinerary.heading"_ls].toString().toDouble(&success);
return success ? heading : NAN;
}
}
return {};
@@ -101,6 +108,7 @@ QHash<int, QByteArray> LiveLocationsModel::roleNames() const
r.insert(AssetRole, "asset");
r.insert(AuthorRole, "author");
r.insert(IsLiveRole, "isLive");
r.insert(HeadingRole, "heading");
return r;
}
@@ -121,7 +129,7 @@ QRectF LiveLocationsModel::boundingBox() const
void LiveLocationsModel::addEvent(const Quotient::RoomEvent *event)
{
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info") {
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
LiveLocationData data;
data.senderId = event->senderId();
data.beaconInfo = event->contentJson();

View File

@@ -49,6 +49,7 @@ public:
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
AuthorRole, /**< The author of the event. */
IsLiveRole, /**< Boolean that indicates whether a live location beacon is still live. */
HeadingRole, /**< Heading in degree (not part of any MSC yet, using an Itinerary extension). */
};
Q_ENUM(Roles)

View File

@@ -3,6 +3,8 @@
#include "locationsmodel.h"
#include <QGuiApplication>
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
@@ -13,7 +15,7 @@ LocationsModel::LocationsModel(QObject *parent)
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
if (event->contentJson()["msgtype"_ls] == "m.location"_ls) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
@@ -23,7 +25,7 @@ LocationsModel::LocationsModel(QObject *parent)
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
if (event->contentJson()["msgtype"_ls] == "m.location"_ls) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
@@ -34,7 +36,7 @@ LocationsModel::LocationsModel(QObject *parent)
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
if (event->contentJson()["msgtype"_ls] == "m.location"_ls) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
@@ -43,11 +45,15 @@ LocationsModel::LocationsModel(QObject *parent)
});
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
});
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
const auto uri = event->contentJson()["org.matrix.msc3488.location"_ls]["uri"_ls].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
if (parts.size() < 2) {
qWarning() << "invalid geo: URI" << uri;
@@ -61,7 +67,7 @@ void LocationsModel::addLocation(const RoomMessageEvent *event)
.latitude = latitude,
.longitude = longitude,
.content = event->contentJson(),
.author = dynamic_cast<NeoChatUser *>(m_room->user(event->senderId())),
.author = m_room->user(event->senderId()),
};
endInsertRows();
}

View File

@@ -10,6 +10,7 @@
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/user.h>
class LocationsModel : public QAbstractListModel
{
@@ -51,7 +52,7 @@ private:
float latitude;
float longitude;
QJsonObject content;
NeoChatUser *author;
Quotient::User *author;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);

View File

@@ -25,7 +25,7 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["source"].toUrl();
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
@@ -39,7 +39,7 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
}
}
if (role == TempSourceRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["tempInfo"].toMap()["source"].toUrl();
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
@@ -52,10 +52,10 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["width"].toFloat();
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("width")].toFloat();
}
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()["height"].toFloat();
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
return sourceModel()->data(mapToSource(index), role);

View File

@@ -5,7 +5,7 @@
#include <QSortFilterProxyModel>
#include "models/collapsestateproxymodel.h"
#include "models/messagefiltermodel.h"
/**
* @class MediaMessageFilterModel
@@ -22,7 +22,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
SourceRole = CollapseStateProxyModel::LastRole + 1, /**< The mxc source URL for the item. */
SourceRole = MessageFilterModel::LastRole + 1, /**< The mxc source URL for the item. */
TempSourceRole, /**< Source for the temporary content (either blurhash or mxc URL). */
TypeRole, /**< The type of the media (image or video). */
CaptionRole, /**< The caption for the item. */

View File

@@ -20,12 +20,12 @@
#include <QDebug>
#include <QGuiApplication>
#include <QMovie>
#include <QTimeZone>
#include <KLocalizedString>
#include "models/reactionmodel.h"
#include "neochatuser.h"
#include "texthandler.h"
using namespace Quotient;
@@ -78,7 +78,7 @@ MessageEventModel::MessageEventModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyRole});
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
});
}
@@ -531,7 +531,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AuthorRole) {
auto author = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId()));
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->getUser(author);
}
@@ -579,7 +579,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (isPending) {
// A pending event with an m.new_content key will be merged into the
// original event so don't show.
if (evt.contentJson().contains("m.new_content")) {
if (evt.contentJson().contains("m.new_content"_ls)) {
return EventStatus::Hidden;
}
return pendingIt->deliveryStatus();
@@ -668,18 +668,18 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == IsReplyRole) {
return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
return !evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
if (role == ReplyIdRole) {
return evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
return evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
if (role == ReplyAuthor) {
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (replyPtr) {
auto replyUser = static_cast<NeoChatUser *>(m_currentRoom->user(replyPtr->senderId()));
auto replyUser = m_currentRoom->user(replyPtr->senderId());
return m_currentRoom->getUser(replyUser);
} else {
return m_currentRoom->getUser(nullptr);
@@ -733,8 +733,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
return QVariantMap{
{"display", m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
{"type", type},
{"display"_ls, m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
}
@@ -789,7 +789,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == AssetRole) {
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"].toObject()["type"].toString();
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
@@ -808,7 +808,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
auto user = m_currentRoom->user(userId);
users += m_currentRoom->getUser(user);
}
@@ -820,7 +820,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
userIds.remove(m_currentRoom->localUser()->id());
if (userIds.count() > 5) {
return QStringLiteral("+ ") + QString::number(userIds.count() - 5);
return QStringLiteral("+ %1").arg(userIds.count() - 5);
} else {
return QString();
}
@@ -836,7 +836,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId));
auto user = m_currentRoom->user(userId);
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
@@ -866,30 +866,28 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == VerifiedRole) {
#ifdef Quotient_E2EE_ENABLED
if (evt.originalEvent()) {
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
Q_ASSERT(encrypted);
return m_currentRoom->connection()->isVerifiedSession(encrypted->sessionId().toLatin1());
}
#endif
return false;
}
if (role == DisplayNameForInitialsRole) {
auto user = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId()));
auto user = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id()));
}
if (role == AuthorDisplayNameRole) {
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"]["displayname"].isNull() && evt.stateKey() == evt.senderId()) {
auto previousDisplayName = evt.unsignedJson()["prev_content"]["displayname"].toString().toHtmlEscaped();
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"_ls]["displayname"_ls].isNull() && evt.stateKey() == evt.senderId()) {
auto previousDisplayName = evt.unsignedJson()["prev_content"_ls]["displayname"_ls].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = evt.senderId();
}
return previousDisplayName;
} else {
auto author = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId()));
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->htmlSafeMemberName(author->id());
}
}
@@ -945,76 +943,79 @@ QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::File
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"] = QUrl();
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"] = source;
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"] = QUrl();
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"] = mimeType.name();
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"] = mimeType.iconName();
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"] = fileInfo->payloadSize;
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"] = castInfo->imageSize.width();
mediaInfo["height"] = castInfo->imageSize.height();
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"].toUrl().scheme() == "mxc") {
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"].toString();
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"] = QUrl();
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"] = QUrl("image://blurhash/" + blurhash);
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"] = tempInfo;
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"] = castInfo->imageSize.width();
mediaInfo["height"] = castInfo->imageSize.height();
mediaInfo["duration"] = castInfo->duration;
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"].toUrl().scheme() == "mxc") {
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"].toString();
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"] = QUrl();
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"] = QUrl("image://blurhash/" + blurhash);
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"] = tempInfo;
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"] = castInfo->duration;
mediaInfo["duration"_ls] = castInfo->duration;
}
}
@@ -1062,13 +1063,13 @@ void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageE
return;
};
QMap<QString, QList<NeoChatUser *>> reactions = {};
QMap<QString, QList<Quotient::User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId())));
reactions[e->key()].append(m_currentRoom->user(e->senderId()));
}
}
@@ -1095,7 +1096,7 @@ void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageE
if (m_reactionModels.contains(eventId)) {
m_reactionModels[eventId]->setReactions(res);
} else if (res.size() > 0) {
m_reactionModels[eventId] = new ReactionModel(this, res, static_cast<NeoChatUser *>(m_currentRoom->localUser()));
m_reactionModels[eventId] = new ReactionModel(this, res, m_currentRoom->localUser());
} else {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];

View File

@@ -3,6 +3,8 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include "messageeventmodel.h"
#include "neochatconfig.h"
@@ -32,22 +34,192 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
// Don't show redacted (i.e. deleted) messages.
if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) {
return false;
}
// Don't show hidden or replaced messages.
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
// Don't show events with an unknown type.
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
if (eventType == MessageEventModel::Other) {
return false;
}
// Don't show state events that are not the first in a consecutive group on the
// same day as they will be grouped as a single delegate.
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
const bool previousEventIsState =
sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == MessageEventModel::DelegateType::State;
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
if (eventType == MessageEventModel::State && notLastRow && previousEventIsState && !newDay) {
return false;
}
return true;
}
QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
} else if (role == StateEventsRole) {
return stateEventsList(mapToSource(index).row());
} else if (role == AuthorListRole) {
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> MessageFilterModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
return roles;
}
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
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)
!= MessageEventModel::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
)) {
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();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// 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\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}
QVariantList MessageFilterModel::stateEventsList(int sourceRow) const
{
QVariantList stateEvents;
for (int i = sourceRow; i >= 0; i--) {
auto nextState = QVariantMap{
{QStringLiteral("author"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
{QStringLiteral("authorDisplayName"), sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
{QStringLiteral("text"), sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
};
stateEvents.append(nextState);
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
return stateEvents;
}
QVariantList MessageFilterModel::authorList(int sourceRow) const
{
QVariantList uniqueAuthors;
for (int i = sourceRow; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
if (uniqueAuthors.count() > 5) {
uniqueAuthors = uniqueAuthors.mid(0, 5);
}
return uniqueAuthors;
}
QString MessageFilterModel::excessAuthors(int row) const
{
QVariantList uniqueAuthors;
for (int i = row; i >= 0; i--) {
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
if (!uniqueAuthors.contains(nextAvatar)) {
uniqueAuthors.append(nextAvatar);
}
if (i > 0
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
!= MessageEventModel::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
)) {
break;
}
}
int excessAuthors;
if (uniqueAuthors.count() > 5) {
excessAuthors = uniqueAuthors.count() - 5;
} else {
excessAuthors = 0;
}
QString excessAuthorsString;
if (excessAuthors == 0) {
return QString();
} else {
return QStringLiteral("+ %1").arg(excessAuthors);
}
}
#include "moc_messagefiltermodel.cpp"

View File

@@ -5,21 +5,79 @@
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
/**
* @class MessageFilterModel
*
* This model filters out any messages that should be hidden.
*
* Deleted messages are only hidden if the user hasn't set them to be shown.
*
* The model also contains the roles and functions to support aggregating multiple
* consecutive state events into a single delegate. The state events must all happen
* on the same day to be aggregated.
*/
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */
StateEventsRole, /**< List of state events in the aggregated state. */
AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */
ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr);
/**
* @brief Custom filter function to remove hidden messages.
*/
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
/**
* @brief Get the given role value at the given index.
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QString aggregateEventToString(int row) const;
/**
* @brief Return a list of consecutive state events starting at row.
*
* If state events happen on different days they will be split into two aggregate
* events.
*/
[[nodiscard]] QVariantList stateEventsList(int row) const;
/**
* @brief List of the first 5 unique authors for the aggregate state events starting at row.
*/
[[nodiscard]] QVariantList authorList(int row) const;
/**
* @brief The number of unique authors beyond the first 5 for the aggregate state events starting at row.
*/
[[nodiscard]] QString excessAuthors(int row) const;
};

View File

@@ -25,7 +25,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
beginResetModel();
nextBatch = "";
nextBatch = QString();
attempted = false;
rooms.clear();
m_server.clear();
@@ -41,6 +41,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
}
if (m_connection) {
@@ -67,15 +68,17 @@ void PublicRoomListModel::setServer(const QString &value)
beginResetModel();
nextBatch = "";
nextBatch = QString();
attempted = false;
rooms.clear();
Q_EMIT loadingChanged();
endResetModel();
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
}
if (m_connection) {
@@ -101,7 +104,7 @@ void PublicRoomListModel::setKeyword(const QString &value)
beginResetModel();
nextBatch = "";
nextBatch = QString();
attempted = false;
rooms.clear();
@@ -110,6 +113,7 @@ void PublicRoomListModel::setKeyword(const QString &value)
if (job) {
job->abandon();
job = nullptr;
Q_EMIT loadingChanged();
}
if (m_connection) {
@@ -133,6 +137,7 @@ void PublicRoomListModel::next(int count)
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, {}});
Q_EMIT loadingChanged();
connect(job, &BaseJob::finished, this, [this] {
attempted = true;
@@ -150,6 +155,7 @@ void PublicRoomListModel::next(int count)
}
this->job = nullptr;
Q_EMIT loadingChanged();
});
}
@@ -186,7 +192,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
auto avatarUrl = room.avatarUrl;
if (avatarUrl.isEmpty()) {
return "";
return {};
}
return avatarUrl.url().remove(0, 6);
}
@@ -253,4 +259,9 @@ bool PublicRoomListModel::hasMore() const
return !(attempted && nextBatch.isEmpty());
}
bool PublicRoomListModel::loading() const
{
return job != nullptr;
}
#include "moc_publicroomlistmodel.cpp"

View File

@@ -48,6 +48,11 @@ class PublicRoomListModel : public QAbstractListModel
*/
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged)
/**
* @biref Whether the model is still loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
/**
* @brief Defines the model roles.
@@ -98,6 +103,8 @@ public:
[[nodiscard]] bool hasMore() const;
[[nodiscard]] bool loading() const;
/**
* @brief Load the next set of rooms.
*
@@ -111,6 +118,7 @@ private:
QString m_keyword;
bool attempted = false;
bool m_loading = false;
QString nextBatch;
QVector<Quotient::PublicRoomsChunk> rooms;
@@ -122,4 +130,5 @@ Q_SIGNALS:
void serverChanged();
void keywordChanged();
void hasMoreChanged();
void loadingChanged();
};

View File

@@ -69,18 +69,20 @@ PushRuleModel::PushRuleModel(QObject *parent)
void PushRuleModel::controllerConnectionChanged()
{
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules("m.push_rules");
if (Controller::instance().activeConnection()) {
connect(Controller::instance().activeConnection(), &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
updateNotificationRules(QStringLiteral("m.push_rules"));
}
}
void PushRuleModel::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
if (type != QStringLiteral("m.push_rules")) {
return;
}
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson["global"].toObject());
const QJsonObject ruleDataJson = Controller::instance().activeConnection()->accountDataJson(QStringLiteral("m.push_rules"));
const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson[QStringLiteral("global")].toObject());
beginResetModel();
m_rules.clear();
@@ -201,7 +203,7 @@ bool PushRuleModel::globalNotificationsEnabled() const
void PushRuleModel::setGlobalNotificationsEnabled(bool enabled)
{
setNotificationRuleEnabled("override", ".m.rule.master", !enabled);
setNotificationRuleEnabled(QStringLiteral("override"), QStringLiteral(".m.rule.master"), !enabled);
}
bool PushRuleModel::globalNotificationsSet() const
@@ -302,26 +304,26 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
kind = PushNotificationKind::Override;
Quotient::PushCondition roomCondition;
roomCondition.kind = "event_match";
roomCondition.key = "room_id";
roomCondition.kind = QStringLiteral("event_match");
roomCondition.key = QStringLiteral("room_id");
roomCondition.pattern = roomId;
pushConditions.append(roomCondition);
Quotient::PushCondition keywordCondition;
keywordCondition.kind = "event_match";
keywordCondition.key = "content.body";
keywordCondition.kind = QStringLiteral("event_match");
keywordCondition.key = QStringLiteral("content.body");
keywordCondition.pattern = keyword;
pushConditions.append(keywordCondition);
}
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>("global",
auto job = Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind),
keyword,
actions,
QLatin1String(""),
QLatin1String(""),
QString(),
QString(),
pushConditions,
roomId.isEmpty() ? keyword : QLatin1String(""));
roomId.isEmpty() ? keyword : QString());
connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString();
});
@@ -340,7 +342,7 @@ void PushRuleModel::removeKeyword(const QString &keyword)
}
auto kind = PushNotificationKind::kindString(m_rules[index].kind);
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>("global", kind, m_rules[index].id);
auto job = Controller::instance().activeConnection()->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
});
@@ -348,10 +350,10 @@ void PushRuleModel::removeKeyword(const QString &keyword)
void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>("global", kind, ruleId);
auto job = Controller::instance().activeConnection()->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled]() {
if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleEnabledJob>("global", kind, ruleId, enabled);
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
}
});
}
@@ -359,13 +361,13 @@ void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QStrin
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QVector<QVariant> actions;
if (ruleId == ".m.rule.call") {
actions = actionToVariant(action, "ring");
if (ruleId == QStringLiteral(".m.rule.call")) {
actions = actionToVariant(action, QStringLiteral("ring"));
} else {
actions = actionToVariant(action);
}
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>("global", kind, ruleId, actions);
Controller::instance().activeConnection()->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
}
PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVariant> &actions, bool enabled)
@@ -383,10 +385,10 @@ PushNotificationAction::Action PushRuleModel::variantToAction(const QVector<QVar
}
QJsonObject action = i.toJsonObject();
if (action["set_tweak"].toString() == "sound") {
if (action[QStringLiteral("set_tweak")].toString() == QStringLiteral("sound")) {
isNoisy = true;
} else if (action["set_tweak"].toString() == "highlight") {
if (action["value"].toString() != "false") {
} else if (action[QStringLiteral("set_tweak")].toString() == QStringLiteral("highlight")) {
if (action[QStringLiteral("value")].toString() != QStringLiteral("false")) {
highlightEnabled = true;
}
}
@@ -423,19 +425,19 @@ QVector<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action
QVector<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append("notify");
actions.append(QStringLiteral("notify"));
} else {
actions.append("dont_notify");
actions.append(QStringLiteral("dont_notify"));
}
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) {
QJsonObject soundTweak;
soundTweak.insert("set_tweak", "sound");
soundTweak.insert("value", sound);
soundTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("sound"));
soundTweak.insert(QStringLiteral("value"), sound);
actions.append(soundTweak);
}
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) {
QJsonObject highlightTweak;
highlightTweak.insert("set_tweak", "highlight");
highlightTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("highlight"));
actions.append(highlightTweak);
}

View File

@@ -236,6 +236,6 @@ private:
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action);
PushNotificationAction::Action variantToAction(const QVector<QVariant> &actions, bool enabled);
QVector<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = "default");
QVector<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default"));
};
Q_DECLARE_METATYPE(PushRuleModel *)

View File

@@ -7,9 +7,9 @@
#include <KLocalizedString>
#include "neochatuser.h"
#include <Quotient/user.h>
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, NeoChatUser *localUser)
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, Quotient::User *localUser)
: QAbstractListModel(parent)
, m_localUser(localUser)
{
@@ -31,7 +31,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
if (role == TextRole) {
if (reaction.authors.count() > 1) {
return reaction.reaction + QStringLiteral(" %1").arg(reaction.authors.count());
return QStringLiteral("%1 %2").arg(reaction.reaction, QString::number(reaction.authors.count()));
} else {
return reaction.reaction;
}
@@ -52,7 +52,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
text += i18nc("Separate the usernames of users", " and ");
}
}
text += reaction.authors.at(i).toMap()["displayName"].toString();
text += reaction.authors.at(i).toMap()[QStringLiteral("displayName")].toString();
}
if (reaction.authors.count() > 3) {
@@ -74,7 +74,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
if (role == HasLocalUser) {
for (auto author : reaction.authors) {
if (author.toMap()["id"] == m_localUser->id()) {
if (author.toMap()[QStringLiteral("id")] == m_localUser->id()) {
return true;
}
}

View File

@@ -5,7 +5,10 @@
#include <QAbstractListModel>
class NeoChatUser;
namespace Quotient
{
class User;
}
/**
* @class ReactionModel
@@ -36,7 +39,7 @@ public:
HasLocalUser, /**< Whether the local user is in the list of authors. */
};
explicit ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, NeoChatUser *localUser = nullptr);
explicit ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, Quotient::User *localUser = nullptr);
/**
* @brief Get the given role value at the given index.
@@ -67,6 +70,6 @@ public:
private:
QList<Reaction> m_reactions;
NeoChatUser *m_localUser;
Quotient::User *m_localUser;
};
Q_DECLARE_METATYPE(ReactionModel *)

View File

@@ -40,19 +40,19 @@ RoomListModel::RoomListModel(QObject *parent)
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
const auto launcherUrl = "application://org.kde.neochat.desktop"_ls;
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
dbusUnityProperties["count"_ls] = counterSlice;
dbusUnityProperties["count-visible"_ls] = true;
} else {
dbusUnityProperties["count-visible"] = false;
dbusUnityProperties["count-visible"_ls] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat", "com.canonical.Unity.LauncherEntry", "Update");
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_ls, "com.canonical.Unity.LauncherEntry"_ls, "Update"_ls);
signal.setArguments({launcherUrl, dbusUnityProperties});
@@ -297,9 +297,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return NeoChatRoomType::Normal;
}
QJsonObject contentJson = creationEvent->contentJson();
QJsonObject::const_iterator typeIter = contentJson.find("type");
QJsonObject::const_iterator typeIter = contentJson.find("type"_ls);
if (typeIter != contentJson.end()) {
if (typeIter.value().toString() == "m.space") {
if (typeIter.value().toString() == "m.space"_ls) {
return NeoChatRoomType::Space;
}
}
@@ -338,7 +338,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == AvatarImageRole) {
return room->avatar(128);
}
if (role == IdRole) {
if (role == RoomIdRole) {
return room->id();
}
if (role == IsSpaceRole) {
@@ -379,7 +379,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[CategoryVisibleRole] = "categoryVisible";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[IdRole] = "id";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
return roles;
}
@@ -400,7 +400,7 @@ QString RoomListModel::categoryName(int category)
case NeoChatRoomType::Space:
return i18n("Spaces");
default:
return "Deadbeef";
return {};
}
}

View File

@@ -72,7 +72,7 @@ public:
CategoryVisibleRole, /**< If the room's category is visible. */
SubtitleTextRole, /**< The text to show as the room subtitle. */
AvatarImageRole, /**< The room avatar as an image. */
IdRole, /**< The room matrix ID. */
RoomIdRole, /**< The room matrix ID. */
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
};
@@ -163,7 +163,7 @@ private:
QMap<int, bool> m_categoryVisibility;
int m_notificationCount = 0;
QString m_activeSpaceId = "";
QString m_activeSpaceId;
void connectRoomSignals(NeoChatRoom *room);

View File

@@ -5,13 +5,14 @@
#include "messageeventmodel.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include <QGuiApplication>
#include <Quotient/connection.h>
#include <Quotient/events/stickerevent.h>
#include <KLocalizedString>
#include <Quotient/connection.h>
using namespace Quotient;
// TODO search only in the current room
@@ -19,6 +20,9 @@ using namespace Quotient;
SearchModel::SearchModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
});
}
QString SearchModel::searchText() const
@@ -53,7 +57,7 @@ void SearchModel::search()
.searchTerm = m_searchText,
.keys = {},
.filter = filter,
.orderBy = "recent",
.orderBy = "recent"_ls,
.eventContext = SearchJob::IncludeEventContext{3, 3, true},
.includeState = false,
.groupings = none,
@@ -109,7 +113,7 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return false;
case ReplyAuthorRole:
if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
return m_room->getUser(static_cast<NeoChatUser *>(m_room->user(replyPtr->senderId())));
return m_room->getUser(m_room->user(replyPtr->senderId()));
} else {
return m_room->getUser(nullptr);
}
@@ -153,8 +157,8 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
}
return QVariantMap{
{"display", m_room->eventToString(*replyPtr, Qt::RichText)},
{"type", type},
{"display"_ls, m_room->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
}
case IsPendingRole:
@@ -162,13 +166,13 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
case ShowLinkPreviewRole:
return false;
case IsReplyRole:
return !event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
return !event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
case HighlightRole:
return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
case EventIdRole:
return event.id();
case ReplyIdRole:
return event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
return event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
return MessageEventModel::DelegateType::Message;
}
@@ -257,7 +261,7 @@ QString renderDate(const QDateTime &timestamp)
return i18n("The day before yesterday");
}
if (date > QDate::currentDate().addDays(-7)) {
return date.toString("dddd");
return date.toString("dddd"_ls);
}
return QLocale::system().toString(date, QLocale::ShortFormat);

View File

@@ -15,8 +15,8 @@
ServerListModel::ServerListModel(QObject *parent)
: QAbstractListModel(parent)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, "Servers");
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
QString domain = Controller::instance().activeConnection()->domain();
@@ -91,8 +91,8 @@ int ServerListModel::rowCount(const QModelIndex &parent) const
void ServerListModel::checkServer(const QString &url)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, "Servers");
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
if (!serverGroup.hasKey(url)) {
if (Quotient::isJobPending(m_checkServerJob)) {
@@ -108,8 +108,8 @@ void ServerListModel::checkServer(const QString &url)
void ServerListModel::addServer(const QString &url)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, "Servers");
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
if (!serverGroup.hasKey(url)) {
Server newServer = Server{
@@ -129,8 +129,8 @@ void ServerListModel::addServer(const QString &url)
void ServerListModel::removeServerAtIndex(int row)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, "Servers");
KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup serverGroup(&dataResource, QStringLiteral("Servers"));
serverGroup.deleteEntry(data(index(row), UrlRole).toString());
beginRemoveRows(QModelIndex(), row, row);

View File

@@ -80,14 +80,15 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != QStringLiteral("upgraded")
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) {
return acceptRoom;
} else {
const auto &rooms = SpaceHierarchyCache::instance().getRoomListForSpace(m_activeSpaceId, false);
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IdRole).toString()) != rooms.end()
return std::find(rooms.begin(), rooms.end(), sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::RoomIdRole).toString())
!= rooms.end()
&& acceptRoom;
}
}

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