Compare commits

..

156 Commits

Author SHA1 Message Date
Joshua Goins
2bb55eece7 Don't flag invite notifications as persistent
These really don't need to be persistent, as they even stick around
when NeoChat is closed. This also spams the user's notification system
usually, if they get lots of invitations at once which don't go away
automatically.

(cherry picked from commit 51197d7c1a)
2024-07-25 16:38:13 -04:00
Joshua Goins
2877e40647 Hint that the user can auto-reject invitations on the invite page
This should help make the new setting more discoverable.

(cherry picked from commit 2a2a2e0c05)
2024-07-25 16:38:13 -04:00
Joshua Goins
768ec242fa Allow blocking invites from people you don't share a room with
Matrix currently has a significant moderation loophole, thanks to
invites. Right now, anyone can invite anyone to a room - and clients
like NeoChat will gladly display these rooms to them and even give you
a notification.

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

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

Since this depends on MSC 2666 - a currently unstable feature - the
server may not support it and NeoChat will disable the setting in this
case.

(cherry picked from commit 07fee30cc0)
2024-07-25 16:38:13 -04:00
Joshua Goins
194751627f Don't display notifications for invite rooms
Sometimes a ghost notification will appear, this is sometimes a stray
m.room.invite notification we didn't handle or some other event. Let's
outright reject all notifications from Invite-type rooms to prevent this
from happening altogether.

(cherry picked from commit 83c6ce0ace)
2024-07-25 16:38:13 -04:00
l10n daemon script
bc701c51d9 GIT_SILENT Sync po/docbooks with svn 2024-07-25 03:13:15 +00:00
l10n daemon script
35939b4af4 GIT_SILENT Sync po/docbooks with svn 2024-07-23 03:14:18 +00:00
l10n daemon script
a178b8b6ca SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-07-23 03:04:52 +00:00
Albert Astals Cid
a6994318de GIT_SILENT Upgrade release service version to 24.07.80. 2024-07-21 12:25:07 +02:00
l10n daemon script
5022ed1a63 GIT_SILENT Sync po/docbooks with svn 2024-07-21 01:27:43 +00:00
James Graham
799ffa18c4 Fix the javascript when using .? 2024-07-20 22:35:41 +00:00
Tobias Fella
6323e27040 Remove customemojimodel_p.h
It's been unused for a long time
2024-07-20 22:49:22 +02:00
Tobias Fella
03acb26109 Fix some clazy warnings 2024-07-20 22:38:45 +02:00
Tobias Fella
34fc1f6a6b Fix text alignment in ReadMarkerDelegate 2024-07-20 21:54:55 +02:00
James Graham
042032ec46 Update user sort
Update the user model so it also sorts by power level and update how we initialize the model to improve performance.

The following is also changed:
- Store a single `UserListModel` in `RoomManager` and use it for everything, this means we don't create extra models (incluiding the long initialisation for each in big rooms)
- By using the single model once it has loaded the users of the new room opening and closing the draw now happens instantly (previously the model would have to be loaded every time the drawer was opened).
- To stop the initial loading and room change of Neochat slowing down (as the `UserListModel` would be loaded before the `TimelineView` is shown) the initialisation of the model is delayed until the `TimelineView` has loaded. This prioritises showing some messages in the timeline over populating the model so in large rooms the user list will initially be blank, but this keeps the initial load snappier.
2024-07-20 18:12:30 +00:00
James Graham
73de99f661 Create a list model for readmarkers
Create a list model for read markers. The primary reason is to stop `RoomMembers` being accessed after their state event is deleted. With this the read marker doesn't pass and `RoomMember` objects to qml.
2024-07-20 18:05:15 +00:00
l10n daemon script
cc068f9ebb GIT_SILENT Sync po/docbooks with svn 2024-07-19 01:26:38 +00:00
l10n daemon script
2bfc2b1944 GIT_SILENT Sync po/docbooks with svn 2024-07-18 01:23:41 +00:00
l10n daemon script
029936112f GIT_SILENT made messages (after extraction) 2024-07-18 00:39:13 +00:00
Andreas Gattringer
d5e400e8a4 stop video on destruction to avoid segfault 2024-07-17 11:08:53 +00:00
Andreas Gattringer
9dbb9c3f3b fix filedownload on maximized view and context menu
- move the logic for remembering local filenames from
  messagecontentmodel to neochatroom
- use it also when maximized and in context menu opening, to stop
  re-downloading
- make onFileTransferCompleted signal connection one-shot (stops memory
  leak)
2024-07-17 08:29:29 +00:00
l10n daemon script
a101e6b0a7 GIT_SILENT Sync po/docbooks with svn 2024-07-17 01:24:34 +00:00
Andreas Gattringer
c525ea55ce fix typo in PollComponent that causes polls to not be checked correctly 2024-07-16 21:41:25 +02:00
Andreas Gattringer
8ca45f298f show thumbail when video is stopped 2024-07-16 12:27:35 +02:00
Andreas Gattringer
cb4c6cb677 Audio filename, caption and seeking
- display caption independent of filename (consistancy to File)
- fix audio seeking not working
- change two comments and a variable name
2024-07-16 12:27:35 +02:00
Andreas Gattringer
d574a97a35 take the correct filename for synthax highlighting in code block 2024-07-16 12:27:35 +02:00
Andreas Gattringer
20f9a86ad9 stop assigning undefined to bool in TimelineView and MessageDelegate 2024-07-16 12:27:35 +02:00
Andreas Gattringer
a4a411cf1f correctly display filename in MimeComponents
(previously it would include the caption text and save with it as
filename)
Don't append filename to the caption.

Relevant spec: https://spec.matrix.org/latest/client-server-api/#mfile
2024-07-16 12:27:35 +02:00
Andreas Gattringer
029bda5734 fix code preview components
- fix them vanishing after "opening" a file (they get downloaded to
  /tmp/ without extension, which caused them to lose preview)
- make all text/plain mimetype files preview
- don't show them in Replies (in consistency with media components)
2024-07-16 12:27:35 +02:00
Andreas Gattringer
0fd578e6aa Refactor MimeComponent
- Fix size and duration not showing in MimeComponents in replies
- Display Itinerary in replies as MimeComponent (in consistency with media
  components)
2024-07-16 12:27:35 +02:00
l10n daemon script
95e1bee5e6 GIT_SILENT Sync po/docbooks with svn 2024-07-16 01:23:40 +00:00
Tobias Fella
c8dc10f311 Don't escape display name for leave events in subtitle 2024-07-15 22:04:32 +02:00
l10n daemon script
a00f4e393b GIT_SILENT Sync po/docbooks with svn 2024-07-15 01:26:14 +00:00
l10n daemon script
5a7dea8857 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-07-15 01:18:59 +00:00
Andreas Gattringer
27e8970fff fix segfault at loadError from message with attached file 2024-07-14 19:46:04 +00:00
Andreas Gattringer
7848274ba1 fix resetting of message content components
and don't reset whole Moessage content when author changes
2024-07-14 18:25:02 +02:00
l10n daemon script
145bf0298b GIT_SILENT Sync po/docbooks with svn 2024-07-14 01:23:33 +00:00
James Graham
0392a33b54 Content Model Author
Make sure that the Author in the content model is only ever updated int he following circumstacnes:
- The member updates their displayname or avatar
- A previously hidden author line is shown

This is done by only doing a full reset on initialisation or when a previously hidden author is shown. The rest of the time we only reset the other content in the message, i.e. everything below the author line.
2024-07-13 16:01:42 +00:00
Andreas Gattringer
b211f46e3e Do heavy things less often on room change event 2024-07-13 15:22:52 +00:00
James Graham
709711c3ca Fix the InlineMessages for the room upgrades in RoomSettings
Fix the InlineMessages for the room upgrades in RoomSettings, they should show even if we aren't currently in the room. Resolve resource can join the predecessor/sucessor if required.

Closes network/neochat#618
2024-07-13 15:14:50 +00:00
l10n daemon script
841607406f GIT_SILENT Sync po/docbooks with svn 2024-07-13 01:25:56 +00:00
Andreas Gattringer
70b726b04d reset currentRoom before opening last room of connection 2024-07-12 07:41:25 +00:00
l10n daemon script
2799248106 GIT_SILENT Sync po/docbooks with svn 2024-07-12 01:24:11 +00:00
Carl Schwan
2321299084 Pass ApplicationWindow to AccountMenu 2024-07-11 20:55:04 +00:00
Andreas Gattringer
aa00773a3b update only position of HoverActions when contentY of TimelineView
changes
2024-07-11 14:06:58 +02:00
l10n daemon script
733de1d0e1 GIT_SILENT Sync po/docbooks with svn 2024-07-11 01:24:16 +00:00
Tobias Fella
c16e4ad412 Fix opening pages from account menu 2024-07-10 21:39:15 +02:00
l10n daemon script
d1cf7a07f1 GIT_SILENT Sync po/docbooks with svn 2024-07-10 01:25:42 +00:00
James Graham
8751f6fea7 Never refresh the author role except when a member updated signal is generated. This should remove a potential source of crashes where a RoomMember object tries to access an already deleted room member state event 2024-07-09 20:21:13 +01:00
Andreas Gattringer
ea1b577ec7 fix room settings not opening from RoomDrawerPage, by launching it according to RoomDrawer 2024-07-09 10:49:18 +02:00
l10n daemon script
a41afa70eb GIT_SILENT Sync po/docbooks with svn 2024-07-08 01:23:33 +00:00
Tobias Fella
e11c97cdc0 Move NeoChatRoom::showMessage to Controller
It doesn't fit into NeoChatRoom conceptually
2024-07-07 13:44:55 +02:00
l10n daemon script
d76e44512e GIT_SILENT Sync po/docbooks with svn 2024-07-07 01:24:08 +00:00
l10n daemon script
fd1931377d SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-07-07 01:17:56 +00:00
James Graham
68ce6c4ed7 It seems like it's possible for contentModel.Author to be accessed before the contentModel is set so make sure this fails nicely 2024-07-06 20:05:16 +00:00
Carl Schwan
a2ee330307 Use new SearchDialog from Kirigami 2024-07-06 18:16:43 +00:00
Tobias Fella
c3a17f951e Only enable taphandler for mouse clicks
BUG: 486545
2024-07-06 20:11:21 +02:00
Nicolas Fella
443c0f34d7 Rework Add Server dialog
Use plain items instead of FormCard delegate items, this isn't a FormCard

Set placeholder text for textfield

Remove redundant subtitle

Use inlinemessage for errors

Before:

![image.png](/uploads/375d3c668271f6bce699cc9462f6deb9/image.png){width=405 height=247}

After:

![image.png](/uploads/b8d79a4fdb0914d1f82ecec0760ad80f/image.png){width=404 height=234}
2024-07-06 18:10:34 +00:00
Tobias Fella
8725600368 Port away from deprecated QDateTime constructor 2024-07-06 20:00:04 +02:00
Tobias Fella
3b83b7f190 Set Qt policy 4 2024-07-06 20:00:04 +02:00
Tobias Fella
e0c3a1c143 Port to ecm_add_qml_module 2024-07-06 19:50:20 +02:00
Tobias Fella
bec1ad7bee Fix crashes when logging out of connection 2024-07-06 19:44:00 +02:00
Tobias Fella
9e6c00f78c Fix logout from account menu 2024-07-06 19:28:54 +02:00
Tobias Fella
abbbd9d705 Use RoomMember object for code component 2024-07-06 16:27:08 +02:00
Tobias Fella
4a5c012f05 Fix avatar in chatbar 2024-07-06 16:22:03 +02:00
Tobias Fella
319862b3d4 Fix avatars in full window image view 2024-07-06 16:20:16 +02:00
Tobias Fella
1fcf58024d Fix avatars in CodeMaximizeComponent 2024-07-06 16:18:49 +02:00
Tobias Fella
9db162d0fc Fix avatar in state delegates 2024-07-06 16:16:44 +02:00
Tobias Fella
13b15390c3 Fix crashes due to event being deleted 2024-07-06 15:54:29 +02:00
l10n daemon script
799b62e9d2 GIT_SILENT Sync po/docbooks with svn 2024-07-06 01:25:14 +00:00
Tobias Fella
d91ed535ad Don't access event after it was deleted
- NeoChat stores pointer to event
- Event is replaced
- libQuotient deletes the event and notifies us that the event changes
- We're accessing the event to check its id
- Boom

Make this not boom by accessing the ID that we're additionally storing anyway.
2024-07-05 16:54:38 +02:00
l10n daemon script
c48b9874bf GIT_SILENT Sync po/docbooks with svn 2024-07-04 01:22:48 +00:00
l10n daemon script
6a788f6c32 GIT_SILENT Sync po/docbooks with svn 2024-07-03 01:26:09 +00:00
Tobias Fella
53519a604e Don't store member objects in userlistmodel 2024-07-02 18:17:27 +02:00
l10n daemon script
2818b87f02 GIT_SILENT Sync po/docbooks with svn 2024-07-02 01:24:50 +00:00
l10n daemon script
50e10133b9 GIT_SILENT Sync po/docbooks with svn 2024-07-01 01:23:04 +00:00
James Graham
068161719e Update author documentation now we've moved to RoomMember 2024-06-30 17:59:40 +01:00
James Graham
c8d5d095e0 REmove unneeded includes 2024-06-30 17:59:40 +01:00
James Graham
52d07320ef Make the author line in the bubble and reply be part of the content model 2024-06-30 17:59:40 +01:00
Tobias Fella
158b9ea2ca Fix opening QR code 2024-06-30 18:06:57 +02:00
James Graham
6a100bfbab When switching rooms first set the room to nullptr to clear any objects in MessageEventModel and UserListModel 2024-06-30 14:24:21 +00:00
Tobias Fella
b3bd6ee176 Fix showing avatar image in HiddenDelegate 2024-06-30 15:53:02 +02:00
Tobias Fella
04696b27eb Use type for member property 2024-06-30 15:53:02 +02:00
Tobias Fella
332937dbc1 Register RoomMember as foreign type 2024-06-30 15:52:45 +02:00
l10n daemon script
92d932ce5c GIT_SILENT Sync po/docbooks with svn 2024-06-30 01:25:04 +00:00
Carl Schwan
af3c4f536a Fix dev tools 2024-06-29 18:03:43 +02:00
Carl Schwan
b98ca5af52 Use SpellcheckingConfigurationModule 2024-06-29 18:03:43 +02:00
Carl Schwan
a15a11f44b Rework config dialog
Use new KirigamiAddons ConfigurationsView as a replacement for
CategorizedSettings. Fix some issues on desktop when running Qt 6.8
and some less recent issues on mobile.

Visually this looks the same.
2024-06-29 18:03:40 +02:00
Tobias Fella
1460132772 Fix typing indicator 2024-06-29 17:30:29 +02:00
Tobias Fella
76d68bb3e4 Fix tests 2024-06-29 17:30:29 +02:00
Tobias Fella
08df0ebbea user details dialog fixes 2024-06-29 17:30:29 +02:00
Tobias Fella
e918db2cc1 Port away from function that we can't use yet 2024-06-29 17:30:29 +02:00
James Graham
2e0dc8db94 Make sure that the member objects get updated for the MessageEventModel and MessageContentModel when the user updates their avatar or name 2024-06-29 17:30:29 +02:00
James Graham
b78a9f2a9c Find users by Id in UserListModel as there may be users with the same display name which will end up with the wrong member replaced 2024-06-29 17:30:28 +02:00
James Graham
832e6b9de0 Make sure that the member object gets switched when a member's avatar or name is updated because the old state event will now be deleted and we need to ref the new one. 2024-06-29 17:30:28 +02:00
James Graham
feec7ca408 Don't store RoomMembers in ReactionModel
Don't store RoomMember objects in the reaction model just grab them everytime incase the state event is replaced.
2024-06-29 17:30:28 +02:00
James Graham
408f0a12e2 Use new libquotient functionality to encrypt direct chats by default
Needs https://github.com/quotient-im/libQuotient/pull/730
2024-06-29 17:30:28 +02:00
James Graham
35ab4e6e09 Use updated membersTyping functions from libquotient 2024-06-29 17:30:28 +02:00
James Graham
430bafafe7 Make use of new RoomMember item from libquotient
Depends on https://github.com/quotient-im/libQuotient/pull/695

Currently basic just to show a working implementation using RoomMember. Currently only the room event and search models are moved over. Will change everything else over once the dependent pr is complete.
2024-06-29 17:30:28 +02:00
James Graham
58d727350d Remove uses of Quotient:Omittable
Note this technically won't build for now because of the lack of RoomMember support but I'll push that at the quotient-next branch next. 

This is needed as well to get a branch that builds on dev.
2024-06-29 17:30:04 +02:00
l10n daemon script
c6a4057659 GIT_SILENT Sync po/docbooks with svn 2024-06-29 01:24:06 +00:00
Heiko Becker
60ac806690 GIT_SILENT Update Appstream for new release
(cherry picked from commit c00debf1c3)
2024-06-28 23:18:10 +02:00
James Graham
e9263fc596 Use the new libquotient version of the serveracl event
Use with https://github.com/quotient-im/libQuotient/pull/729
2024-06-28 15:02:56 +02:00
Tobias Fella
3dd28a0382 Unconditionally use SSSS 2024-06-28 14:33:23 +02:00
l10n daemon script
066cb8184e GIT_SILENT Sync po/docbooks with svn 2024-06-28 01:33:31 +00:00
l10n daemon script
373e22b999 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-06-28 01:20:45 +00:00
Tobias Fella
fd8725f649 Add basic cross-signing support
(cherry picked from commit e1795076c8c41a34b91c31df35d1e344d0b14887)
2024-06-27 18:51:38 +02:00
Tobias Fella
3d433762b1 Fix ifs for ssss 2024-06-27 18:23:29 +02:00
Tobias Fella
ea4cb5bf62 Disable FreeBSD CI 2024-06-27 18:08:47 +02:00
Tobias Fella
a6ca3b8203 Require libQuotient 0.8.2 2024-06-27 18:08:28 +02:00
l10n daemon script
bc4ceb6d52 GIT_SILENT Sync po/docbooks with svn 2024-06-27 01:26:42 +00:00
l10n daemon script
24480229cd GIT_SILENT Sync po/docbooks with svn 2024-06-26 01:25:49 +00:00
l10n daemon script
8b10573197 GIT_SILENT Sync po/docbooks with svn 2024-06-25 01:23:09 +00:00
l10n daemon script
cbc81e8285 GIT_SILENT Sync po/docbooks with svn 2024-06-24 01:22:01 +00:00
Tobias Fella
0b9a978061 Update global menu 2024-06-23 21:09:33 +02:00
l10n daemon script
e974e5d13b GIT_SILENT Sync po/docbooks with svn 2024-06-23 01:25:25 +00:00
l10n daemon script
5a42e86bf6 GIT_SILENT Sync po/docbooks with svn 2024-06-22 01:29:39 +00:00
l10n daemon script
889946e186 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-06-22 01:19:19 +00:00
l10n daemon script
5c47e8044e GIT_SILENT made messages (after extraction) 2024-06-22 00:39:08 +00:00
l10n daemon script
1a974ac305 GIT_SILENT Sync po/docbooks with svn 2024-06-21 01:23:47 +00:00
Tobias Fella
db1bf61805 GlobalMenu: remove shortcut for QuickSwitcher
The shortcut needs to work when there is no Global Menu, so it's also in QuickSwitcher.qml.
It can't be in both places, since that breaks it. So we remove it here.

BUG: 488212
2024-06-20 18:14:36 +02:00
Tobias Fella
5456b4a7ff Fix global menu 2024-06-20 18:03:42 +02:00
l10n daemon script
a08ffaae77 GIT_SILENT Sync po/docbooks with svn 2024-06-20 01:27:52 +00:00
l10n daemon script
18445f55f0 GIT_SILENT Sync po/docbooks with svn 2024-06-19 01:24:36 +00:00
Albert Astals Cid
2daf3b5c4b CI: Disable requiring Windows tests passing
Has been broken for 4 consecutive weeks
2024-06-18 22:42:00 +02:00
l10n daemon script
923b844212 GIT_SILENT Sync po/docbooks with svn 2024-06-18 01:29:17 +00:00
James Lyne
0aec9f8472 Fix search results on room search page
Add missing IsEditable role to search model
2024-06-17 20:15:36 +00:00
l10n daemon script
23b0c8a143 GIT_SILENT Sync po/docbooks with svn 2024-06-17 01:25:34 +00:00
l10n daemon script
3c2c2e2bd8 GIT_SILENT Sync po/docbooks with svn 2024-06-16 01:24:54 +00:00
l10n daemon script
c1465a7368 GIT_SILENT Sync po/docbooks with svn 2024-06-14 01:22:57 +00:00
l10n daemon script
be1dadab74 GIT_SILENT Sync po/docbooks with svn 2024-06-13 01:23:33 +00:00
Derry Tutt
1cba39eae9 Update strings to be more clear for the average user 2024-06-12 19:37:39 +00:00
Tobias Fella
a0c8bdf021 Make notifications more useful
- Refactor and cleanup code
- Don't paginate through notifications - it spams the server with requests and realistically only contains anything relevant on startup after a long time, in which case you're going to lose some notification anyway
- Only show newest notification for the respective room, closing the old notification if one exists
- Only show text of this notification

BUG: 475228
2024-06-12 21:28:09 +02:00
l10n daemon script
6fdb22a5b5 GIT_SILENT Sync po/docbooks with svn 2024-06-12 01:26:41 +00:00
l10n daemon script
19c370a273 GIT_SILENT made messages (after extraction) 2024-06-12 00:38:48 +00:00
Tobias Fella
861336ea97 Remove unnecessary check 2024-06-11 21:48:56 +02:00
l10n daemon script
24219bcb03 GIT_SILENT Sync po/docbooks with svn 2024-06-11 01:29:06 +00:00
Tobias Fella
77ed762e2c Use plaintext for room aliases 2024-06-10 22:20:37 +02:00
l10n daemon script
d45b6cb03d GIT_SILENT Sync po/docbooks with svn 2024-06-10 01:35:39 +00:00
Heiko Becker
9a921b2e0d GIT_SILENT Update Appstream for new release
(cherry picked from commit c5c47d7b67)
2024-06-10 00:48:21 +02:00
l10n daemon script
c17e213e11 GIT_SILENT Sync po/docbooks with svn 2024-06-09 01:23:44 +00:00
Joshua Goins
6275d7afaa Switch from QQC2.ApplicationWindow.overlay to QQC2.Overlay.overlay
Closes #648
2024-06-08 11:47:30 -04:00
Joshua Goins
364eda6400 Fix keyboard navigation on search pages
Some of our search pages (such as the room and user search) has a list
header item. Due to how this works, it's not actually a part of the
list view keyboard navigation and a whole separate item. So in the tab
order, it comes *after* the list view which makes no sense. And it's
part of the list view, so users must expect it to be selectable with the
up and down arrows like other items.

This simple change makes it so it behaves as expected. The first actual
list item is selected by default, but it's possible to navigate to the
list header item via the up arrow key and then return to the list view
using the down arrow. The list header item is also removed from the tab
order and the whole page is much nicer to use now.
2024-06-08 15:44:38 +00:00
Joshua Goins
ccf34cfe20 The "Search Room" action should be called "Search Rooms" 2024-06-08 15:42:10 +00:00
Joshua Goins
7daae6a2d9 Fix the tooltips for the two drawer buttons at the top
One of them didn't even have a tooltip, which is a simple oversight
since it already has accessible text. The tooltips now use the attached
property instead of creating a new QQC2.ToolTip too.
2024-06-08 15:42:10 +00:00
Joshua Goins
277a4ad124 Fix keyboard navigation in space drawer
Some of the items were able to activated via the keyboard, but many were
not like the notifications and "create a space" buttons. This is because
the signals were hooked up to onClicked but the accessible and keyboard
nav were hooked up to onSelected. All of the buttons trigger their
actions with onSelected now.
2024-06-08 15:32:37 +00:00
Joshua Goins
b11d46e34a Add keyboard navigation for server selection in room search dialog
This was previously not keyboard navigable at all, making it
impossible to switch servers in this dialog solely with a keyboard. This
patch makes it possible to do some basic selection but not deletion yet,
but it's a good start.
2024-06-08 15:32:26 +00:00
Joshua Goins
e8ad0a055d Remove room member highlight on click
Previously it was possible to keep clicking and highlighting each member
which doesn't make any sense. We could make this exclusive by having it
highlight only when index == currentIndex, but honestly it doesn't need
to be highlighted at all. Clicking on a room member opens their user
card, there's no persistent state the user needs to keep track of here.
2024-06-08 11:25:49 -04:00
Joshua Goins
8a8c745d77 Use Qt.alpha in ThemeRadioButton
This was newly added in Qt6 and simplifies a Qt.rgba call we used here.
2024-06-08 14:35:09 +00:00
Joshua Goins
a523fe7674 Add focus border for the theme radio button, used on the Appearance page
Otherwise it's impossible to tell which option you're on, if you're
solely using a keyboard.
2024-06-08 14:35:09 +00:00
Joshua Goins
dc9a150929 Fix QR code not showing when tapping the button under account settings 2024-06-08 14:34:58 +00:00
Joshua Goins
be66ffef0f Fix map copyright link activation
The argument was missing, so it wasn't possible to actually click and
visit the copyright notices linked on maps.
2024-06-08 14:34:48 +00:00
Joshua Goins
f278cc0c86 Don't show the map if there's no locations available
It's hard to the read the text when there's a beige map behind it, and
unnecessary anyway.
2024-06-08 14:34:48 +00:00
Nicolas Fella
7f72808a9a Fixup AttachDialog
Use standard spacing values

Use implicit button size
2024-06-08 14:34:40 +00:00
Joshua Goins
1d5297c0f0 Rename the header for room actions "Actions" instead of "Options"
These aren't really configurable options in the usual sense, but rather
actions you can take in the room.
2024-06-08 14:34:21 +00:00
Joshua Goins
e40528ba45 Use a more natural sounding action name for favoriting the room
"Make room favorite" doesn't sound very natural in English, but
"Favorite this room" is and fits in with the rest of the actions here.
2024-06-08 14:34:21 +00:00
Tobias Fella
29972b5867 Port away from commitSingleShot 2024-06-08 15:43:16 +02:00
181 changed files with 27807 additions and 28118 deletions

View File

@@ -5,11 +5,11 @@ include:
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
# - /gitlab-templates/android-qt6.yml
# - /gitlab-templates/linux-qt6.yml
# - /gitlab-templates/windows-qt6.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
# - /gitlab-templates/craft-android-qt6-apks.yml
# - /gitlab-templates/craft-appimage-qt6.yml
# - /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml

View File

@@ -40,4 +40,4 @@ Dependencies:
Options:
per-test-timeout: 90
require-passing-tests-on: [ '@all' ]
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD' ]

View File

@@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "07")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MICRO "80")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -60,6 +60,9 @@ set_package_properties(Qt6 PROPERTIES
)
qt_policy(SET QTP0001 NEW)
if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
@@ -102,7 +105,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.7)
find_package(QuotientQt6 0.8.2)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"

View File

@@ -38,8 +38,8 @@ Due to the nature of the Matrix specification development NeoChat also supports
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
Nightly builds for linux and windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
Nightly builds for android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
Nightly builds for Linux and Windows can be downloaded from [cdn.kde.org](https://cdn.kde.org/ci-builds/network/neochat/).
Nightly builds for Android are available from [KDE's nightly F-Droid repository](https://community.kde.org/Android/F-Droid).
Nightly Flatpaks are available from [KDE's nightly Flatpak repository](https://userbase.kde.org/Tutorials/Flatpak).
## Building NeoChat

View File

@@ -25,7 +25,7 @@
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
"@bob:kde.org"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
@@ -47,7 +47,7 @@
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"@bob:kde.org": {
"ts": 1436451550453
}
}
@@ -67,6 +67,18 @@
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim2:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
@@ -136,6 +148,22 @@
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:kde.org",
"state_key": "@bob:kde.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
@@ -156,7 +184,7 @@
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
"@bob:kde.org"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2

View File

@@ -130,7 +130,23 @@
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"state_key": "@alice:matrix.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:example.org",
"state_key": "@bob:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234

View File

@@ -75,8 +75,6 @@ private Q_SLOTS:
void nullThread();
void location();
void nullLocation();
void readMarkers();
void nullReadMarkers();
};
void EventHandlerTest::initTestCase()
@@ -521,59 +519,5 @@ void EventHandlerTest::nullLocation()
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
}
void EventHandlerTest::readMarkers()
{
EventHandler eventHandler(room, room->messageEvents().at(0).get());
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].id(), QStringLiteral("@alice:example.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: Alice Margatroid"));
EventHandler eventHandler2(room, room->messageEvents().at(2).get());
QCOMPARE(eventHandler2.hasReadMarkers(), true);
readMarkers = eventHandler2.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler2.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler2.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
void EventHandlerTest::nullReadMarkers()
{
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QList<Quotient::RoomMember>());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
EventHandler noEventHandler(room, nullptr);
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QList<Quotient::RoomMember>());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -52,7 +52,7 @@ void ReactionModelTest::basicReaction()
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>"));
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍"));
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalMember), false);
}
@@ -63,7 +63,7 @@ void ReactionModelTest::newReaction()
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QStringLiteral("Alice Margatroid reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QSignalSpy spy(model, SIGNAL(modelReset()));
@@ -72,7 +72,7 @@ void ReactionModelTest::newReaction()
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆"));
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org and @bob:example.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QStringLiteral("Alice Margatroid and Bob reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
delete model;
}

View File

@@ -95,8 +95,10 @@
<p xml:lang="ka">NeoChat ჩატის აპია, რომელიც საშუალება გაძლევთ, Matrix-ის ქსელის საშუალებები ბოლომდე გამოიყენოთ. ის გაძლევთ უსაფრთხო გზას, გააგზავნოთ ტექსტური შეტყობინებები, ვიდეოებ და აუდიოფაილები თქვენს ოჯახთან, კოლეგებთან და მეგობრებთან.</p>
<p xml:lang="lv">„NeoChat“ ir tērzēšanas programma, kas ļauj pilnvērtīgi izmantot „Matrix“ tīklu. Tā sniedz drošu veidu teksta ziņu, video un audio sūtīšanai ģimenes locekļiem, kolēģiem un draugiem.</p>
<p xml:lang="nl">NeoChat is een chat-toepassing die u het volledige voordeel van het Matrix-netwerk laat genieten. Het levert u op een veilige manier tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden te verzenden.</p>
<p xml:lang="nn">NeoChat er ein prateapp som lèt deg bruka all funksjonalitet i Matrix-nettverket. Du kan utveksla tekst, lyd og videoar med vennar, familie og kollegaar på ein trygg måte.</p>
<p xml:lang="pl">NoeChat to aplikacja do rozmów, która umożliwia wykorzystanie wszystkich możliwości Matriksa. Umożliwia wysyłanie wiadomości tekstowych, filmów i dźwięków w bezpieczny sposób do twojej rodziny, kolegów i przyjaciół.</p>
<p xml:lang="sl">NeoChat je aplikacija za klepet, ki vam omogoča, da v celoti izkoristite omrežje Matrix. Zagotavlja vam varen način za pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek vaši družini, sodelavcem in prijateljem.</p>
<p xml:lang="sv">NeoChat är ett chattprogram som låter dig dra full nytta av Matrix-nätverket. Det ger dig ett säkert sätt att skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner.</p>
<p xml:lang="tr">NeoChat, Matrix ağının tüm özelliklerini kullanan bir sohbet uygulamasıdır. Ailenize, arkadaşlarınıza ve iş arkadaşlarınıza metin iletileri, ses ve video dosyaları göndermenin kolay bir yolunu sunar.</p>
<p xml:lang="uk">NeoChat є програмою для спілкування, за допомогою якої ви можете скористатися усіма перевагами мережі Matrix. За її допомогою ви можете безпечно надсилати текстові повідомлення, відео та звукові файли вашим родичам, колегам та друзям.</p>
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
@@ -212,7 +214,7 @@
<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="tr">Çıkartma Paketleri — MSC2545</li>
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
<li xml:lang="x-test">xxSticker Packs - MSC2545xx</li>
<li xml:lang="zh-TW">貼圖包 - MSC2545</li>
@@ -327,8 +329,10 @@
<caption xml:lang="ka">აღმოაჩინეთ ახალი საზოგადოებები Matrix Spaces-თან ერთად</caption>
<caption xml:lang="lv">Atklājiet jaunas kopienas ar „Matrix“ telpām</caption>
<caption xml:lang="nl">Ontdek nieuwe gemeenschappen met Matrix-ruimten</caption>
<caption xml:lang="nn">Oppdag nye fellesskap med Matrix Spaces</caption>
<caption xml:lang="pl">Odkrywaj nowe społeczności w Przestrzeniach Matriksa</caption>
<caption xml:lang="sl">Odkrijte nove skupnosti z Matrix Spaces</caption>
<caption xml:lang="sv">Upptäck nya gemenskaper med Matrix Spaces</caption>
<caption xml:lang="tr">Matrix Alanlar ile yeni topluluklar keşfedin</caption>
<caption xml:lang="uk">Пошук нових спільнот за допомогою Matrix Spaces</caption>
<caption xml:lang="x-test">xxDiscover new communities with Matrix Spacesxx</caption>
@@ -411,6 +415,8 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.05.2" date="2024-07-04"/>
<release version="24.05.1" date="2024-06-13"/>
<release version="24.05.0" date="2024-05-23"/>
<release version="24.02.2" date="2024-04-11"/>
<release version="24.02.1" date="2024-03-21"/>

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

@@ -77,7 +77,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
></term>
<listitem>
<para
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:exemple.org o matrix:r/root:exemple.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
>L'URI de Matrix per a un usuari o una sala. P. ex. matrix:u/usuari:example.org o matrix:r/root:example.org. Això farà que el NeoChat intenti obrir la sala o conversa indicada. </para>
</listitem>
</varlistentry>
</variablelist>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,8 @@ add_library(neochat STATIC
jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatgetcommonroomsjob.h
mediasizehelper.cpp
mediasizehelper.h
eventhandler.cpp
@@ -186,13 +188,15 @@ add_library(neochat STATIC
models/permissionsmodel.h
threepidbindhelper.cpp
threepidbindhelper.h
models/readmarkermodel.cpp
models/readmarkermodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat
QML_FILES
qml/Main.qml
@@ -282,7 +286,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
qml/CrossSigningSetupDialog.qml
DEPENDENCIES
QtCore
QtQuick

View File

@@ -13,13 +13,15 @@ import org.kde.neochat
QQC2.Popup {
id: root
padding: 16
padding: Kirigami.Units.largeSpacing
signal chosen(string path)
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
icon.name: 'mail-attachment'
@@ -28,7 +30,7 @@ QQC2.Popup {
onClicked: {
root.close();
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay);
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay);
fileDialog.chosen.connect(path => root.chosen(path));
fileDialog.open();
}
@@ -37,11 +39,8 @@ QQC2.Popup {
Kirigami.Separator {}
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
icon.name: 'insert-image'
text: i18n("Clipboard image")
onClicked: {

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(chatbar STATIC)
qt_add_qml_module(chatbar
ecm_add_qml_module(chatbar GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.chatbar
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/chatbar
QML_FILES

View File

@@ -115,7 +115,7 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
locationChooser.createObject(QQC2.Overlay.overlay, {
room: root.currentRoom
}).open();
}
@@ -364,7 +364,7 @@ QQC2.Control {
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarSource
userAvatar: _private.chatBarCache.relationUser.avatarUrl
text: _private.chatBarCache.relationMessage
onCancel: {

View File

@@ -103,14 +103,16 @@ Controller::Controller(QObject *parent)
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
if (!m_endpoint.isEmpty()) {
connection->setupPushNotifications(m_endpoint);
}
});
connect(
connection,
&NeoChatConnection::syncDone,
this,
[this, connection] {
if (!m_endpoint.isEmpty()) {
connection->setupPushNotifications(m_endpoint);
}
},
Qt::SingleShotConnection);
}
oldAccountCount = m_accountRegistry.size();
});
@@ -390,8 +392,6 @@ QString Controller::loadFileContent(const QString &path) const
return QString::fromLatin1(file.readAll());
}
#include "moc_controller.cpp"
void Controller::setTestMode(bool test)
{
testMode = test;
@@ -406,3 +406,14 @@ void Controller::removeConnection(const QString &userId)
SettingsGroup("Accounts"_ls).remove(userId);
}
}
bool Controller::csSupported() const
{
#if Quotient_VERSION_MINOR > 9
return true;
#else
return false;
#endif
}
#include "moc_controller.cpp"

View File

@@ -50,7 +50,19 @@ class Controller : public QObject
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
Q_PROPERTY(bool csSupported READ csSupported CONSTANT)
public:
/**
* @brief Define the types on inline messages that can be shown.
*/
enum MessageType {
Positive, /**< Positive message, typically green. */
Info, /**< Info message, typically highlight color. */
Error, /**< Error message, typically red. */
};
Q_ENUM(MessageType)
static Controller &instance();
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
@@ -94,6 +106,8 @@ public:
Q_INVOKABLE void removeConnection(const QString &userId);
bool csSupported() const;
private:
explicit Controller(QObject *parent = nullptr);
@@ -121,4 +135,5 @@ Q_SIGNALS:
void connectionDropped(NeoChatConnection *connection);
void activeConnectionChanged(NeoChatConnection *connection);
void accountsLoadingChanged();
void showMessage(MessageType messageType, const QString &message);
};

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(devtools STATIC)
qt_add_qml_module(devtools
ecm_add_qml_module(devtools GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.devtools
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/devtools
QML_FILES

View File

@@ -33,6 +33,7 @@ public:
* a room message.
*/
enum Type {
Author, /**< The message sender and time. */
Text, /**< A text message. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */

View File

@@ -241,17 +241,19 @@ Qt::TextFormat EventHandler::messageBodyInputFormat(const Quotient::RoomMessageE
QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
{
QString body;
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
// if filename is given or body is equal to filename,
// then body is a caption
QString filename = event.content()->fileInfo()->originalName;
QString body = event.plainBody();
if (filename.isEmpty() || filename == body) {
return QString();
}
return fileCaption;
return body;
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const EventContent::TextContent *>(event.content())->body;
} else {
@@ -319,7 +321,10 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName();
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
subjectName = sanitized(*e.prevContent()->displayName);
if (prettyPrint) {
subjectName = subjectName.toHtmlEscaped();
}
}
}
@@ -657,23 +662,30 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
QString eventId = event->id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
bool isSticker = false;
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
const EventContent::FileInfo *fileInfo;
fileInfo = roomMessageEvent->content()->fileInfo();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(fileInfo, eventId, false, false);
// if filename isn't specifically given, it is in body
// https://spec.matrix.org/latest/client-server-api/#mfile
mediaInfo["filename"_ls] = (fileInfo->originalName.isEmpty()) ? roomMessageEvent->plainBody() : fileInfo->originalName;
return mediaInfo;
} else if (event->is<StickerEvent>()) {
const EventContent::FileInfo *fileInfo;
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
isSticker = true;
return getMediaInfoFromFileInfo(fileInfo, eventId, false, true);
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId, false, isSticker);
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
@@ -949,102 +961,4 @@ QString EventHandler::getLocationAssetType() const
return assetType;
}
bool EventHandler::hasReadMarkers() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
return false;
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localMember().id());
return userIds.size() > 0;
}
QList<Quotient::RoomMember> EventHandler::getReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localMember().id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QList<Quotient::RoomMember> users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
users += m_room->member(userId);
}
return users;
}
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localMember().id());
if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
} else {
return QString();
}
}
QString EventHandler::getReadMarkersString() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localMember().id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto member = m_room->member(userId);
QString displayName;
if (member.isEmpty()) {
displayName = i18nc("A member who is not in the room has been requested.", "unknown member");
} else {
displayName = member.displayName();
}
readMarkersString += displayName + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

View File

@@ -342,43 +342,6 @@ public:
*/
QString getLocationAssetType() const;
/**
* @brief Whether the event has any read marker for other users.
*/
bool hasReadMarkers() const;
/**
* @brief Returns a list of user read marker for the event.
*
* @param maxMarkers the maximum number of users to return. Usually the number
* of user read makers shown is limited to not clutter the UI.
* This needs to be the same as used in getNumberExcessReadMarkers
* so that the markers line up with the number displayed, i.e.
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QList<Quotient::RoomMember> getReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
* be the same as used in getReadMarkers so that the value lines
* up with the number displayed, i.e. the number of users shown
* plus the excess number will be the total number of other user
* read markers at an event.
*/
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
QString getReadMarkersString() const;
private:
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;

View File

@@ -3,6 +3,7 @@
#include "imagepackevent.h"
#include <QJsonObject>
#include <Quotient/omittable.h>
using namespace Quotient;
@@ -10,10 +11,10 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
{
if (json.contains(QStringLiteral("pack"))) {
pack = ImagePackEventContent::Pack{
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["display_name"_ls]),
fromJson<std::optional<QUrl>>(json["pack"_ls].toObject()["avatar_url"_ls]),
fromJson<std::optional<QStringList>>(json["pack"_ls].toObject()["usage"_ls]),
fromJson<std::optional<QString>>(json["pack"_ls].toObject()["attribution"_ls]),
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 = std::nullopt;
@@ -30,9 +31,9 @@ ImagePackEventContent::ImagePackEventContent(const QJsonObject &json)
images += ImagePackImage{
k,
fromJson<QUrl>(json["images"_ls][k]["url"_ls].toString()),
fromJson<std::optional<QString>>(json["images"_ls][k]["body"_ls]),
fromJson<Omittable<QString>>(json["images"_ls][k]["body"_ls]),
info,
fromJson<std::optional<QStringList>>(json["images"_ls][k]["usage"_ls]),
fromJson<Omittable<QStringList>>(json["images"_ls][k]["usage"_ls]),
};
}
}

View File

@@ -8,6 +8,7 @@
#include <Quotient/accountregistry.h>
#include <Quotient/e2ee/sssshandler.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/roommember.h>
#include "controller.h"
#include "neochatconfig.h"
@@ -50,3 +51,9 @@ struct ForeignSSSSHandler {
QML_FOREIGN(Quotient::SSSSHandler)
QML_NAMED_ELEMENT(SSSSHandler)
};
struct RoomMemberForeign {
Q_GADGET
QML_FOREIGN(Quotient::RoomMember)
QML_NAMED_ELEMENT(RoomMember)
};

View File

@@ -9,5 +9,5 @@
class NeochatAdd3PIdJob : public Quotient::BaseJob
{
public:
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
explicit NeochatAdd3PIdJob(const QString &clientSecret, const QString &sid, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth)
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;

View File

@@ -5,8 +5,10 @@
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const std::optional<QJsonObject> &auth = std::nullopt);
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth)
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;

View File

@@ -4,9 +4,10 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const std::optional<QJsonObject> &auth = std::nullopt);
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

@@ -5,7 +5,7 @@
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth)
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;

View File

@@ -4,9 +4,10 @@
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const std::optional<QJsonObject> &auth = std::nullopt);
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = {});
};

View File

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

View File

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

View File

@@ -54,14 +54,19 @@ void LoginHelper::init()
m_connection = new NeoChatConnection();
}
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
});
connect(
m_connection.get(),
&Connection::loginFlowsChanged,
this,
[this]() {
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
},
Qt::SingleShotConnection);
});
connect(m_connection, &Connection::connected, this, [this] {
Q_EMIT connected();
@@ -100,9 +105,14 @@ void LoginHelper::init()
Q_EMIT Controller::instance().errorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection.get(), &Connection::syncDone, this, [this]() {
Q_EMIT loaded();
});
connect(
m_connection.get(),
&Connection::syncDone,
this,
[this]() {
Q_EMIT loaded();
},
Qt::SingleShotConnection);
}
void LoginHelper::setHomeserverReachable(bool reachable)
@@ -182,11 +192,16 @@ QUrl LoginHelper::ssoUrl() const
void LoginHelper::loginWithSso()
{
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection.get(), &Connection::loginFlowsChanged, this, [this]() {
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
});
connect(
m_connection.get(),
&Connection::loginFlowsChanged,
this,
[this]() {
SsoSession *session = m_connection->prepareForSso(m_deviceName);
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
},
Qt::SingleShotConnection);
}
bool LoginHelper::testing() const

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(login STATIC)
qt_add_qml_module(login
ecm_add_qml_module(login GENERATE_PLUGIN_SOURCE
URI org.kde.neochat.login
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/login
QML_FILES

View File

@@ -13,7 +13,7 @@ LoginStep {
id: root
FormCard.FormTextDelegate {
text: i18n("Please wait. This might take a little while.")
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
}
FormCard.AbstractFormDelegate {
contentItem: QQC2.BusyIndicator {}

View File

@@ -92,7 +92,7 @@ FormCard.FormCardPage {
}
QQC2.ToolButton {
text: i18nc("@action:button", "Remove this account")
text: i18nc("@action:button", "Log out of this account")
icon.name: "edit-delete-remove"
onClicked: Controller.removeConnection(modelData)
display: QQC2.Button.IconOnly

View File

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

View File

@@ -9,18 +9,16 @@
#include "customemojimodel.h"
#include "emojimodel.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "userlistmodel.h"
CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_userListModel(new UserListModel(this))
, m_userListModel(RoomManager::instance().userListModel())
, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
connect(this, &CompletionModel::roomChanged, this, [this]() {
m_userListModel->setRoom(m_room);
});
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance());
}

View File

@@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "customemojimodel.h"
#include <QRegularExpression>
class NeoChatConnection;
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
QRegularExpression regexp;
};
struct CustomEmojiModel::Private {
QPointer<NeoChatConnection> connection;
QList<CustomEmoji> emojies;
};

View File

@@ -26,7 +26,7 @@ int EmojiModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
int total = 0;
for (const auto &category : _emojis) {
for (const auto &category : std::as_const(_emojis)) {
total += category.count();
}
return total;
@@ -35,7 +35,7 @@ int EmojiModel::rowCount(const QModelIndex &parent) const
QVariant EmojiModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
for (const auto &category : _emojis) {
for (const auto &category : std::as_const(_emojis)) {
if (row >= category.count()) {
row -= category.count();
continue;
@@ -79,7 +79,8 @@ QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
{
QVariantList result;
for (const auto &e : _emojis.values()) {
const auto &values = _emojis.values();
for (const auto &e : values) {
for (const auto &variant : e) {
const auto &emoji = qvariant_cast<Emoji>(variant);
if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) {
@@ -121,7 +122,8 @@ QVariantList EmojiModel::emojis(Category category) const
}
if (category == HistoryNoCustom) {
QVariantList list;
for (const auto &e : emojiHistory()) {
const auto &history = emojiHistory();
for (const auto &e : history) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
@@ -224,8 +226,9 @@ QVariantList EmojiModel::categoriesWithCustom() const
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
for (const auto &historicEmoji : lastUsedEmojis()) {
for (const auto &emojiCategory : _emojis) {
const auto &lastUsed = lastUsedEmojis();
for (const auto &historicEmoji : lastUsed) {
for (const auto &emojiCategory : std::as_const(_emojis)) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);

View File

@@ -6,6 +6,7 @@
#include <Quotient/events/roommessageevent.h>
#include <Quotient/room.h>
#include "messagecontentmodel.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
@@ -29,40 +30,6 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("video"))) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>().localPath;
} else {
return QUrl();
}
} else {
return QUrl();
}
}
if (role == TempSourceRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QLatin1String("mimeType")].toString().contains(QLatin1String("image"))) {
return MediaType::Image;
} else {
return MediaType::Video;
}
}
if (role == CaptionRole) {
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("width")].toFloat();
}
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
// We need to catch this one and return true if the next media object was
// on a different day.
if (role == MessageEventModel::ShowSectionRole) {
@@ -70,6 +37,45 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay;
}
// Catch and force the author to be shown for all rows
if (role == MessageEventModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(MessageEventModel::ContentModelRole));
if (model != nullptr) {
model->setShowAuthor(true);
}
return QVariant::fromValue<MessageContentModel *>(model);
}
QVariantMap mediaInfo = mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap();
if (role == TempSourceRole) {
return mediaInfo[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == CaptionRole) {
return mapToSource(index).data(Qt::DisplayRole);
}
if (role == SourceWidthRole) {
return mediaInfo[QStringLiteral("width")].toFloat();
}
if (role == SourceHeightRole) {
return mediaInfo[QStringLiteral("height")].toFloat();
}
bool isVideo = mediaInfo[QStringLiteral("mimeType")].toString().contains(QStringLiteral("video"));
if (role == TypeRole) {
return (isVideo) ? MediaType::Video : MediaType::Image;
}
if (role == SourceRole) {
if (isVideo) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>().localPath;
}
} else {
return mediaInfo[QStringLiteral("source")].toUrl();
}
}
return sourceModel()->data(mapToSource(index), role);
}

View File

@@ -30,20 +30,23 @@
using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(event != nullptr ? event->id() : QString())
, m_event(event)
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
, m_event(loadEvent<RoomEvent>(event->fullJson()))
, m_isPending(isPending)
, m_isReply(isReply)
{
initializeModel();
}
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(eventId)
, m_isPending(isPending)
, m_isReply(isReply)
{
initializeModel();
@@ -59,10 +62,10 @@ void MessageContentModel::initializeModel()
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_event = m_room->getEvent(eventId);
m_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
Q_EMIT eventUpdated();
updateReplyModel();
updateComponents();
resetContent();
return true;
}
}
@@ -75,9 +78,10 @@ void MessageContentModel::initializeModel()
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == serverEvent->id()) {
if (m_eventId == serverEvent->id()) {
beginResetModel();
m_event = serverEvent;
m_isPending = false;
m_event = loadEvent<RoomEvent>(serverEvent->fullJson());
Q_EMIT eventUpdated();
endResetModel();
}
@@ -85,70 +89,95 @@ void MessageContentModel::initializeModel()
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_event->id() == newEvent->id()) {
if (m_eventId == newEvent->id()) {
beginResetModel();
m_event = newEvent;
m_event = loadEvent<RoomEvent>(newEvent->fullJson());
Q_EMIT eventUpdated();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
if (m_room != nullptr && m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
QString mxcUrl;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
}
if (mxcUrl.isEmpty()) {
return;
}
auto localPath = m_room->fileTransferInfo(m_event->id()).localPath.toLocalFile();
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
config.writePathEntry(mxcUrl.mid(6), localPath);
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_event->id()) {
updateComponents();
if (m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_event->id() || newEventId == m_event->id())) {
if (m_event != nullptr && (oldEventId == m_eventId || newEventId == m_eventId)) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
updateComponents(newEventId == m_event->id());
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
updateComponents();
resetContent();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
updateComponents();
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
if (m_event != nullptr) {
updateReplyModel();
}
updateComponents();
resetModel();
}
bool MessageContentModel::showAuthor() const
{
return m_showAuthor;
}
void MessageContentModel::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
if (m_event != nullptr) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
endInsertRows();
} else {
beginRemoveRows({}, 0, 0);
m_components.remove(0, 1);
endRemoveRows();
}
}
Q_EMIT showAuthorChanged();
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
@@ -164,7 +193,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {};
}
EventHandler eventHandler(m_room, m_event);
EventHandler eventHandler(m_room, m_event.get());
const auto component = m_components[index.row()];
if (role == DisplayRole) {
@@ -193,14 +222,30 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (role == EventIdRole) {
return eventHandler.getId();
}
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTime(m_isPending, lastUpdated);
}
if (role == TimeStringRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId();
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTimeString(false, QLocale::ShortFormat, m_isPending, lastUpdated);
}
if (role == AuthorRole) {
return QVariant::fromValue(eventHandler.getAuthor(false));
return QVariant::fromValue(eventHandler.getAuthor(m_isPending));
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(fileInfo());
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get()));
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
@@ -215,7 +260,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return eventHandler.getLocationAssetType();
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_event->id()));
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
@@ -254,6 +299,8 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[ComponentTypeRole] = "componentType";
roles[ComponentAttributesRole] = "componentAttributes";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
@@ -270,7 +317,7 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
return roles;
}
void MessageContentModel::updateComponents(bool isEditing)
void MessageContentModel::resetModel()
{
beginResetModel();
m_components.clear();
@@ -281,35 +328,63 @@ void MessageContentModel::updateComponents(bool isEditing)
return;
}
if (m_showAuthor) {
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
}
m_components += messageContentComponents();
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing)
{
Q_ASSERT(m_event != nullptr);
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
beginRemoveRows({}, startRow, rowCount() - 1);
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing);
if (newComponents.size() == 0) {
return;
}
beginInsertRows({}, startRow, startRow + newComponents.size() - 1);
m_components += newComponents;
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
{
QList<MessageComponent> newComponents;
if (eventCast<const Quotient::RoomMessageEvent>(m_event)
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
m_components += MessageComponent{MessageComponentType::Verification, QString(), {}};
endResetModel();
return;
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
if (m_event->isRedacted()) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
if (m_replyModel != nullptr) {
m_components += MessageComponent{MessageComponentType::Reply, QString(), {}};
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
}
if (isEditing) {
m_components += MessageComponent{MessageComponentType::Edit, QString(), {}};
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
} else {
EventHandler eventHandler(m_room, m_event);
m_components.append(componentsForType(eventHandler.messageComponentType()));
EventHandler eventHandler(m_room, m_event.get());
newComponents.append(componentsForType(eventHandler.messageComponentType()));
}
if (m_room->urlPreviewEnabled()) {
addLinkPreviews();
newComponents = addLinkPreviews(newComponents);
}
endResetModel();
return newComponents;
}
void MessageContentModel::updateReplyModel()
@@ -318,7 +393,7 @@ void MessageContentModel::updateReplyModel()
return;
}
EventHandler eventHandler(m_room, m_event);
EventHandler eventHandler(m_room, m_event.get());
if (!eventHandler.hasReply()) {
return;
}
@@ -347,36 +422,52 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, QString(), {}};
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
if (m_emptyItinerary) {
auto fileTransferInfo = fileInfo();
if (!m_isReply) {
auto fileTransferInfo = m_room->cachedFileTransferInfo(m_event.get());
#ifndef Q_OS_ANDROID
KSyntaxHighlighting::Repository repository;
const auto definitionForFile = repository.definitionForFileName(fileTransferInfo.localPath.toString());
if (definitionForFile.isValid() || QFileInfo(fileTransferInfo.localPath.path()).suffix() == QStringLiteral("txt")) {
QFile file(fileTransferInfo.localPath.path());
file.open(QIODevice::ReadOnly);
components += MessageComponent{MessageComponentType::Code,
QString::fromStdString(file.readAll().toStdString()),
{{QStringLiteral("class"), definitionForFile.name()}}};
}
Q_ASSERT(event->content() != nullptr && event->content()->fileInfo() != nullptr);
const QMimeType mimeType = event->content()->fileInfo()->mimeType;
if (mimeType.name() == QStringLiteral("text/plain") || mimeType.parentMimeTypes().contains(QStringLiteral("text/plain"))) {
QString originalName = event->content()->fileInfo()->originalName;
if (originalName.isEmpty()) {
originalName = event->plainBody();
}
KSyntaxHighlighting::Repository repository;
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName);
if (!definitionForFile.isValid()) {
definitionForFile = repository.definitionForMimeType(mimeType.name());
}
QFile file(fileTransferInfo.localPath.path());
file.open(QIODevice::ReadOnly);
components += MessageComponent{MessageComponentType::Code,
QString::fromStdString(file.readAll().toStdString()),
{{QStringLiteral("class"), definitionForFile.name()}}};
}
#endif
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
QImageReader reader(fileTransferInfo.localPath.path());
components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
QImageReader reader(fileTransferInfo.localPath.path());
components += MessageComponent{MessageComponentType::Pdf, QString(), {{QStringLiteral("size"), reader.size()}}};
}
}
} else if (m_itineraryModel != nullptr) {
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
if (m_itineraryModel->rowCount() > 0) {
updateItineraryModel();
}
} else {
updateItineraryModel();
if (m_itineraryModel != nullptr) {
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
}
}
auto body = EventHandler::rawMessageBody(*event);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced());
return components;
}
case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: {
if (!m_event->is<StickerEvent>()) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event);
@@ -418,24 +509,26 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
}
}
void MessageContentModel::addLinkPreviews()
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
{
int i = 0;
while (i < m_components.size()) {
const auto component = m_components.at(i);
while (i < inputComponents.size()) {
const auto component = inputComponents.at(i);
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
const auto links = LinkPreviewer::linkPreviews(component.content);
for (qsizetype j = 0; j < links.size(); ++j) {
const auto linkPreview = linkPreviewComponent(links[j]);
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
m_components.insert(i + j + 1, linkPreview);
inputComponents.insert(i + j + 1, linkPreview);
}
};
}
}
i++;
}
return inputComponents;
}
void MessageContentModel::closeLinkPreview(int row)
@@ -445,8 +538,7 @@ void MessageContentModel::closeLinkPreview(int row)
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
m_components.remove(row);
m_components.squeeze();
updateComponents();
endResetModel();
resetContent();
}
}
@@ -458,7 +550,7 @@ void MessageContentModel::updateItineraryModel()
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
auto filePath = fileInfo().localPath;
auto filePath = m_room->cachedFileTransferInfo(m_event.get()).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel;
m_itineraryModel = nullptr;
@@ -467,17 +559,17 @@ void MessageContentModel::updateItineraryModel()
m_itineraryModel = new ItineraryModel(this);
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
if (m_itineraryModel->rowCount() == 0) {
m_emptyItinerary = true;
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
m_emptyItinerary = true;
updateComponents();
resetContent();
}
});
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
m_emptyItinerary = true;
m_itineraryModel->deleteLater();
m_itineraryModel = nullptr;
m_emptyItinerary = true;
updateComponents();
resetContent();
});
}
m_itineraryModel->setPath(filePath.toString());
@@ -486,42 +578,4 @@ void MessageContentModel::updateItineraryModel()
}
}
FileTransferInfo MessageContentModel::fileInfo() const
{
if (m_room == nullptr || m_event == nullptr) {
return {};
}
QString mxcUrl;
int total;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
total = event->content()->fileInfo()->payloadSize;
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
total = event->image().fileInfo()->payloadSize;
}
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
if (!config.hasKey(mxcUrl.mid(6))) {
return m_room->fileTransferInfo(m_event->id());
}
const auto path = config.readPathEntry(mxcUrl.mid(6), QString());
QFileInfo info(path);
if (!info.isFile()) {
config.deleteEntry(mxcUrl);
return m_room->fileTransferInfo(m_event->id());
}
// TODO: we could check the hash here
return FileTransferInfo{
.status = FileTransferInfo::Completed,
.isUpload = false,
.progress = total,
.total = total,
.localDir = QUrl(info.dir().path()),
.localPath = QUrl::fromLocalFile(path),
};
}
#include "moc_messagecontentmodel.cpp"

View File

@@ -39,6 +39,11 @@ class MessageContentModel : public QAbstractListModel
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief Whether the author component is being shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public:
/**
* @brief Defines the model roles.
@@ -48,6 +53,8 @@ public:
ComponentTypeRole, /**< The type of component to visualise the message. */
ComponentAttributesRole, /**< The attributes of the component. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
AuthorRole, /**< The author of the event. */
MediaInfoRole, /**< The media info for the event. */
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
@@ -66,8 +73,11 @@ public:
};
Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false);
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
/**
* @brief Get the given role value at the given index.
@@ -98,19 +108,25 @@ public:
Q_INVOKABLE void closeLinkPreview(int row);
Q_SIGNALS:
void showAuthorChanged();
void eventUpdated();
private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
const Quotient::RoomEvent *m_event = nullptr;
QString m_eventSenderId;
Quotient::RoomEventPtr m_event;
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
void initializeModel();
QList<MessageComponent> m_components;
void updateComponents(bool isEditing = false);
void resetModel();
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();
@@ -119,12 +135,10 @@ private:
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
MessageComponent linkPreviewComponent(const QUrl &link);
void addLinkPreviews();
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
QList<QUrl> m_removedLinkPreviews;
void updateItineraryModel();
bool m_emptyItinerary = false;
Quotient::FileTransferInfo fileInfo() const;
};

View File

@@ -26,6 +26,7 @@
#include "messagecontentmodel.h"
#include "models/messagefiltermodel.h"
#include "models/reactionmodel.h"
#include "readmarkermodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -36,7 +37,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[DelegateTypeRole] = "delegateType";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[SectionRole] = "section";
roles[AuthorRole] = "author";
roles[HighlightRole] = "isHighlighted";
@@ -46,8 +46,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ThreadRootRole] = "threadRoot";
roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
roles[ExcessReadMarkersRole] = "excessReadMarkers";
roles[ReadMarkersStringRole] = "readMarkersString";
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
@@ -84,12 +82,17 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
return;
}
beginResetModel();
if (m_currentRoom) {
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_readMarkerModels.clear();
m_currentRoom->disconnect(this);
m_reactionModels.clear();
m_currentRoom = nullptr;
endResetModel();
}
beginResetModel();
m_currentRoom = room;
Q_EMIT roomChanged();
if (room) {
@@ -97,9 +100,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
createEventObjects(roomMessageEvent);
}
createEventObjects(&*event->viewAs<RoomEvent>());
if (event->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
}
@@ -112,11 +113,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
if (message != nullptr) {
createEventObjects(message);
}
createEventObjects(event.get());
if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
}
@@ -126,9 +123,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
});
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
createEventObjects(roomMessageEvent);
}
createEventObjects(event.get());
if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
}
@@ -149,7 +144,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
if (biggest < m_currentRoom->maxTimelineIndex()) {
auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1;
refreshEventRoles(rowBelowInserted, {MessageFilterModel::ShowAuthorRole});
refreshEventRoles(rowBelowInserted, {ContentModelRole});
}
for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) {
refreshLastUserEvents(i);
@@ -176,13 +171,13 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
endMoveRows();
movingEvent = false;
}
refreshRow(timelineBaseIndex()); // Refresh the looks
fullEventRefresh(timelineBaseIndex());
refreshLastUserEvents(0);
if (timelineBaseIndex() > 0) { // Refresh below, see #312
refreshEventRoles(timelineBaseIndex() - 1, {MessageFilterModel::ShowAuthorRole});
refreshEventRoles(timelineBaseIndex() - 1, {ContentModelRole});
}
});
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow);
connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::fullEventRefresh);
connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) {
beginRemoveRows({}, i, i);
});
@@ -192,10 +187,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
moveReadMarker(toEventId);
});
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
if (message != nullptr) {
createEventObjects(message);
}
createEventObjects(newEvent);
});
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
if (eventId.isEmpty()) { // How did we get here?
@@ -203,25 +195,42 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) {
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
createEventObjects(event);
}
createEventObjects(eventIt->event());
if (eventIt->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
}
}
refreshEventRoles(eventId, {Qt::DisplayRole});
});
connect(m_currentRoom, &Room::changed, this, [this]() {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
auto event = it->event();
refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole});
connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) {
if (changes.testFlag(Quotient::Room::Change::Other)) {
// this is slow
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
createEventObjects(it->event());
}
}
});
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] {
beginResetModel();
endResetModel();
});
connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
auto event = it->event();
if (event->senderId() == member.id()) {
refreshEventRoles(event->id(), {AuthorRole});
}
}
});
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
auto event = it->event();
if (event->senderId() == member.id()) {
refreshEventRoles(event->id(), {AuthorRole});
}
}
});
qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id();
} else {
lastReadEventId.clear();
@@ -235,14 +244,15 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
}
int MessageEventModel::refreshEvent(const QString &eventId)
void MessageEventModel::fullEventRefresh(int row)
{
return refreshEventRoles(eventId);
}
void MessageEventModel::refreshRow(int row)
{
refreshEventRoles(row);
auto roles = roleNames().keys();
// The author of an event never changes so should only be updated when a member
// changed signal is emitted.
// This also avoids any race conditions where a member is updating and this refresh
// tries to access a member event that has already been deleted.
roles.removeAll(AuthorRole);
refreshEventRoles(row, roles);
}
int MessageEventModel::timelineBaseIndex() const
@@ -335,11 +345,11 @@ QDateTime MessageEventModel::makeMessageTimestamp(const Quotient::Room::rev_iter
using Quotient::TimelineItem;
auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp);
if (rit != timeline.rend()) {
return {rit->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
return {rit->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime};
};
auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp);
if (it != timeline.end()) {
return {it->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime};
return {it->event()->originTimestamp().date(), {0, 0}, QTimeZone::LocalTime};
};
// What kind of room is that?..
@@ -358,8 +368,7 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow)
const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize());
for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) {
if ((*it)->senderId() == lastSender) {
auto idx = index(it - timelineBottom);
Q_EMIT dataChanged(idx, idx);
fullEventRefresh(it - timelineBottom);
}
}
}
@@ -491,11 +500,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == ProgressInfoRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasFileContent()) {
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt));
}
}
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
if (eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt));
}
}
@@ -504,11 +513,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getTime(isPending, lastUpdated);
}
if (role == TimeStringRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
@@ -539,19 +543,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == ReadMarkersRole) {
return QVariant::fromValue(eventHandler.getReadMarkers());
}
if (role == ExcessReadMarkersRole) {
return eventHandler.getNumberExcessReadMarkers();
}
if (role == ReadMarkersStringRole) {
return eventHandler.getReadMarkersString();
if (m_readMarkerModels.contains(evt.id())) {
return QVariant::fromValue<ReadMarkerModel *>(m_readMarkerModels[evt.id()].get());
} else {
return QVariantList();
}
}
if (role == ShowReadMarkersRole) {
return eventHandler.hasReadMarkers();
return m_readMarkerModels.contains(evt.id());
}
if (role == ReactionRole) {
@@ -612,30 +612,61 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
}
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
return;
}
auto eventId = event->id();
// ReactionModel handles updates to add and remove reactions, we only need to
// ReadMarkerModel handles updates to add and remove markers, we only need to
// handle adding and removing whole models here.
if (m_reactionModels.contains(eventId)) {
if (m_readMarkerModels.contains(eventId)) {
// If a model already exists but now has no reactions remove it
if (m_reactionModels[eventId]->rowCount() <= 0) {
m_reactionModels.remove(eventId);
if (m_readMarkerModels[eventId]->rowCount() <= 0) {
m_readMarkerModels.remove(eventId);
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole});
}
}
} else {
if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
auto memberIds = m_currentRoom->userIdsAtEvent(eventId);
memberIds.remove(m_currentRoom->localMember().id());
if (memberIds.size() > 0) {
// If a model doesn't exist and there are reactions add it.
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(event, m_currentRoom));
if (reactionModel->rowCount() > 0) {
m_reactionModels[eventId] = reactionModel;
auto newModel = QSharedPointer<ReadMarkerModel>(new ReadMarkerModel(eventId, m_currentRoom));
if (newModel->rowCount() > 0) {
m_readMarkerModels[eventId] = newModel;
if (!resetting) {
refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole});
}
}
}
}
if (const auto roomEvent = eventCast<const RoomMessageEvent>(event)) {
// ReactionModel handles updates to add and remove reactions, we only need to
// handle adding and removing whole models here.
if (m_reactionModels.contains(eventId)) {
// If a model already exists but now has no reactions remove it
if (m_reactionModels[eventId]->rowCount() <= 0) {
m_reactionModels.remove(eventId);
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
}
}
} else {
if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
// If a model doesn't exist and there are reactions add it.
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(roomEvent, m_currentRoom));
if (reactionModel->rowCount() > 0) {
m_reactionModels[eventId] = reactionModel;
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
}
}
}
}
}
}

View File

@@ -10,6 +10,7 @@
#include "linkpreviewer.h"
#include "neochatroom.h"
#include "pollhandler.h"
#include "readmarkermodel.h"
class ReactionModel;
@@ -42,7 +43,6 @@ public:
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */
HighlightRole, /**< Whether the event should be highlighted. */
@@ -59,8 +59,6 @@ public:
ShowSectionRole, /**< Whether the section header should be shown. */
ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */
ExcessReadMarkersRole, /**< The number of other users at the event after the first 5. */
ReadMarkersStringRole, /**< String with the display name and mxID of the users at the event. */
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
ReactionRole, /**< List model for this event. */
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
@@ -108,10 +106,6 @@ public:
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
int refreshEvent(const QString &eventId);
void refreshRow(int row);
private:
QPointer<NeoChatRoom> m_currentRoom = nullptr;
QString lastReadEventId;
@@ -121,6 +115,7 @@ private:
bool movingEvent = false;
KFormat m_format;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;
@@ -129,12 +124,13 @@ private:
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
void fullEventRefresh(int row);
void refreshLastUserEvents(int baseTimelineRow);
void refreshEventRoles(int row, const QList<int> &roles = {});
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
void moveReadMarker(const QString &toEventId);
void createEventObjects(const Quotient::RoomMessageEvent *event);
void createEventObjects(const Quotient::RoomEvent *event);
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
bool m_initialized = false;

View File

@@ -6,6 +6,7 @@
#include <KLocalizedString>
#include "enums/delegatetype.h"
#include "messagecontentmodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "timelinemodel.h"
@@ -91,22 +92,12 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
} else if (role == ShowAuthorRole) {
for (auto r = index.row() + 1; r < rowCount(); ++r) {
auto i = this->index(r, 0);
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole)
|| data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State
|| data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000
|| data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day()
!= data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day();
}
} else if (role == MessageEventModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(MessageEventModel::ContentModelRole));
if (model != nullptr && !showAuthor(index)) {
model->setShowAuthor(false);
}
return true;
return QVariant::fromValue<MessageContentModel *>(model);
}
return QSortFilterProxyModel::data(index, role);
}
@@ -118,10 +109,28 @@ QHash<int, QByteArray> MessageFilterModel::roleNames() const
roles[StateEventsRole] = "stateEvents";
roles[AuthorListRole] = "authorList";
roles[ExcessAuthorsRole] = "excessAuthors";
roles[ShowAuthorRole] = "showAuthor";
return roles;
}
bool MessageFilterModel::showAuthor(QModelIndex index) const
{
for (auto r = index.row() + 1; r < rowCount(); ++r) {
auto i = this->index(r, 0);
// Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved.
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole)
|| data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State
|| data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000
|| data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day()
!= data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day();
}
}
return true;
}
QString MessageFilterModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;

View File

@@ -34,7 +34,6 @@ public:
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. */
ShowAuthorRole, /**< Whether the author (name and avatar) should be shown at this message. */
LastRole, // Keep this last
};
@@ -62,6 +61,8 @@ public:
private:
bool eventIsVisible(int sourceRow, const QModelIndex &sourceParent) const;
bool showAuthor(QModelIndex index) const;
/**
* @brief Aggregation of the text of consecutive state events starting at row.
*

View File

@@ -99,7 +99,7 @@ void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind
for (const auto &rule : rules) {
QString roomId;
if (rule.conditions.size() > 0) {
for (const auto &condition : rule.conditions) {
for (const auto &condition : std::as_const(rule.conditions)) {
if (condition.key == QStringLiteral("room_id")) {
roomId = condition.pattern;
}
@@ -163,7 +163,7 @@ PushRuleSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
}
// If the rule has push conditions and one is a room ID it is a room only keyword.
if (!rule.conditions.isEmpty()) {
for (auto condition : rule.conditions) {
for (const auto &condition : std::as_const(rule.conditions)) {
if (condition.key == QStringLiteral("room_id")) {
return PushRuleSection::RoomKeywords;
}

View File

@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "readmarkermodel.h"
#include <KLocalizedString>
#include <Quotient/roommember.h>
#define MAXMARKERS 5
ReadMarkerModel::ReadMarkerModel(const QString &eventId, NeoChatRoom *room)
: QAbstractListModel(nullptr)
, m_room(room)
, m_eventId(eventId)
{
Q_ASSERT(!m_eventId.isEmpty());
Q_ASSERT(m_room != nullptr);
connect(m_room, &NeoChatRoom::changed, this, [this](Quotient::Room::Changes changes) {
if (m_room != nullptr && changes.testFlag(Quotient::Room::Change::Other)) {
auto memberIds = m_room->userIdsAtEvent(m_eventId).values();
if (memberIds == m_markerIds) {
return;
}
beginResetModel();
m_markerIds.clear();
endResetModel();
beginResetModel();
memberIds.removeAll(m_room->localMember().id());
m_markerIds = memberIds;
endResetModel();
Q_EMIT reactionUpdated();
}
});
connect(m_room, &NeoChatRoom::memberNameUpdated, this, [this](Quotient::RoomMember member) {
if (m_markerIds.contains(member.id())) {
const auto memberIndex = index(m_markerIds.indexOf(member.id()));
Q_EMIT dataChanged(memberIndex, memberIndex);
}
});
connect(m_room, &NeoChatRoom::memberAvatarUpdated, this, [this](Quotient::RoomMember member) {
if (m_markerIds.contains(member.id())) {
const auto memberIndex = index(m_markerIds.indexOf(member.id()));
Q_EMIT dataChanged(memberIndex, memberIndex);
}
});
beginResetModel();
auto userIds = m_room->userIdsAtEvent(m_eventId);
userIds.remove(m_room->localMember().id());
m_markerIds = userIds.values();
endResetModel();
Q_EMIT reactionUpdated();
}
QVariant ReadMarkerModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.row() >= rowCount()) {
qDebug() << "ReactionModel, something's wrong: index.row() >= rowCount()";
return {};
}
const auto member = m_room->member(m_markerIds.value(index.row()));
if (role == DisplayNameRole) {
return member.htmlSafeDisplayName();
}
if (role == AvatarUrlRole) {
return member.avatarUrl();
}
if (role == ColorRole) {
return member.color();
}
return {};
}
int ReadMarkerModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return std::min(int(m_markerIds.size()), MAXMARKERS);
}
QHash<int, QByteArray> ReadMarkerModel::roleNames() const
{
return {
{DisplayNameRole, "displayName"},
{AvatarUrlRole, "avatarUrl"},
{ColorRole, "memberColor"},
};
}
QString ReadMarkerModel::readMarkersString()
{
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", m_markerIds.size());
for (const auto &memberId : m_markerIds) {
auto member = m_room->member(memberId);
QString displayName = member.htmlSafeDisambiguatedName();
if (displayName.isEmpty()) {
displayName = i18nc("A member who is not in the room has been requested.", "unknown member");
}
readMarkersString += displayName + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
QString ReadMarkerModel::excessReadMarkersString()
{
if (m_room == nullptr) {
return {};
}
if (m_markerIds.size() > MAXMARKERS) {
return QStringLiteral("+ ") + QString::number(m_markerIds.size() - MAXMARKERS);
} else {
return QString();
}
}
#include "moc_readmarkermodel.cpp"

View File

@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <qtmetamacros.h>
#include "neochatroom.h"
/**
* @class ReadMarkerModel
*
* This class defines the model for visualising a list of reactions to an event.
*/
class ReadMarkerModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
Q_PROPERTY(QString readMarkersString READ readMarkersString NOTIFY reactionUpdated)
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*/
Q_PROPERTY(QString excessReadMarkersString READ excessReadMarkersString NOTIFY reactionUpdated)
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the member in the room. */
AvatarUrlRole, /**< The avatar for the member in the room. */
ColorRole, /**< The color for the member. */
};
explicit ReadMarkerModel(const QString &eventId, NeoChatRoom *room);
QString readMarkersString();
QString excessReadMarkersString();
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void reactionUpdated();
private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QList<QString> m_markerIds;
};

View File

@@ -221,6 +221,10 @@ int RoomTreeModel::rowCount(const QModelIndex &parent) const
parentItem = static_cast<RoomTreeItem *>(parent.internalPointer());
}
if (!parentItem) {
return 0;
}
return parentItem->childCount();
}

View File

@@ -82,8 +82,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
EventHandler eventHandler(m_room, &event);
switch (role) {
case ShowAuthorRole:
return true;
case AuthorRole:
return QVariant::fromValue(eventHandler.getAuthor());
case ShowSectionRole:
@@ -93,10 +91,6 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
case SectionRole:
return eventHandler.getTimeString(true);
case TimeRole:
return eventHandler.getTime();
case TimeStringRole:
return eventHandler.getTimeString(false);
case ShowReactionsRole:
return false;
case ShowReadMarkersRole:
@@ -122,6 +116,9 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
}
return {};
}
case IsEditableRole: {
return false;
}
}
return DelegateType::Message;
}
@@ -142,9 +139,6 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{AuthorRole, "author"},
{ShowSectionRole, "showSection"},
{SectionRole, "section"},
{TimeRole, "time"},
{TimeStringRole, "timeString"},
{ShowAuthorRole, "showAuthor"},
{EventIdRole, "eventId"},
{ExcessReadMarkersRole, "excessReadMarkers"},
{HighlightRole, "isHighlighted"},
@@ -158,6 +152,7 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{IsThreadedRole, "isThreaded"},
{ThreadRootRole, "threadRoot"},
{ContentModelRole, "contentModel"},
{IsEditableRole, "isEditable"},
};
}

View File

@@ -52,12 +52,9 @@ public:
*/
enum Roles {
DelegateTypeRole = Qt::DisplayRole + 1,
ShowAuthorRole,
AuthorRole,
ShowSectionRole,
SectionRole,
TimeRole,
TimeStringRole,
EventIdRole,
ExcessReadMarkersRole,
HighlightRole,
@@ -71,6 +68,7 @@ public:
IsThreadedRole,
ThreadRootRole,
ContentModelRole,
IsEditableRole,
};
Q_ENUM(Roles)
explicit SearchModel(QObject *parent = nullptr);

View File

@@ -5,7 +5,9 @@
#include <QGuiApplication>
#include <Quotient/avatar.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/room.h>
#include "enums/powerlevel.h"
#include "neochatroom.h"
@@ -25,9 +27,16 @@ void UserListModel::setRoom(NeoChatRoom *room)
}
if (m_currentRoom) {
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_currentRoom->disconnect(this);
m_currentRoom->connection()->disconnect(this);
m_currentRoom = nullptr;
m_members.clear();
endResetModel();
}
m_currentRoom = room;
if (m_currentRoom) {
@@ -39,13 +48,16 @@ void UserListModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
refreshMember(member, {AvatarRole});
});
connect(m_currentRoom, &Room::changed, this, &UserListModel::refreshAllMembers);
connect(m_currentRoom, &Room::memberListChanged, this, [this]() {
// this is slow
UserListModel::refreshAllMembers();
});
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
}
refreshAllMembers();
m_active = false;
Q_EMIT roomChanged();
}
@@ -56,6 +68,9 @@ NeoChatRoom *UserListModel::room() const
QVariant UserListModel::data(const QModelIndex &index, int role) const
{
if (!m_currentRoom) {
return {};
}
if (!index.isValid()) {
return QVariant();
}
@@ -65,25 +80,25 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
"users.count()";
return {};
}
auto member = m_members.at(index.row());
auto memberId = m_members.at(index.row());
if (role == DisplayNameRole) {
return member.disambiguatedName();
return m_currentRoom->member(memberId).disambiguatedName();
}
if (role == UserIdRole) {
return member.id();
return memberId;
}
if (role == AvatarRole) {
return member.avatarUrl();
return m_currentRoom->memberAvatar(memberId).url();
}
if (role == ObjectRole) {
return QVariant::fromValue(member);
return QVariant::fromValue(memberId);
}
if (role == PowerLevelRole) {
auto plEvent = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return 0;
}
return plEvent->powerLevelForUser(member.id());
return plEvent->powerLevelForUser(memberId);
}
if (role == PowerLevelStringRole) {
auto pl = m_currentRoom->currentState().get<RoomPowerLevelsEvent>();
@@ -93,7 +108,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
return QStringLiteral("Not Available");
}
auto userPl = pl->powerLevelForUser(member.id());
auto userPl = pl->powerLevelForUser(memberId);
return i18nc("%1 is the name of the power level, e.g. admin and %2 is the value that represents.",
"%1 (%2)",
@@ -124,7 +139,7 @@ void UserListModel::memberJoined(const Quotient::RoomMember &member)
{
auto pos = findUserPos(member);
beginInsertRows(QModelIndex(), pos, pos);
m_members.insert(pos, member);
m_members.insert(pos, member.id());
endInsertRows();
}
@@ -144,6 +159,8 @@ void UserListModel::refreshMember(const Quotient::RoomMember &member, const QLis
{
auto pos = findUserPos(member);
if (pos != m_members.size()) {
// The update will have changed the state event so we need to insert the updated member object.
m_members.insert(pos, member.id());
Q_EMIT dataChanged(index(pos), index(pos), roles);
} else {
qWarning() << "Trying to access a room member not in the user list";
@@ -153,11 +170,26 @@ void UserListModel::refreshMember(const Quotient::RoomMember &member, const QLis
void UserListModel::refreshAllMembers()
{
beginResetModel();
m_members.clear();
if (m_currentRoom != nullptr) {
m_members = m_currentRoom->members();
std::sort(m_members.begin(), m_members.end(), m_currentRoom->memberSorter());
m_members = m_currentRoom->joinedMemberIds();
#if Quotient_VERSION_MINOR > 8
MemberSorter sorter;
#else
MemberSorter sorter(m_currentRoom);
#endif
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
const auto leftPl = m_currentRoom->getUserPowerLevel(left);
const auto rightPl = m_currentRoom->getUserPowerLevel(right);
if (leftPl > rightPl) {
return true;
} else if (rightPl > leftPl) {
return false;
}
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
});
}
endResetModel();
Q_EMIT usersRefreshed();
@@ -165,15 +197,18 @@ void UserListModel::refreshAllMembers()
int UserListModel::findUserPos(const RoomMember &member) const
{
return findUserPos(member.displayName());
return findUserPos(member.id());
}
int UserListModel::findUserPos(const QString &username) const
int UserListModel::findUserPos(const QString &userId) const
{
if (!m_currentRoom) {
return 0;
}
return m_currentRoom->memberSorter().lowerBoundIndex(m_members, username);
const auto pos = std::find_if(m_members.cbegin(), m_members.cend(), [&userId](const QString &memberId) {
return userId == memberId;
});
return pos - m_members.cbegin();
}
QHash<int, QByteArray> UserListModel::roleNames() const
@@ -190,4 +225,14 @@ QHash<int, QByteArray> UserListModel::roleNames() const
return roles;
}
void UserListModel::activate()
{
if (m_active) {
return;
}
m_active = true;
refreshAllMembers();
}
#include "moc_userlistmodel.cpp"

View File

@@ -77,6 +77,8 @@ public:
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
void activate();
Q_SIGNALS:
void roomChanged();
void usersRefreshed();
@@ -92,7 +94,9 @@ private Q_SLOTS:
private:
QPointer<NeoChatRoom> m_currentRoom;
QList<Quotient::RoomMember> m_members;
QList<QString> m_members;
bool m_active = false;
int findUserPos(const Quotient::RoomMember &member) const;
[[nodiscard]] int findUserPos(const QString &username) const;

View File

@@ -255,7 +255,7 @@ Action=Popup
Name=Share
Name[ar]=شارك
Name[ca]=Compartició
Name[ca@valencia]=Compartició
Name[ca@valencia]=Compartiu
Name[cs]=Sdílet
Name[en_GB]=Share
Name[eo]=Kundividi
@@ -269,8 +269,10 @@ Name[it]=Condivisione
Name[ka]=გაზიარება
Name[lv]=Kopīgot
Name[nl]=Gedeelde
Name[nn]=Del
Name[pl]=Udostępnij
Name[sl]=Deli
Name[sv]=Dela
Name[ta]=பகிர்
Name[tr]=Paylaş
Name[uk]=Оприлюднення
@@ -292,8 +294,10 @@ Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[lv]=Satura kopīgošanas rezultāts
Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald
Comment[pl]=Wynik udostępniania kawałka treści
Comment[sl]=Rezultat deljenega kosa vsebine
Comment[sv]=Resultatet av att dela innehåll
Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு
Comment[tr]=Bir parça içerik paylaşımının sonucu
Comment[uk]=Результат оприлюднення даних

View File

@@ -187,5 +187,11 @@
<default>false</default>
</entry>
</group>
<group name="Security">
<entry name="RejectUnknownInvites" type="bool">
<label>Reject unknown invites</label>
<default>false</default>
</entry>
</group>
</kcfg>

View File

@@ -12,23 +12,20 @@
#include "linkpreviewer.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include <Quotient/connection.h>
#include <Quotient/csapi/cross_signing.h>
#include <Quotient/e2ee/cryptoutils.h>
#include <Quotient/e2ee/e2ee_common.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
#include <olm/pk.h>
#include <KLocalizedString>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/csapi/versions.h>
#include <Quotient/database.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
@@ -71,6 +68,10 @@ void NeoChatConnection::connectSignals()
});
connect(this, &NeoChatConnection::syncDone, this, [this] {
setIsOnline(true);
connect(this, &NeoChatConnection::syncDone, this, [this]() {
NotificationsManager::instance().handleNotifications(this);
});
});
connect(this, &NeoChatConnection::networkError, this, [this]() {
setIsOnline(false);
@@ -132,6 +133,21 @@ void NeoChatConnection::connectSignals()
Q_EMIT homeNotificationsChanged();
Q_EMIT homeHaveHighlightNotificationsChanged();
});
// Fetch unstable features
// TODO: Expose unstableFeatures() in libQuotient
connect(
this,
&Connection::connected,
this,
[this] {
auto job = callApi<GetVersionsJob>(BackgroundRequest);
connect(job, &GetVersionsJob::success, this, [this, job] {
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_ls);
Q_EMIT canCheckMutualRoomsChanged();
});
},
Qt::SingleShotConnection);
}
int NeoChatConnection::badgeNotificationCount() const
@@ -200,6 +216,11 @@ QVariantList NeoChatConnection::getSupportedRoomVersions() const
return supportedRoomVersions;
}
bool NeoChatConnection::canCheckMutualRooms() const
{
return m_canCheckMutualRooms;
}
void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
{
auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
@@ -326,9 +347,14 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic, co
connect(job, &CreateRoomJob::failure, this, [job] {
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()), {});
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
RoomManager::instance().resolveResource(room->id());
});
connect(
this,
&Connection::newRoom,
this,
[](Room *room) {
RoomManager::instance().resolveResource(room->id());
},
Qt::SingleShotConnection);
}
void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
@@ -358,9 +384,14 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
connect(job, &CreateRoomJob::failure, this, [job] {
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()), {});
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
RoomManager::instance().resolveResource(room->id());
});
connect(
this,
&Connection::newRoom,
this,
[](Room *room) {
RoomManager::instance().resolveResource(room->id());
},
Qt::SingleShotConnection);
}
bool NeoChatConnection::directChatExists(Quotient::User *user)
@@ -543,248 +574,4 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return previewer;
}
void NeoChatConnection::setupCrossSigningKeys(const QString &password)
{
auto masterKeyPrivate = getRandom<32>();
auto masterKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray masterKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(masterKeyContext.get(),
masterKeyPublic.data(),
masterKeyPublic.length(),
masterKeyPrivate.data(),
masterKeyPrivate.viewAsByteArray().length());
auto selfSigningKeyPrivate = getRandom<32>();
auto selfSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray selfSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(selfSigningKeyContext.get(),
selfSigningKeyPublic.data(),
selfSigningKeyPublic.length(),
selfSigningKeyPrivate.data(),
selfSigningKeyPrivate.viewAsByteArray().length());
auto userSigningKeyPrivate = getRandom<32>();
auto userSigningKeyContext = makeCStruct(olm_pk_signing, olm_pk_signing_size, olm_clear_pk_signing);
QByteArray userSigningKeyPublic(olm_pk_signing_public_key_length(), 0);
olm_pk_signing_key_from_seed(userSigningKeyContext.get(),
userSigningKeyPublic.data(),
userSigningKeyPublic.length(),
userSigningKeyPrivate.data(),
userSigningKeyPrivate.viewAsByteArray().length());
database()->storeEncrypted("m.cross_signing.master"_ls, masterKeyPrivate.viewAsByteArray());
database()->storeEncrypted("m.cross_signing.self_signing"_ls, selfSigningKeyPrivate.viewAsByteArray());
database()->storeEncrypted("m.cross_signing.user_signing"_ls, userSigningKeyPrivate.viewAsByteArray());
auto masterKey = CrossSigningKey{
.userId = userId(),
.usage = {"master"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic), QString::fromLatin1(masterKeyPublic)}},
.signatures = {},
};
auto selfSigningKey = CrossSigningKey{
.userId = userId(),
.usage = {"self_signing"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(selfSigningKeyPublic), QString::fromLatin1(selfSigningKeyPublic)}},
};
auto userSigningKey = CrossSigningKey{
.userId = userId(),
.usage = {"user_signing"_ls},
.keys = {{"ed25519:"_ls + QString::fromLatin1(userSigningKeyPublic), QString::fromLatin1(userSigningKeyPublic)}},
};
auto selfSigningKeyJson = toJson(selfSigningKey);
selfSigningKeyJson.remove("signatures"_ls);
selfSigningKey.signatures = QJsonObject{
{userId(),
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(selfSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
auto userSigningKeyJson = toJson(userSigningKey);
userSigningKeyJson.remove("signatures"_ls);
userSigningKey.signatures = QJsonObject{
{userId(),
QJsonObject{{"ed25519:"_ls + QString::fromLatin1(masterKeyPublic),
QString::fromLatin1(sign(masterKeyPrivate.viewAsByteArray(), QJsonDocument(userSigningKeyJson).toJson(QJsonDocument::Compact)))}}}};
const auto encodedMasterKeyPrivate = viewAsByteArray(masterKeyPrivate).toBase64();
const auto encodedSelfSigningKeyPrivate = viewAsByteArray(selfSigningKeyPrivate).toBase64();
const auto encodedUserSigningKeyPrivate = viewAsByteArray(userSigningKeyPrivate).toBase64();
callApi<UploadCrossSigningKeysJob>(masterKey, selfSigningKey, userSigningKey, std::nullopt)
.then(
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
finishCrossSigningSetup(encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
QString::fromLatin1(masterKeyPublic));
},
[this,
password,
masterKey,
selfSigningKey,
userSigningKey,
encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
masterKeyPublic](const auto &job) {
callApi<UploadCrossSigningKeysJob>(masterKey,
selfSigningKey,
userSigningKey,
AuthenticationData{
.type = "m.login.password"_ls,
.session = job->jsonData()["session"_ls].toString(),
.authInfo =
QVariantHash{
{"password"_ls, password},
{"identifier"_ls,
QJsonObject{
{"type"_ls, "m.id.user"_ls},
{"user"_ls, userId()},
}},
},
})
.then(
[this, encodedMasterKeyPrivate, encodedSelfSigningKeyPrivate, encodedUserSigningKeyPrivate, masterKeyPublic]() {
finishCrossSigningSetup(encodedMasterKeyPrivate,
encodedSelfSigningKeyPrivate,
encodedUserSigningKeyPrivate,
QString::fromLatin1(masterKeyPublic));
},
[]() {
qWarning() << "Failed to setup cross-signing keys";
});
});
}
void NeoChatConnection::finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
const QByteArray &encodedSelfSigningKeyPrivate,
const QByteArray &encodedUserSigningKeyPrivate,
const QString &masterKeyPublic)
{
auto key = getRandom(32);
QByteArray data = QByteArrayLiteral("\x8B\x01") + viewAsByteArray(key);
data[8] &= ~(1 << 7); // Byte 63 needs to be set to 0
data.append(std::accumulate(data.cbegin(), data.cend(), uint8_t{0}, std::bit_xor<>()));
data = base58Encode(data);
QList<QString> groups;
for (auto i = 0; i < data.size() / 4; i++) {
groups += QString::fromLatin1(data.mid(i * 4, i * 4 + 4));
}
// The key to be shown to the user
const auto formatted = groups.join(QStringLiteral(" "));
Q_EMIT showSecurityKey(formatted);
const auto identifier = QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha256));
setAccountData("m.secret_storage.default_key"_ls,
{
{"key"_ls, identifier},
});
struct EncryptionData {
QString ciphertext;
QString iv;
QString mac;
};
auto encryptAccountData = [this, &key, identifier](QLatin1String info, const QByteArray &plainText) {
const auto iv = getRandom(16);
const auto &kdfKeys = hkdfSha256(byte_view_t<>(key).subspan<0, DefaultPbkdf2KeyLength>(), zeroes<32>(), asCBytes<>(info));
if (!kdfKeys.has_value()) {
qWarning() << "Key Setup: Failed to calculate HKDF" << info;
// Q_EMIT error(DecryptionError);
return EncryptionData{};
}
const auto &encrypted = aesCtr256Encrypt(plainText, kdfKeys.value().aes(), asCBytes<AesBlockSize>(iv));
if (!encrypted.has_value()) {
qWarning() << "Key Setup: Failed to encrypt test keys" << info;
// emit error(DecryptionError);
return EncryptionData{};
}
const auto &hmacResult = hmacSha256(kdfKeys.value().mac(), encrypted.value());
if (!hmacResult.has_value()) {
qWarning() << "Key Setup: Failed to calculate HMAC" << info;
// emit error(DecryptionError);
return EncryptionData{};
}
return EncryptionData{
.ciphertext = QString::fromLatin1(encrypted.value().toBase64()),
.iv = QString::fromLatin1(iv.viewAsByteArray()),
.mac = QString::fromLatin1(hmacResult.value().toBase64()),
};
};
auto testData = encryptAccountData({}, zeroedByteArray());
setAccountData("m.secret_storage.key.%1"_ls.arg(identifier),
{
{"algorithm"_ls, "m.secret_storage.v1.aes-hmac-sha2"_ls},
{"iv"_ls, testData.iv},
{"mac"_ls, testData.mac},
});
auto masterData = encryptAccountData("m.cross_signing.master"_ls, encodedMasterKeyPrivate);
setAccountData("m.cross_signing.master"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, masterData.iv},
{"ciphertext"_ls, masterData.ciphertext},
{"mac"_ls, masterData.mac},
}}}}});
auto selfSigningData = encryptAccountData("m.cross_signing.self_signing"_ls, encodedSelfSigningKeyPrivate);
setAccountData("m.cross_signing.self_signing"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, selfSigningData.iv},
{"ciphertext"_ls, selfSigningData.ciphertext},
{"mac"_ls, selfSigningData.mac},
}}}}});
auto userSigningData = encryptAccountData("m.cross_signing.user_signing"_ls, encodedUserSigningKeyPrivate);
setAccountData("m.cross_signing.user_signing"_ls,
{{"encrypted"_ls,
QJsonObject{{identifier,
QJsonObject{
{"iv"_ls, userSigningData.iv},
{"ciphertext"_ls, userSigningData.ciphertext},
{"mac"_ls, userSigningData.mac},
}}}}});
// Adding the verified master key manually so that we don't have to wait until we receive it from the server
auto query = database()->prepareQuery(QStringLiteral("INSERT INTO master_keys(userId, key, verified) VALUES (:userId, :key, :verified);"));
query.bindValue(":userId"_ls, userId());
query.bindValue(":key"_ls, masterKeyPublic);
query.bindValue(":verified"_ls, true);
database()->execute(query);
const auto selfSigningKey = database()->loadEncrypted("m.cross_signing.self_signing"_ls);
QHash<QString, QHash<QString, QJsonObject>> signatures;
auto json = QJsonObject{
{"keys"_ls,
QJsonObject{
{"ed25519:"_ls + deviceId(), edKeyForUserDevice(userId(), deviceId())},
{"curve25519:"_ls + deviceId(), curveKeyForUserDevice(userId(), deviceId())},
}},
{"algorithms"_ls, QJsonArray{"m.olm.v1.curve25519-aes-sha2"_ls, "m.megolm.v1.aes-sha2"_ls}},
{"device_id"_ls, deviceId()},
{"user_id"_ls, userId()},
};
auto signature = sign(selfSigningKey, QJsonDocument(json).toJson(QJsonDocument::Compact));
json["signatures"_ls] = QJsonObject{
{userId(),
QJsonObject{
{"ed25519:"_ls + database()->selfSigningPublicKey(), QString::fromLatin1(signature)},
}},
};
signatures[userId()][deviceId()] = json;
callApi<UploadCrossSigningSignaturesJob>(signatures).onFailure([](const auto &job) {
qWarning() << "Failed to upload self-signing signature" << job->error() << job->errorString();
});
// TODO start a key backup and store in account data
}
#include "moc_neochatconnection.cpp"

View File

@@ -79,6 +79,11 @@ class NeoChatConnection : public Quotient::Connection
*/
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
/**
* @brief Whether the server supports querying a user's mutual rooms.
*/
Q_PROPERTY(bool canCheckMutualRooms READ canCheckMutualRooms NOTIFY canCheckMutualRoomsChanged)
public:
/**
* @brief Defines the status after an attempt to change the password on an account.
@@ -95,6 +100,7 @@ public:
Q_INVOKABLE void logout(bool serverSideLogout);
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
bool canCheckMutualRooms() const;
/**
* @brief Change the password for an account.
@@ -184,8 +190,6 @@ public:
LinkPreviewer *previewerForLink(const QUrl &link);
Q_INVOKABLE void setupCrossSigningKeys(const QString &password);
Q_SIGNALS:
void labelChanged();
void identityServerChanged();
@@ -198,7 +202,7 @@ Q_SIGNALS:
void passwordStatus(NeoChatConnection::PasswordStatus status);
void userConsentRequired(QUrl url);
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
void showSecurityKey(const QString &securityKey);
void canCheckMutualRoomsChanged();
private:
bool m_isOnline = true;
@@ -207,12 +211,10 @@ private:
ThreePIdModel *m_threePIdModel;
void connectSignals();
void finishCrossSigningSetup(const QByteArray &encodedMasterKeyPrivate,
const QByteArray &encodedSelfSigningKeyPrivate,
const QByteArray &encodedUserSigningKeyPrivate,
const QString &masterKeyPublic);
int m_badgeNotificationCount = 0;
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
bool m_canCheckMutualRooms = false;
};

View File

@@ -36,10 +36,12 @@
#include "chatbarcache.h"
#include "clipboard.h"
#include "controller.h"
#include "eventhandler.h"
#include "events/joinrulesevent.h"
#include "events/pollevent.h"
#include "filetransferpseudojob.h"
#include "jobs/neochatgetcommonroomsjob.h"
#include "neochatconfig.h"
#include "notificationsmanager.h"
#include "roomlastmessageprovider.h"
@@ -68,6 +70,26 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
setFileUploadingProgress(0);
setHasFileUploading(false);
});
connect(this, &Room::fileTransferCompleted, this, [this](QString eventId) {
const auto evtIt = findInTimeline(eventId);
if (evtIt != messageEvents().rend()) {
const auto m_event = evtIt->viewAs<RoomEvent>();
QString mxcUrl;
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) {
if (event->hasFileContent()) {
mxcUrl = event->content()->fileInfo()->url().toString();
}
} else if (auto event = eventCast<const Quotient::StickerEvent>(m_event)) {
mxcUrl = event->image().fileInfo()->url().toString();
}
if (mxcUrl.isEmpty()) {
return;
}
auto localPath = this->fileTransferInfo(eventId).localPath.toLocalFile();
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
config.writePathEntry(mxcUrl.mid(6), localPath);
}
});
connect(this, &Room::addedMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::cleanupExtraEventRange);
@@ -96,23 +118,52 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
});
connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged);
connectSingleShot(this, &Room::baseStateLoaded, this, [this]() {
updatePushNotificationState(QStringLiteral("m.push_rules"));
connect(
this,
&Room::baseStateLoaded,
this,
[this]() {
updatePushNotificationState(QStringLiteral("m.push_rules"));
Q_EMIT canEncryptRoomChanged();
if (this->joinState() != JoinState::Invite) {
return;
}
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
QImage avatar_image;
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
} else {
qWarning() << "using this room's avatar";
avatar_image = avatar(128);
}
NotificationsManager::instance().postInviteNotification(this, displayName(), member(roomMemberEvent->senderId()).htmlSafeDisplayName(), avatar_image);
});
Q_EMIT canEncryptRoomChanged();
if (this->joinState() != JoinState::Invite) {
return;
}
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
auto showNotification = [this, roomMemberEvent] {
QImage avatar_image;
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
} else {
qWarning() << "using this room's avatar";
avatar_image = avatar(128);
}
NotificationsManager::instance().postInviteNotification(this,
displayName(),
member(roomMemberEvent->senderId()).htmlSafeDisplayName(),
avatar_image);
};
if (NeoChatConfig::rejectUnknownInvites()) {
auto job = this->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, roomMemberEvent, showNotification] {
QJsonObject replyData = job->jsonData();
if (replyData.contains(QStringLiteral("joined"))) {
const bool inAnyOfOurRooms = !replyData[QStringLiteral("joined")].toArray().isEmpty();
if (inAnyOfOurRooms) {
showNotification();
} else {
leaveRoom();
}
}
});
} else {
showNotification();
}
},
Qt::SingleShotConnection);
connect(this, &Room::changed, this, [this] {
Q_EMIT canEncryptRoomChanged();
Q_EMIT parentIdsChanged();
@@ -840,11 +891,18 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
{
auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
if (!powerLevelEvent) {
return 0;
if (!successorId().isEmpty()) {
return 0; // No one can upgrade a room that's already upgraded
}
return powerLevelEvent->powerLevelForUser(userId);
const auto &mId = userId.isEmpty() ? connection()->userId() : userId;
if (const auto *plEvent = currentState().get<RoomPowerLevelsEvent>()) {
return plEvent->powerLevelForUser(mId);
}
if (const auto *createEvent = creation()) {
return createEvent->senderId() == mId ? 100 : 0;
}
return 0; // That's rather weird but may happen, according to rvdh
}
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
@@ -1280,7 +1338,6 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
m_currentPushNotificationState = state;
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
}
void NeoChatRoom::updatePushNotificationState(QString type)
@@ -1341,10 +1398,9 @@ void NeoChatRoom::updatePushNotificationState(QString type)
void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason)
{
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
connect(job, &BaseJob::finished, this, [this, job]() {
connect(job, &BaseJob::finished, this, [job]() {
if (job->error() == BaseJob::Success) {
Q_EMIT showMessage(Positive, i18n("Report sent successfully."));
Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("Report sent successfully."));
}
});
}
@@ -1365,7 +1421,7 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
const auto event = evtIt->viewAs<RoomMessageEvent>();
if (event->hasFileContent()) {
const auto transferInfo = fileTransferInfo(eventId);
const auto transferInfo = cachedFileTransferInfo(event);
if (transferInfo.completed()) {
UrlHelper helper;
helper.openUrl(transferInfo.localPath);
@@ -1373,15 +1429,20 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
downloadFile(eventId,
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
UrlHelper helper;
helper.openUrl(transferInfo.localPath);
}
});
connect(
this,
&Room::fileTransferCompleted,
this,
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
UrlHelper helper;
helper.openUrl(transferInfo.localPath);
}
},
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
}
}
}
@@ -1401,20 +1462,66 @@ void NeoChatRoom::copyEventMedia(const QString &eventId)
downloadFile(eventId,
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
}
});
connect(
this,
&Room::fileTransferCompleted,
this,
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
Q_UNUSED(localFile);
Q_UNUSED(fileMetadata);
if (id == eventId) {
auto transferInfo = fileTransferInfo(eventId);
Clipboard clipboard;
clipboard.setImage(transferInfo.localPath);
}
},
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
}
}
}
}
FileTransferInfo NeoChatRoom::cachedFileTransferInfo(const Quotient::RoomEvent *event) const
{
QString mxcUrl;
int total = 0;
if (auto evt = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (evt->hasFileContent()) {
mxcUrl = evt->content()->fileInfo()->url().toString();
total = evt->content()->fileInfo()->payloadSize;
}
} else if (auto evt = eventCast<const Quotient::StickerEvent>(event)) {
mxcUrl = evt->image().fileInfo()->url().toString();
total = evt->image().fileInfo()->payloadSize;
}
FileTransferInfo transferInfo = fileTransferInfo(event->id());
if (transferInfo.active()) {
return transferInfo;
}
auto config = KSharedConfig::openStateConfig(QStringLiteral("neochatdownloads"))->group(QStringLiteral("downloads"));
if (!config.hasKey(mxcUrl.mid(6))) {
return transferInfo;
}
const auto path = config.readPathEntry(mxcUrl.mid(6), QString());
QFileInfo info(path);
if (!info.isFile()) {
config.deleteEntry(mxcUrl);
return transferInfo;
}
// TODO: we could check the hash here
return FileTransferInfo{
.status = FileTransferInfo::Completed,
.isUpload = false,
.progress = total,
.total = total,
.localDir = QUrl(info.dir().path()),
.localPath = QUrl::fromLocalFile(path),
};
}
ChatBarCache *NeoChatRoom::mainCache() const
{
return m_mainCache;
@@ -1713,4 +1820,13 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
setState(type, stateKey, QJsonDocument::fromJson(content).object());
}
#if Quotient_VERSION_MINOR == 8
QList<RoomMember> NeoChatRoom::otherMembersTyping() const
{
auto memberTyping = membersTyping();
memberTyping.removeAll(localMember());
return memberTyping;
}
#endif
#include "moc_neochatroom.cpp"

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