Compare commits

...

145 Commits

Author SHA1 Message Date
James Graham
45c46ddcbb Tobias' fix 2024-09-12 09:12:53 +01:00
James Graham
31d83ac0e3 Hack to see if kquickimageeditor is also the problem 2024-09-12 08:12:31 +01:00
James Graham
909eec30d2 Temp disable color scheme so CI builds 2024-09-11 08:51:59 +01:00
James Graham
dbed3e99c2 Create a mobile version of FileDelegateContextMenu with no purpose import 2024-09-11 08:33:49 +01:00
l10n daemon script
4d0db0b5c2 GIT_SILENT Sync po/docbooks with svn 2024-09-10 01:26:40 +00:00
l10n daemon script
6e49aaf17b GIT_SILENT Sync po/docbooks with svn 2024-09-09 01:28:45 +00:00
Joshua Goins
1092d75f2e Remove vestigial references to window geometry
These weren't removed in d165cd955d
because I forgot.
2024-09-08 07:32:00 +00:00
Joshua Goins
8059c3797d Remove unused lambda capture variables
This removes two compile time warnings.
2024-09-08 07:24:24 +00:00
Joshua Goins
1302d62ad9 SpaceDrawer: Remove seemingly non-existent signal call 2024-09-08 07:23:58 +00:00
Joshua Goins
8eaae4034d Shorten the error message passive notification
The default timeout is a bit long, "short" is 3 seconds shorter than the
default. For  long-term network errors, we have a banner telling you so
anyway. This should hopefully reduce the notification spam when you have
temporary network dropouts.
2024-09-08 07:23:30 +00:00
l10n daemon script
354e3414a1 GIT_SILENT Sync po/docbooks with svn 2024-09-08 01:28:05 +00:00
James Graham
fc24beae6d Use im-kick-user consistently for logout.
I went with im-kick-user as it's fully red at all sizes.

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

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

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

Also define some of the commonly used strings

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -13,3 +13,4 @@ include:
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml

View File

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

View File

@@ -107,8 +107,13 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(QLatin1String("$153456789:example.org")));
});
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));

View File

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

View File

@@ -46,6 +46,7 @@ private Q_SLOTS:
void sendCustomEmojiCode_data();
void sendCustomEmojiCode();
void receiveSpacelessSelfClosingTag();
void receiveStripReply();
void receivePlainTextIn();
@@ -252,6 +253,19 @@ void TextHandlerTest::sendCustomEmojiCode()
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}
void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
const QString testInputString = QStringLiteral("Test...<br/>...ing");
const QString testRichOutputString = QStringLiteral("Test...<br/>...ing");
const QString testPlainOutputString = QStringLiteral("Test...\n...ing");
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}
void TextHandlerTest::receiveStripReply()
{
const QString testInputString = QStringLiteral(
@@ -274,6 +288,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
QTest::newRow("ampersand") << QStringLiteral("a &amp; b") << QStringLiteral("a & b");
QTest::newRow("quote") << QStringLiteral("&quot;a and b&quot;") << QStringLiteral("\"a and b\"");
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
QTest::newRow("unescape") << QStringLiteral("can&#x27;t") << QStringLiteral("can't");
}
void TextHandlerTest::receiveRichInPlainOut()
@@ -448,6 +463,9 @@ void TextHandlerTest::receiveRichPlainUrl()
QString testOutputStringMxId = QStringLiteral(
"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>");
QString testInputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b");
QString testOutputStringMxIdWithPrefix = QStringLiteral("a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b");
TextHandler testTextHandler;
testTextHandler.setData(testInputStringLink1);
@@ -461,6 +479,9 @@ void TextHandlerTest::receiveRichPlainUrl()
testTextHandler.setData(testInputStringMxId);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId);
testTextHandler.setData(testInputStringMxIdWithPrefix);
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix);
}
void TextHandlerTest::receiveRichEdited_data()
@@ -535,7 +556,7 @@ void TextHandlerTest::componentOutput_data()
"someField }\nCustomQml {\n someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you "
"defined the id")
<< QList<MessageComponent>{
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like<br/>"), {}},
MessageComponent{
MessageComponentType::Code,
QStringLiteral(

View File

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

View File

@@ -25,6 +25,7 @@
<name xml:lang="fi">NeoChat</name>
<name xml:lang="fr">NeoChat</name>
<name xml:lang="gl">NeoChat</name>
<name xml:lang="he">NeoChat</name>
<name xml:lang="hu">NeoChat</name>
<name xml:lang="ia">Neochat</name>
<name xml:lang="id">NeoChat</name>
@@ -61,6 +62,7 @@
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="he">התכתבות עם החברים שלך ב־matrix</summary>
<summary xml:lang="hu">Csevegjen barátaival a matrixon</summary>
<summary xml:lang="ia">Starta Conversation con tu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
@@ -89,6 +91,8 @@
<p xml:lang="eu">NeoChat, Matrix sarearen abantaila guztiei probetsua ateratzeko aukera ematen dizun berriketa aplikaizo bat da. Zure familiari, kideei eta lagunei testu mezuak, bideoak eta audio fitxategiak era seguruan bidaltzeko aukera ematen dizu.</p>
<p xml:lang="fi">NeoChat on keskustelusovellus, jolla Matrix-verkosta saa täyden hyödyn. Se tarjoaa salatun kanavan lähettää perheelle, työkavereille ja ystäville tekstiviestejä sekä video- ja äänitiedostoja.</p>
<p xml:lang="fr">NeoChat est une application de discussions vous permettant de profiter pleinement du réseau Matrix. Elle vous offre un moyen sécurisé d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos ami(e)s.</p>
<p xml:lang="gl">NeoChat é unha aplicación de conversa que lle permite usar todas as funcionalidades da rede Matrix. Fornece unha forma segura de enviar mensaxes de texto e ficheiros de vídeo e son a familiares, amizades ou no traballo.</p>
<p xml:lang="he">NeoChat הוא יישום התכתבות שמאפשר לך לנצל את רשת Matrix במלואה. הוא מספק דרך מאובטחת לשליחת הודעות כתובות, סרטונים וקובצי שמע למשפחה, לעמיתים לעבודה ולחברים.</p>
<p xml:lang="hu">A NeoChat egy olyan csevegőalkalmazás, amellyel teljes mértékben kihasználhatja a Matrix hálózatot. Biztonságos módot biztosít szöveges üzenetek, videók és hangfájlok küldéséhez családtagjainak, kollégáinak és barátainak.</p>
<p xml:lang="ia">NeoChat es un app de conversation que te permitte prender avantage plen del rete Matrix. Il te forni un modo secur de inviar messages de texto, videos e files audio a tui familia, collegas e amicos.</p>
<p xml:lang="it">NeoChat è un'applicazione di chat che ti consente di sfruttare appieno la rete Matrix. Ti fornisce un modo sicuro per inviare messaggi di testo, video e file audio a familiari, colleghi e amici.</p>
@@ -114,6 +118,7 @@
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
<p xml:lang="he">NeoChat מתיימר להיות יישום עתיר יכולות לפי מפרט Matrix. כיוון שזה ייעודו, כל מה שבמפרט היציב עם חריגות משמעותיות כגון VoIP, שרשורים ועוד מגוון היבטים של הצפנה מקצה לקצה נתמכים גם הם. יש מספר השמטות קטן עקב העובדה שהמפרט של Matrix ממשיך להתפתח אך המטרה היא להמשיך לספק תמיכה בסופו של דבר לכל המפרט.</p>
<p xml:lang="hu">A NeoChat célja, hogy a Matrix specifikációnak megfelelő teljes funkcionalitású alkalmazás legyen. Mint ilyen, a jelenlegi stabil specifikáció támogatott a VoIP, a szálak és a végpontok közötti titkosítás egyes elemeinek kivételével. Van még néhány kisebb hiányosság annak köszönhetően, hogy a Matrix specifikáció folyamatosan fejlődik, de végső cél a teljes specifikáció megvalósítása.</p>
<p xml:lang="ia">NeoChat aspira a esser un application plenmente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
@@ -142,6 +147,7 @@
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
<p xml:lang="he">מטבע הדברים, הפיתוח של NeoChat תומך במגוון יכולות מפוקפקות כתלות בהתפתחות המפרט הטכני של Matrix. היכולות האלה הן:</p>
<p xml:lang="hu">A Matrix specifikáció fejlesztésének jellegéből adódóan a NeoChat számos instabil funkciót is támogat. Jelenleg a következőket:</p>
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
@@ -172,6 +178,7 @@
<li xml:lang="fi">Kyselyt MSC3381</li>
<li xml:lang="fr">Sondages - MSC3381</li>
<li xml:lang="gl">Enquisas — MSC3381</li>
<li xml:lang="he">סקרים - MSC3381</li>
<li xml:lang="hu">Szavazások - MSC3381</li>
<li xml:lang="ia">Inquestas - MSC3381</li>
<li xml:lang="it">Sondaggi - MSC3381</li>
@@ -200,6 +207,7 @@
<li xml:lang="fi">Tarrapakkaukset MSC2545</li>
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
<li xml:lang="he">חבילות מדבקות - MSC2545</li>
<li xml:lang="hu">Matricacsomagok - MSC2545</li>
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
@@ -229,6 +237,7 @@
<li xml:lang="fi">Sijaintitapahtumat MSC3488</li>
<li xml:lang="fr">Événements de lieu - MSC3488</li>
<li xml:lang="gl">Localización de eventos — MSC3488</li>
<li xml:lang="he">אירועי מקום - MSC3488</li>
<li xml:lang="hu">Események helyadatai - MSC3488</li>
<li xml:lang="ia">Eventos de Location - MSC3488</li>
<li xml:lang="it">Località eventi - MSC3488</li>
@@ -292,6 +301,7 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -323,6 +333,8 @@
<caption xml:lang="eu">Ezagutu komunitate berriak Matrixeko Tokiak erabiliz</caption>
<caption xml:lang="fi">Löydä uusia yhteisöjä Matrix Spacesillä</caption>
<caption xml:lang="fr">Découvrez de nouvelles communautés avec les espaces sous Matrix</caption>
<caption xml:lang="gl">Descubra novas comunidades dos espazos de Matrix.</caption>
<caption xml:lang="he">אפשר להיחשף לקהילות חדשות דרך Matrix Spaces</caption>
<caption xml:lang="hu">Fedezzen fel új közösségeket a Matrix Terek segítségével</caption>
<caption xml:lang="ia">Discoperi nove communitate con Matrix Spaces (Spatios de Matrix)</caption>
<caption xml:lang="it">Scopri nuove comunità con Matrix Spaces</caption>
@@ -358,6 +370,7 @@
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
<caption xml:lang="he">תצוגה ראשית עם רשימת חדרים, צ׳אט ופרטי חדר</caption>
<caption xml:lang="hu">A fő nézet a szobalistával, csevegéssel és szobainformációkkal</caption>
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
@@ -391,6 +404,7 @@
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
<caption xml:lang="fr">Écran de connexion</caption>
<caption xml:lang="gl">Pantalla de identificación.</caption>
<caption xml:lang="he">מסך כניסה</caption>
<caption xml:lang="hu">Bejelentkező képernyő</caption>
<caption xml:lang="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption>
@@ -415,6 +429,8 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.08.1" date="2024-09-12"/>
<release version="24.08.0" date="2024-08-22"/>
<release version="24.05.2" date="2024-07-04"/>
<release version="24.05.1" date="2024-06-13"/>
<release version="24.05.0" date="2024-05-23"/>

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5724
po/gl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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
@@ -188,6 +190,10 @@ add_library(neochat STATIC
threepidbindhelper.h
models/readmarkermodel.cpp
models/readmarkermodel.h
neochatroommember.cpp
neochatroommember.h
models/threadmodel.cpp
models/threadmodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -236,11 +242,9 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/FileDelegateContextMenuMobile.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/RoomSearchPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
@@ -284,6 +288,8 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -301,7 +307,7 @@ add_subdirectory(devtools)
add_subdirectory(login)
add_subdirectory(chatbar)
if(UNIX)
if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml)
else()
set_source_files_properties(qml/ShareActionStub.qml PROPERTIES
@@ -421,7 +427,7 @@ if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PUBLIC NEOCHAT_FLATPAK)

View File

@@ -56,7 +56,7 @@ void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
handleMessage(chatBarCache->text(), handledText, chatBarCache);
}
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)

View File

@@ -219,7 +219,7 @@ QQC2.Control {
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
if (!repeatTimer.running && NeoChatConfig.typingNotifications) {
var textExists = text.length > 0;
root.currentRoom.sendTypingNotification(textExists);
textExists ? repeatTimer.start() : repeatTimer.stop();
@@ -353,8 +353,8 @@ QQC2.Control {
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
endPercentWidth: NeoChatConfig.compactLayout ? 100 : 85
maxWidth: NeoChatConfig.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.width
}
@@ -395,7 +395,7 @@ QQC2.Control {
repeatTimer.stop();
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
_private.chatBarCache.clearRelations();
messageSent();
}

View File

@@ -119,8 +119,7 @@ QString ChatBarCache::relationMessage() const
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
EventHandler eventhandler(room, &**event);
return eventhandler.getMarkdownBody();
return EventHandler::markdownBody(&**event);
}
return {};
}
@@ -140,8 +139,8 @@ void ChatBarCache::setThreadId(const QString &threadId)
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
const auto oldThreadId = std::exchange(m_threadId, threadId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
}
QString ChatBarCache::attachmentPath() const
@@ -161,6 +160,16 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
Q_EMIT relationIdChanged(oldEventId, m_relationId);
}
void ChatBarCache::clearRelations()
{
const auto oldEventId = std::exchange(m_relationId, QString());
const auto oldThreadId = std::exchange(m_threadId, QString());
m_attachmentPath = QString();
Q_EMIT relationIdChanged(oldEventId, m_relationId);
Q_EMIT threadIdChanged(oldThreadId, m_threadId);
Q_EMIT attachmentPathChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;

View File

@@ -33,7 +33,7 @@ struct Mention {
* A class to cache data from a chat bar.
*
* A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
* necessarily have to use the ChatBar component, e.g. ChatBarComponent.
*
* This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
@@ -43,7 +43,7 @@ struct Mention {
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, MessageEditComponent, NeoChatRoom
* @sa ChatBar, ChatBarComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
@@ -165,6 +165,13 @@ public:
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Clear all relations in the cache.
*
* This includes relation ID, thread root ID and attachment path.
*/
Q_INVOKABLE void clearRelations();
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
@@ -188,7 +195,7 @@ public:
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
void threadIdChanged();
void threadIdChanged(const QString &oldThreadId, const QString &newThreadId);
void attachmentPathChanged();
private:

View File

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

View File

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

View File

@@ -13,13 +13,11 @@
#include <signal.h>
#include <Quotient/accountregistry.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h"
@@ -64,7 +62,11 @@ Controller::Controller(QObject *parent)
});
} else {
auto c = new NeoChatConnection(this);
#if Quotient_VERSION_MINOR > 8
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("device_1234"), QStringLiteral("token_1234"));
#else
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
#endif
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
@@ -202,15 +204,33 @@ void Controller::invokeLogin()
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
} else {
connect(
connection->allRooms()[0],
&Room::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
}
});
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
});
#if Quotient_VERSION_MINOR > 8
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
#else
connection->assumeIdentity(account.userId(), accessToken);
#endif
});
}
}
@@ -251,23 +271,19 @@ QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const QStrin
return job;
}
bool Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
void Controller::saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken)
{
qDebug() << "Save the access token to the keychain for " << userId;
QKeychain::WritePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(userId);
job.setBinaryData(accessToken);
QEventLoop loop;
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString());
return false;
}
return true;
auto job = new QKeychain::WritePasswordJob(qAppName());
job->setAutoDelete(true);
job->setKey(userId);
job->setBinaryData(accessToken);
connect(job, &QKeychain::WritePasswordJob::finished, this, [job]() {
if (job->error()) {
qWarning() << "Could not save access token to the keychain: " << qPrintable(job->errorString());
}
});
job->start();
}
bool Controller::supportSystemTray() const
@@ -409,11 +425,28 @@ void Controller::removeConnection(const QString &userId)
bool Controller::csSupported() const
{
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 8
return true;
#else
return false;
#endif
}
void Controller::revertToDefaultConfig()
{
const auto config = NeoChatConfig::self();
config->setDefaults();
config->save();
}
bool Controller::isImageShown(const QString &eventId)
{
return m_shownImages.contains(eventId);
}
void Controller::markImageShown(const QString &eventId)
{
m_shownImages.append(eventId);
}
#include "moc_controller.cpp"

View File

@@ -86,7 +86,7 @@ public:
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
void saveAccessTokenToKeyChain(const QString &userId, const QByteArray &accessToken);
[[nodiscard]] bool supportSystemTray() const;
@@ -108,6 +108,17 @@ public:
bool csSupported() const;
/**
* @brief Revert all configuration values to their default.
*
* The parameters along with their defaults are specified in the config file
* neochatconfig.kcfg.
*/
Q_INVOKABLE void revertToDefaultConfig();
Q_INVOKABLE bool isImageShown(const QString &eventId);
Q_INVOKABLE void markImageShown(const QString &eventId);
private:
explicit Controller(QObject *parent = nullptr);
@@ -116,13 +127,11 @@ private:
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const QString &account);
void loadSettings();
void saveSettings() const;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
QMap<QString, QPointer<NeoChatConnection>> m_connectionsLoading;
QString m_endpoint;
QStringList m_shownImages;
private Q_SLOTS:
void invokeLogin();

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,16 +5,17 @@
#include <QMovie>
#include <KFormat>
#include <KLocalizedString>
#include <Quotient/eventitem.h>
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/event.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
@@ -25,9 +26,6 @@
#include "events/locationbeaconevent.h"
#include "events/pollevent.h"
#include "events/widgetevent.h"
#include "linkpreviewer.h"
#include "messagecomponenttype.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
@@ -35,82 +33,52 @@
using namespace Quotient;
EventHandler::EventHandler(const NeoChatRoom *room, const RoomEvent *event)
: m_room(room)
, m_event(event)
QString EventHandler::id(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
qCWarning(EventHandling) << "id called with event set to nullptr.";
return {};
}
return !event->id().isEmpty() ? event->id() : event->transactionId();
}
QString EventHandler::getId() const
QString EventHandler::authorDisplayName(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "authorDisplayName called with room set to nullptr.";
return {};
}
if (event == nullptr) {
qCWarning(EventHandling) << "authorDisplayName called with event set to nullptr.";
return {};
}
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
MessageComponentType::Type EventHandler::messageComponentType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "messageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
}
return MessageComponentType::typeForEvent(*m_event);
}
Quotient::RoomMember EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return {};
}
return isPending ? m_room->localMember() : m_room->member(m_event->senderId());
}
QString EventHandler::getAuthorDisplayName(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& m_event->stateKey() == m_event->senderId()) {
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (is<RoomMemberEvent>(*event) && !event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
&& event->stateKey() == event->senderId()) {
auto previousDisplayName = event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = m_event->senderId();
previousDisplayName = event->senderId();
}
return previousDisplayName;
} else {
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
const auto author = isPending ? room->localMember() : room->member(event->senderId());
return author.htmlSafeDisplayName();
}
}
QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "singleLineAuthorDisplayname called with room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "singleLineAuthorDisplayname called with event set to nullptr.";
return {};
}
const auto author = isPending ? m_room->localMember() : m_room->member(m_event->senderId());
const auto author = isPending ? room->localMember() : room->member(event->senderId());
auto displayName = author.displayName();
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
@@ -121,10 +89,10 @@ QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
return displayName;
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
QDateTime EventHandler::time(const Quotient::RoomEvent *event, bool isPending, QDateTime lastUpdated)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "time called with event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
@@ -132,24 +100,16 @@ QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
return {};
}
return isPending ? lastUpdated : m_event->originTimestamp();
return isPending ? lastUpdated : event->originTimestamp();
}
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
QString EventHandler::timeString(const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
auto ts = getTime(isPending, lastUpdated);
auto ts = time(event, isPending, lastUpdated);
if (ts.isValid()) {
if (relative) {
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
KFormat formatter;
return formatter.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
@@ -157,39 +117,45 @@ QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, b
return {};
}
bool EventHandler::isHighlighted()
QString EventHandler::timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending, const QDateTime &lastUpdated)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
return false;
}
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
return time(event, isPending, lastUpdated).toLocalTime().toString(format);
}
bool EventHandler::isHidden()
bool EventHandler::isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "isHighlighted called with event set to nullptr.";
return false;
}
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return !room->isDirectChat() && room->isEventHighlighted(event);
}
bool EventHandler::isHidden(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (room == nullptr) {
qCWarning(EventHandling) << "isHidden called with room set to nullptr.";
return false;
}
if (event == nullptr) {
qCWarning(EventHandling) << "isHidden called with event set to nullptr.";
return false;
}
if (event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
} else if (roomMemberEvent->isRename() && roomMemberEvent->prevContent() && roomMemberEvent->prevContent()->membership == roomMemberEvent->membership()
&& !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
@@ -197,33 +163,33 @@ bool EventHandler::isHidden()
}
}
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
if (event->isStateEvent() && eventCast<const StateEvent>(event)->repeatsState()) {
return true;
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (auto e = eventCast<const RoomMessageEvent>(event)) {
if (!e->replacedEvent().isEmpty()) {
return true;
}
}
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
if (is<RedactionEvent>(*event) || is<ReactionEvent>(*event)) {
return true;
}
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (auto e = eventCast<const RoomMessageEvent>(event)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return true;
}
}
if (m_room->connection()->isIgnored(m_event->senderId())) {
if (room->connection()->isIgnored(event->senderId())) {
return true;
}
// hide ending live location beacons
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !event->contentJson()["live"_ls].toBool()) {
return true;
}
@@ -262,44 +228,52 @@ QString EventHandler::rawMessageBody(const Quotient::RoomMessageEvent &event)
return body;
}
QString EventHandler::getRichBody(bool stripNewlines) const
QString EventHandler::richBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "richBody called with room set to nullptr.";
return {};
}
return getBody(m_event, Qt::RichText, stripNewlines);
if (event == nullptr) {
qCWarning(EventHandling) << "richBody called with event set to nullptr.";
return {};
}
return getBody(room, event, Qt::RichText, stripNewlines);
}
QString EventHandler::getPlainBody(bool stripNewlines) const
QString EventHandler::plainBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "plainBody called with room set to nullptr.";
return {};
}
return getBody(m_event, Qt::PlainText, stripNewlines);
if (event == nullptr) {
qCWarning(EventHandling) << "plainBody called with event set to nullptr.";
return {};
}
return getBody(room, event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getMarkdownBody() const
QString EventHandler::markdownBody(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMarkdownBody called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "markdownBody called with event set to nullptr.";
return {};
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getMarkdownBody called when m_event isn't a RoomMessageEvent.";
if (!event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "markdownBody called when event isn't a RoomMessageEvent.";
return {};
}
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(m_event);
const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
QString plainBody = roomMessageEvent->plainBody();
plainBody.remove(TextRegex::removeReply);
return plainBody;
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines)
{
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
@@ -310,15 +284,15 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
return switchOnType(
*event,
[this, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(event, format, stripNewlines);
[room, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(room, event, format, stripNewlines);
},
[](const StickerEvent &e) {
return e.body();
},
[this, prettyPrint](const RoomMemberEvent &e) {
[room, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = m_room->member(e.userId()).htmlSafeDisplayName();
auto subjectName = room->member(e.userId()).htmlSafeDisplayName();
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName);
@@ -330,7 +304,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), m_room->member(e.userId()).color().name(), subjectName);
.arg(e.userId(), room->member(e.userId()).color().name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
@@ -393,9 +367,13 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
}
return (e.senderId() != e.userId())
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
: i18n("left the room");
if (e.senderId() == e.userId()) {
return i18n("left the room");
}
if (const auto &reason = e.contentJson()["reason"_ls].toString().toHtmlEscaped(); !reason.isEmpty()) {
return i18n("has put %1 out of the room: %2", subjectName, reason);
}
return i18n("has put %1 out of the room", subjectName);
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
@@ -466,7 +444,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
i18n("Unknown event"));
}
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
QString EventHandler::getMessageBody(const NeoChatRoom *room, const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines)
{
TextHandler textHandler;
@@ -498,24 +476,24 @@ QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextForm
}
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines, event.isReplaced());
return textHandler.handleRecieveRichText(inputFormat, room, &event, stripNewlines, event.isReplaced());
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
}
QString EventHandler::getGenericBody() const
QString EventHandler::genericBody(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "genericBody called with event set to nullptr.";
return {};
}
if (m_event->isRedacted()) {
if (event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return switchOnType(
*m_event,
*event,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
return i18n("sent a message");
@@ -635,29 +613,33 @@ QString EventHandler::getGenericBody() const
i18n("Unknown event"));
}
QString EventHandler::subtitleText() const
QString EventHandler::subtitleText(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "subtitleText called with m_event set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "subtitleText called with room set to nullptr.";
return {};
}
return singleLineAuthorDisplayname() + (m_event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + getPlainBody(true);
if (event == nullptr) {
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
return {};
}
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + plainBody(room, event, true);
}
QVariantMap EventHandler::getMediaInfo() const
QVariantMap EventHandler::mediaInfo(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "mediaInfo called with room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "mediaInfo called with event set to nullptr.";
return {};
}
return getMediaInfoForEvent(m_event);
return getMediaInfoForEvent(room, event);
}
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
QVariantMap EventHandler::getMediaInfoForEvent(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
QString eventId = event->id();
@@ -670,7 +652,7 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
const EventContent::FileInfo *fileInfo;
fileInfo = roomMessageEvent->content()->fileInfo();
QVariantMap mediaInfo = getMediaInfoFromFileInfo(fileInfo, eventId, false, false);
QVariantMap mediaInfo = getMediaInfoFromFileInfo(room, 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;
@@ -682,13 +664,17 @@ QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event)
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
return getMediaInfoFromFileInfo(fileInfo, eventId, false, true);
return getMediaInfoFromFileInfo(room, fileInfo, eventId, false, true);
} else {
return {};
}
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail, bool isSticker) const
QVariantMap EventHandler::getMediaInfoFromFileInfo(const NeoChatRoom *room,
const EventContent::FileInfo *fileInfo,
const QString &eventId,
bool isThumbnail,
bool isSticker)
{
QVariantMap mediaInfo;
@@ -696,7 +682,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
QUrl source = room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid()) {
mediaInfo["source"_ls] = source;
@@ -728,7 +714,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
@@ -751,7 +737,7 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
auto thumbnailInfo = getMediaInfoFromFileInfo(room, castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
@@ -775,156 +761,89 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
return mediaInfo;
}
bool EventHandler::hasReply() const
bool EventHandler::hasReply(const Quotient::RoomEvent *event, bool showFallbacks)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "hasReply called with event set to nullptr.";
return false;
}
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
const auto relations = event->contentPart<QJsonObject>("m.relates_to"_ls);
if (!relations.isEmpty()) {
const bool hasReplyRelation = relations.contains("m.in_reply_to"_ls);
bool isFallingBack = relations["is_falling_back"_ls].toBool();
return hasReplyRelation && (showFallbacks ? true : !isFallingBack);
}
return false;
}
QString EventHandler::getReplyId() const
QString EventHandler::replyId(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "replyId called with event set to nullptr.";
return {};
}
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
return event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
MessageComponentType::Type EventHandler::replyMessageComponentType() const
Quotient::RoomMember EventHandler::replyAuthor(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_room set to nullptr.";
return MessageComponentType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "replyMessageComponentType called with m_event set to nullptr.";
return MessageComponentType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return MessageComponentType::Other;
}
return MessageComponentType::typeForEvent(*replyEvent);
}
Quotient::RoomMember EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
if (room == nullptr) {
qCWarning(EventHandling) << "replyAuthor called with room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
if (event == nullptr) {
qCWarning(EventHandling) << "replyAuthor called with event set to nullptr. Returning empty user.";
return {};
}
if (auto replyPtr = m_room->getReplyForEvent(*m_event)) {
return m_room->member(replyPtr->senderId());
if (auto replyPtr = room->getReplyForEvent(*event)) {
return room->member(replyPtr->senderId());
} else {
return m_room->member(QString());
return room->member(QString());
}
}
QString EventHandler::getReplyRichBody(bool stripNewlines) const
bool EventHandler::isThreaded(const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::RichText, stripNewlines);
}
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
return {};
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return {};
}
return getBody(replyEvent, Qt::PlainText, stripNewlines);
}
QVariantMap EventHandler::getReplyMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
return {};
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(replyPtr);
}
bool EventHandler::isThreaded() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with event set to nullptr.";
return false;
}
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
return (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot() const
QString EventHandler::threadRoot(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists.
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
if (event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return getId();
if (!event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return id(event);
}
return {};
}
float EventHandler::getLatitude() const
float EventHandler::latitude(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "latitude called with event set to nullptr.";
return -100.0;
}
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
const auto geoUri = event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
}
@@ -932,14 +851,14 @@ float EventHandler::getLatitude() const
return latitude.toFloat();
}
float EventHandler::getLongitude() const
float EventHandler::longitude(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "longitude called with event set to nullptr.";
return -200.0;
}
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
const auto geoUri = event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
}
@@ -947,14 +866,14 @@ float EventHandler::getLongitude() const
return latitude.toFloat();
}
QString EventHandler::getLocationAssetType() const
QString EventHandler::locationAssetType(const Quotient::RoomEvent *event)
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
if (event == nullptr) {
qCWarning(EventHandling) << "locationAssetType called with event set to nullptr.";
return {};
}
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
const auto assetType = event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,9 +78,7 @@ void LoginHelper::init()
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
Controller::instance().saveAccessTokenToKeyChain(account.userId(), m_connection->accessToken());
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);

View File

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

View File

@@ -148,7 +148,7 @@ int main(int argc, char *argv[])
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QStringLiteral("https://carlschwan.eu/avatar.png"));
QUrl(QStringLiteral("https://carlschwan.eu/avatar.png")));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
@@ -185,9 +185,6 @@ int main(int argc, char *argv[])
#endif
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
@@ -307,7 +304,6 @@ int main(int argc, char *argv[])
QWindow *window = windowFromEngine(&engine);
WindowController::instance().setWindow(window);
WindowController::instance().restoreGeometry();
return app.exec();
}

View File

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

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QQuickAsyncImageProvider>
#include <Quotient/jobs/mediathumbnailjob.h>
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
* A QQuickImageResponse for an mxc image.
*
* @sa QQuickImageResponse
*/
class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
void startRequest();
void prepareResult();
private:
QString mediaId;
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
QPointer<NeoChatConnection> m_connection;
QImage image;
QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory *textureFactory() const override;
QString errorString() const override;
};
/**
* @class MatrixImageProvider
*
* A QQuickAsyncImageProvider for mxc images.
*
* @sa QQuickAsyncImageProvider
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider *instance = new MatrixImageProvider;
engine->setObjectOwnership(instance, QQmlEngine::CppOwnership);
return instance;
}
/**
* @brief Return a job to provide the image with the given ID.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
QPointer<NeoChatConnection> m_connection;
MatrixImageProvider() = default;
};

View File

@@ -211,7 +211,7 @@ QList<ActionsModel::Action> actions{
Q_EMIT Controller::instance().showMessage(Controller::Positive, i18n("You are already in this room."));
return QString();
}
if (room->members().contains(room->member(text))) {
if (room->joinedMemberIds().contains(text)) {
Q_EMIT Controller::instance().showMessage(Controller::Info, i18nc("<user> is already in this room.", "%1 is already in this room.", text));
return QString();
}

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagecontentmodel.h"
#include "eventhandler.h"
#include "neochatconfig.h"
#include <QImageReader>
@@ -9,9 +10,10 @@
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/qt_connection_util.h>
#include <KLocalizedString>
#include <Quotient/qt_connection_util.h>
#include <Kirigami/Platform/PlatformTheme>
#ifndef Q_OS_ANDROID
#include <KSyntaxHighlighting/Definition>
@@ -19,10 +21,7 @@
#endif
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "filetype.h"
#include "itinerarymodel.h"
#include "linkpreviewer.h"
#include "neochatconnection.h"
#include "neochatroom.h"
@@ -30,20 +29,20 @@
using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(event != nullptr ? event->id() : QString())
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
, m_event(loadEvent<RoomEvent>(event->fullJson()))
, m_isPending(isPending)
, m_isReply(isReply)
{
intiializeEvent(event);
initializeModel();
}
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending)
: QAbstractListModel(nullptr)
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(eventId)
, m_isPending(isPending)
@@ -57,15 +56,27 @@ void MessageContentModel::initializeModel()
Q_ASSERT(m_room != nullptr);
// Allow making a model for an event that is being downloaded but will appear later
// e.g. a reply, but we need an ID to know when it has arrived.
Q_ASSERT(!m_eventId.isEmpty());
// Also note that a pending event may not have an event ID yet but as long as we have an event
// pointer we can pass out the transaction ID until it is set.
Q_ASSERT(!m_eventId.isEmpty() || m_event != nullptr);
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_event = loadEvent<RoomEvent>(m_room->getEvent(eventId)->fullJson());
Q_EMIT eventUpdated();
m_notFound = false;
intiializeEvent(m_room->getEvent(eventId));
updateReplyModel();
resetContent();
resetModel();
return true;
}
}
return false;
});
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = true;
resetModel();
return true;
}
}
@@ -73,16 +84,18 @@ void MessageContentModel::initializeModel()
});
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
intiializeEvent(m_room->getEvent(m_eventId));
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
}
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == serverEvent->id()) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
beginResetModel();
m_isPending = false;
m_event = loadEvent<RoomEvent>(serverEvent->fullJson());
Q_EMIT eventUpdated();
intiializeEvent(serverEvent);
endResetModel();
}
}
@@ -91,8 +104,8 @@ void MessageContentModel::initializeModel()
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == newEvent->id()) {
beginResetModel();
m_event = loadEvent<RoomEvent>(newEvent->fullJson());
Q_EMIT eventUpdated();
intiializeEvent(newEvent);
resetContent();
endResetModel();
}
}
@@ -127,6 +140,13 @@ void MessageContentModel::initializeModel()
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (m_event != nullptr && (oldThreadId == m_eventId || newThreadId == m_eventId)) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
@@ -148,12 +168,47 @@ void MessageContentModel::initializeModel()
}
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
updateReplyModel();
resetModel();
});
if (m_event != nullptr) {
updateReplyModel();
}
resetModel();
}
void MessageContentModel::intiializeEvent(const QString &eventId)
{
const auto newEvent = m_room->getEvent(eventId);
if (newEvent != nullptr) {
intiializeEvent(newEvent);
}
}
void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
return;
}
m_event = loadEvent<RoomEvent>(event->fullJson());
// a pending event may not previously have had an event ID so update.
m_eventId = EventHandler::id(m_event.get());
auto senderId = m_event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
if (m_eventSenderObject == nullptr) {
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
}
bool MessageContentModel::showAuthor() const
{
return m_showAuthor;
@@ -166,7 +221,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
}
m_showAuthor = showAuthor;
if (m_event != nullptr) {
if (m_event != nullptr && m_room->connection()->isIgnored(m_event->senderId())) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -193,10 +248,23 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return {};
}
EventHandler eventHandler(m_room, m_event.get());
const auto component = m_components[index.row()];
if (role == DisplayRole) {
if (m_notFound || (m_event && m_room->connection()->isIgnored(m_event->senderId()))) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
QString disabledTextColor;
if (theme != nullptr) {
disabledTextColor = theme->disabledTextColor().name();
} else {
disabledTextColor = QStringLiteral("#000000");
}
return QString(QStringLiteral("<span style=\"color:%1\">").arg(disabledTextColor)
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
+ QStringLiteral("</span>"));
}
if (component.type == MessageComponentType::Loading && m_isReply) {
return i18n("Loading reply");
}
@@ -211,7 +279,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (!component.content.isEmpty()) {
return component.content;
}
return eventHandler.getRichBody();
return EventHandler::richBody(m_room, m_event.get());
}
if (role == ComponentTypeRole) {
return component.type;
@@ -220,7 +288,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes;
}
if (role == EventIdRole) {
return eventHandler.getId();
return EventHandler::id(m_event.get());
}
if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
@@ -228,7 +296,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTime(m_isPending, lastUpdated);
return EventHandler::time(m_event.get(), m_isPending, lastUpdated);
}
if (role == TimeStringRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) {
@@ -236,13 +304,13 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
});
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return eventHandler.getTimeString(false, QLocale::ShortFormat, m_isPending, lastUpdated);
return EventHandler::timeString(m_event.get(), QStringLiteral("hh:mm"), m_isPending, lastUpdated);
}
if (role == AuthorRole) {
return QVariant::fromValue(eventHandler.getAuthor(m_isPending));
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
}
if (role == MediaInfoRole) {
return eventHandler.getMediaInfo();
return EventHandler::mediaInfo(m_room, m_event.get());
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get()));
@@ -251,25 +319,22 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return eventHandler.getLatitude();
return EventHandler::latitude(m_event.get());
}
if (role == LongitudeRole) {
return eventHandler.getLongitude();
return EventHandler::longitude(m_event.get());
}
if (role == AssetRole) {
return eventHandler.getLocationAssetType();
return EventHandler::locationAssetType(m_event.get());
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == IsReplyRole) {
return eventHandler.hasReply();
}
if (role == ReplyEventIdRole) {
return eventHandler.getReplyId();
return EventHandler::replyId(m_event.get());
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(eventHandler.getReplyAuthor());
return QVariant::fromValue(EventHandler::replyAuthor(m_room, m_event.get()));
}
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
@@ -282,6 +347,12 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == ChatBarCacheRole) {
if (m_room->threadCache()->threadId() == m_eventId) {
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
}
return {};
}
@@ -309,11 +380,11 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[IsReplyRole] = "isReply";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -322,6 +393,12 @@ void MessageContentModel::resetModel()
beginResetModel();
m_components.clear();
if ((m_event && m_room->connection()->isIgnored(m_event->senderId())) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
}
if (m_event == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel();
@@ -336,7 +413,7 @@ void MessageContentModel::resetModel()
endResetModel();
}
void MessageContentModel::resetContent(bool isEditing)
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
{
Q_ASSERT(m_event != nullptr);
@@ -345,7 +422,7 @@ void MessageContentModel::resetContent(bool isEditing)
m_components.remove(startRow, rowCount() - startRow);
endRemoveRows();
const auto newComponents = messageContentComponents(isEditing);
const auto newComponents = messageContentComponents(isEditing, isThreading);
if (newComponents.size() == 0) {
return;
}
@@ -354,7 +431,7 @@ void MessageContentModel::resetContent(bool isEditing)
endInsertRows();
}
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{
QList<MessageComponent> newComponents;
@@ -374,35 +451,45 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
}
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::Edit, QString(), {}};
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
EventHandler eventHandler(m_room, m_event.get());
newComponents.append(componentsForType(eventHandler.messageComponentType()));
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get())));
}
if (m_room->urlPreviewEnabled()) {
newComponents = addLinkPreviews(newComponents);
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && !EventHandler::isThreaded(m_event.get())) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
return newComponents;
}
void MessageContentModel::updateReplyModel()
{
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
if (m_event == nullptr || m_isReply) {
return;
}
EventHandler eventHandler(m_room, m_event.get());
if (!eventHandler.hasReply()) {
if (!EventHandler::hasReply(m_event.get()) || (EventHandler::isThreaded(m_event.get()) && NeoChatConfig::self()->threads())) {
if (m_replyModel) {
delete m_replyModel;
}
return;
}
const auto replyEvent = m_room->findInTimeline(eventHandler.getReplyId());
if (m_replyModel != nullptr) {
return;
}
const auto replyEvent = m_room->findInTimeline(EventHandler::replyId(m_event.get()));
if (replyEvent == m_room->historyEdge()) {
m_replyModel = new MessageContentModel(m_room, eventHandler.getReplyId(), true);
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(m_event.get()), true, false, this);
} else {
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true);
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true, false, this);
}
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {

View File

@@ -6,11 +6,12 @@
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include <Quotient/room.h>
#include "enums/messagecomponenttype.h"
#include "eventhandler.h"
#include "itinerarymodel.h"
#include "neochatroommember.h"
struct MessageComponent {
MessageComponentType::Type type = MessageComponentType::Other;
@@ -64,17 +65,21 @@ public:
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
ReplyAuthorRole, /**< The author of the event that was replied to. */
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
LinkPreviewerRole, /**< The link preview details. */
ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false, bool isPending = false);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false);
explicit MessageContentModel(NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool isReply = false,
bool isPending = false,
MessageContentModel *parent = nullptr);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false, MessageContentModel *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
@@ -115,18 +120,22 @@ private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QString m_eventSenderId;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
Quotient::RoomEventPtr m_event;
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
bool m_notFound = false;
void initializeModel();
void intiializeEvent(const QString &eventId);
void intiializeEvent(const Quotient::RoomEvent *event);
QList<MessageComponent> m_components;
void resetModel();
void resetContent(bool isEditing = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false);
void resetContent(bool isEditing = false, bool isThreading = false);
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
QPointer<MessageContentModel> m_replyModel;
void updateReplyModel();

View File

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

View File

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

View File

@@ -4,12 +4,12 @@
#include "messagefiltermodel.h"
#include <KLocalizedString>
#include <QVariant>
#include "enums/delegatetype.h"
#include "messagecontentmodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "timelinemodel.h"
#include "neochatroommember.h"
using namespace Quotient;

View File

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

View File

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

View File

@@ -11,7 +11,6 @@
#include <Quotient/jobs/basejob.h>
#include "neochatconfig.h"
#include "neochatconnection.h"
#include <KLazyLocalizedString>

View File

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

View File

@@ -245,8 +245,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
return EventHandler::subtitleText(room, room->lastEvent());
}
if (role == AvatarImageRole) {
return room->avatar(128);

View File

@@ -357,8 +357,7 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString();
}
EventHandler eventHandler(room, room->lastEvent());
return eventHandler.subtitleText();
return EventHandler::subtitleText(room, room->lastEvent());
}
if (role == AvatarImageRole) {
return room->avatar(128);

View File

@@ -66,7 +66,17 @@ void SearchModel::search()
m_job = job;
connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel();
m_memberObjects.clear();
m_result = job->searchCategories().roomEvents;
if (m_result.has_value()) {
for (const auto &result : m_result.value().results) {
if (!m_memberObjects.contains(result.result->senderId())) {
m_memberObjects[result.result->senderId()] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, result.result->senderId()));
}
}
}
endResetModel();
setSearching(false);
m_job = nullptr;
@@ -79,18 +89,16 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
auto row = index.row();
const auto &event = *m_result->results[row].result;
EventHandler eventHandler(m_room, &event);
switch (role) {
case AuthorRole:
return QVariant::fromValue(eventHandler.getAuthor());
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(event.senderId()).get());
case ShowSectionRole:
if (row == 0) {
return true;
}
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
case SectionRole:
return eventHandler.getTimeString(true);
return EventHandler::timeString(&event, true);
case ShowReactionsRole:
return false;
case ShowReadMarkersRole:
@@ -98,13 +106,13 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
case IsPendingRole:
return false;
case HighlightRole:
return eventHandler.isHighlighted();
return EventHandler::isHighlighted(m_room, &event);
case EventIdRole:
return eventHandler.getId();
return EventHandler::id(&event);
case IsThreadedRole:
return eventHandler.isThreaded();
return EventHandler::isThreaded(&event);
case ThreadRootRole:
return eventHandler.threadRoot();
return EventHandler::threadRoot(&event);
case ContentModelRole: {
if (!event.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event));

View File

@@ -9,6 +9,8 @@
#include <Quotient/csapi/search.h>
#include "neochatroommember.h"
namespace Quotient
{
class Connection;
@@ -123,4 +125,6 @@ private:
std::optional<Quotient::SearchJob::ResultRoomEvents> m_result = std::nullopt;
Quotient::SearchJob *m_job = nullptr;
bool m_searching = false;
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
};

View File

@@ -7,7 +7,6 @@
#include <Quotient/room.h>
#include "neochatconnection.h"
#include "neochatroom.h"
SpaceChildrenModel::SpaceChildrenModel(QObject *parent)
: QAbstractItemModel(parent)

190
src/models/threadmodel.cpp Normal file
View File

@@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "threadmodel.h"
#include <Quotient/events/event.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
#include <memory>
#include "chatbarcache.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
: QConcatenateTablesProxyModel(room)
, m_threadRootId(threadRootId)
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
{
Q_ASSERT(!m_threadRootId.isEmpty());
Q_ASSERT(room);
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
connect(room, &Quotient::Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(event);
addModels();
}
}
});
connect(room, &Quotient::Room::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
for (const auto &event : events) {
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (EventHandler::isThreaded(roomEvent) && EventHandler::threadRoot(roomEvent) == m_threadRootId) {
addNewEvent(roomEvent);
}
}
}
addModels();
});
fetchMore({});
addModels();
}
QString ThreadModel::threadRootId() const
{
return m_threadRootId;
}
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return m_threadRootContentModel.get();
}
QHash<int, QByteArray> ThreadModel::roleNames() const
{
return m_threadRootContentModel->roleNames();
}
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return !m_currentJob && m_nextBatch.has_value();
}
void ThreadModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent);
if (!m_currentJob && m_nextBatch.has_value()) {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
const auto connection = room->connection();
m_currentJob =
connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, QLatin1String("m.thread"), *m_nextBatch, QString(), 5);
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) {
m_contentModels.push_back(new MessageContentModel(room, event.get()));
}
addModels();
const auto newNextBatch = m_currentJob->nextBatch();
if (!newNextBatch.isEmpty() && *m_nextBatch != newNextBatch) {
*m_nextBatch = newNextBatch;
} else {
// Insert the thread root at the end.
beginInsertRows({}, rowCount(), rowCount());
endInsertRows();
m_nextBatch.reset();
}
m_currentJob.clear();
});
}
}
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
{
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
m_contentModels.push_front(new MessageContentModel(room, event));
}
void ThreadModel::addModels()
{
if (!sourceModels().isEmpty()) {
clearModels();
}
addSourceModel(m_threadRootContentModel.get());
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
addSourceModel(*it);
}
addSourceModel(m_threadChatBarModel);
beginResetModel();
endResetModel();
}
void ThreadModel::clearModels()
{
removeSourceModel(m_threadRootContentModel.get());
for (const auto &model : m_contentModels) {
if (sourceModels().contains(model)) {
removeSourceModel(model);
}
}
removeSourceModel(m_threadChatBarModel);
}
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
: QAbstractListModel(parent)
, m_room(room)
{
if (m_room != nullptr) {
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr && (oldThreadId == threadModel->threadRootId() || newThreadId == threadModel->threadRootId())) {
beginResetModel();
endResetModel();
}
});
}
}
QVariant ThreadChatBarModel::data(const QModelIndex &idx, int role) const
{
if (idx.row() > 1) {
return {};
}
if (role == ComponentTypeRole) {
return MessageComponentType::ChatBar;
}
if (role == ChatBarCacheRole) {
if (m_room == nullptr) {
return {};
}
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
}
return {};
}
int ThreadChatBarModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel != nullptr) {
return m_room->threadCache()->threadId() == threadModel->threadRootId() ? 1 : 0;
}
return 0;
}
QHash<int, QByteArray> ThreadChatBarModel::roleNames() const
{
return {
{ComponentTypeRole, "componentType"},
{ChatBarCacheRole, "chatBarCache"},
};
}
#include "moc_threadmodel.cpp"

141
src/models/threadmodel.h Normal file
View File

@@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <Quotient/util.h>
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include <QPointer>
#include <Quotient/csapi/relations.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include <deque>
#include <optional>
#include "linkpreviewer.h"
#include "messagecontentmodel.h"
class NeoChatRoom;
class ReactionModel;
/**
* @class ThreadChatBarModel
*
* A model to provide a chat bar component to send new messages in a thread.
*/
class ThreadChatBarModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*
* The role values need to match MessageContentModel not to blow up.
*
* @sa MessageContentModel
*/
enum Roles {
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
ChatBarCacheRole = MessageContentModel::ChatBarCacheRole, /**< The ChatBarCache to use. */
};
Q_ENUM(Roles)
explicit ThreadChatBarModel(QObject *parent, NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1 or 0, depending on whether a chat bar should be shown.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with ComponentTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QPointer<NeoChatRoom> m_room;
};
/**
* @class ThreadModel
*
* This class defines the model for visualising a thread.
*
* The class also provides functions to access the data of the root event, typically
* used to visualise the thread in a list of room threads.
*/
class ThreadModel : public QConcatenateTablesProxyModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room);
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
MessageContentModel *threadRootContentModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Whether there is more data available for the model to fetch.
*
* @sa QAbstractItemModel::canFetchMore()
*/
bool canFetchMore(const QModelIndex &parent) const override;
/**
* @brief Fetches the next batch of model data if any is available.
*
* @sa QAbstractItemModel::fetchMore()
*/
void fetchMore(const QModelIndex &parent) override;
private:
QString m_threadRootId;
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<MessageContentModel *> m_contentModels;
ThreadChatBarModel *m_threadChatBarModel;
QList<QString> m_events;
QList<QString> m_pendingEvents;
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;
std::optional<QString> m_nextBatch = QString();
bool m_addingPending = false;
void addNewEvent(const Quotient::RoomEvent *event);
void addModels();
void clearModels();
};

View File

@@ -3,11 +3,15 @@
#include "timelinemodel.h"
#include <Quotient/qt_connection_util.h>
#include "delegatetype.h"
TimelineModel::TimelineModel(QObject *parent)
: QConcatenateTablesProxyModel(parent)
{
m_timelineBeginningModel = new TimelineBeginningModel(this);
addSourceModel(m_timelineBeginningModel);
m_messageEventModel = new MessageEventModel(this);
addSourceModel(m_messageEventModel);
m_timelineEndModel = new TimelineEndModel(this);
@@ -23,6 +27,7 @@ void TimelineModel::setRoom(NeoChatRoom *room)
{
// Both models do their own null checking so just pass along.
m_messageEventModel->setRoom(room);
m_timelineBeginningModel->setRoom(room);
m_timelineEndModel->setRoom(room);
}
@@ -36,6 +41,69 @@ QHash<int, QByteArray> TimelineModel::roleNames() const
return m_messageEventModel->roleNames();
}
TimelineBeginningModel::TimelineBeginningModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void TimelineBeginningModel::setRoom(NeoChatRoom *room)
{
if (room == m_room) {
return;
}
beginResetModel();
if (m_room != nullptr) {
m_room->disconnect(this);
}
m_room = room;
if (m_room != nullptr) {
Quotient::connectUntil(m_room.get(), &Quotient::Room::eventsHistoryJobChanged, this, [this]() {
if (m_room && m_room->allHistoryLoaded()) {
// HACK: We have to do it this way because DelegateChooser doesn't update dynamically.
beginRemoveRows({}, 0, 0);
endRemoveRows();
beginInsertRows({}, 0, 0);
endInsertRows();
return true;
}
return false;
});
}
endResetModel();
}
QVariant TimelineBeginningModel::data(const QModelIndex &idx, int role) const
{
Q_UNUSED(idx)
if (m_room == nullptr) {
return {};
}
if (role == DelegateTypeRole) {
return DelegateType::Successor;
}
return {};
}
int TimelineBeginningModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (m_room == nullptr) {
return 0;
}
return m_room->successorId().isEmpty() ? 0 : 1;
}
QHash<int, QByteArray> TimelineBeginningModel::roleNames() const
{
return {{DelegateTypeRole, "delegateType"}};
}
TimelineEndModel::TimelineEndModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -78,7 +146,11 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
}
if (role == DelegateTypeRole) {
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
if (idx.row() == 1 || rowCount() == 1) {
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
} else {
return DelegateType::Predecessor;
}
}
return {};
}
@@ -86,7 +158,10 @@ QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
int TimelineEndModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
if (m_room == nullptr) {
return 0;
}
return m_room->predecessorId().isEmpty() ? 1 : (m_room->allHistoryLoaded() ? 2 : 1);
}
QHash<int, QByteArray> TimelineEndModel::roleNames() const

View File

@@ -10,6 +10,57 @@
#include "messageeventmodel.h"
#include "neochatroom.h"
/**
* @class TimelineBeginningModel
*
* A model to provide a delegate at the start of the timeline to show upgrades.
*/
class TimelineBeginningModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */
};
Q_ENUM(Roles)
explicit TimelineBeginningModel(QObject *parent = nullptr);
/**
* @brief Set the room for the timeline.
*/
void setRoom(NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1, the answer is always 1.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with DelegateTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
QPointer<NeoChatRoom> m_room = nullptr;
};
/**
* @class TimelineEndModel
*
@@ -108,5 +159,6 @@ Q_SIGNALS:
private:
MessageEventModel *m_messageEventModel = nullptr;
TimelineBeginningModel *m_timelineBeginningModel = nullptr;
TimelineEndModel *m_timelineEndModel = nullptr;
};

View File

@@ -7,7 +7,6 @@
#include <Quotient/avatar.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/room.h>
#include "enums/powerlevel.h"
#include "neochatroom.h"

View File

@@ -125,7 +125,7 @@ void WebShortcutModel::trigger(const QString &data)
void WebShortcutModel::configureWebShortcuts()
{
#ifdef HAVE_KIO
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"), this);
auto job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), QStringList() << QStringLiteral("webshortcuts"), this);
job->exec();
#endif
}

View File

@@ -16,6 +16,7 @@ Name[eu]=NeoChat
Name[fi]=NeoChat
Name[fr]=NeoChat
Name[gl]=NeoChat
Name[he]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
@@ -58,6 +59,7 @@ Comment[eu]=Matrix, deszentralizatutako komunikazio protokolorako, bezero bat
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[gl]=Un cliente para Matrix, o protocolo de comunicación descentralizada.
Comment[he]=לקוח ל־matrix, פרוטוקול התקשורת המבוזר
Comment[hu]=Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz
Comment[ia]=Un cliente per Matrix, le protocollo de communication decentralisate
Comment[id]=Sebuah klien untuk matrix, protokol komunikasi terdecentralisasi
@@ -102,6 +104,7 @@ Name[eu]=Mezu berria
Name[fi]=Uusi viesti
Name[fr]=Nouveau message
Name[gl]=Nova mensaxe
Name[he]=הודעה חדשה
Name[hu]=Új üzenet
Name[ia]=Nove message
Name[id]=Pesan baru
@@ -142,6 +145,7 @@ Comment[eu]=Mezu berri bat dago
Comment[fi]=Saapui uusi viesti
Comment[fr]=Il y a un nouveau message
Comment[gl]=Hai unha nova mensaxe.
Comment[he]=יש הודעה חדשה
Comment[hu]=Új üzenet érkezett
Comment[ia]=Il ha un nove message
Comment[id]=Ada pesan baru
@@ -186,6 +190,7 @@ Name[eu]=Gonbidapen berria
Name[fi]=Uusi kutsu
Name[fr]=Nouvelle invitation
Name[gl]=Novo convite
Name[he]=הזמנה חדשה
Name[hu]=Új meghívó
Name[ia]=Nove invitation
Name[id]=Undangan Baru
@@ -225,6 +230,7 @@ Comment[eu]=Gela baterako gonbidapen berri bat dago
Comment[fi]=Uusi kutsu huoneeseen
Comment[fr]=Il y a une nouvelle invitation dans un salon.
Comment[gl]=Tes un novo convite para unha sala.
Comment[he]=יש הזמנה חדשה לחדר
Comment[hu]=Új meghívó érkezett egy szobába
Comment[ia]=Il ha un nove invitation a un sala
Comment[id]=Ada undangan baru ke sebuah ruangan
@@ -263,6 +269,8 @@ Name[es]=Compartir
Name[eu]=Partekatu
Name[fi]=Jaa
Name[fr]=Partager
Name[gl]=Compartir
Name[he]=שיתוף
Name[hu]=Megosztás
Name[ia]=Comparti
Name[it]=Condivisione
@@ -271,6 +279,7 @@ Name[lv]=Kopīgot
Name[nl]=Gedeelde
Name[nn]=Del
Name[pl]=Udostępnij
Name[pt_BR]=Compartilhar
Name[sl]=Deli
Name[sv]=Dela
Name[ta]=பகிர்
@@ -288,6 +297,8 @@ Comment[es]=El resultado de compartir una parte de contenido
Comment[eu]=Eduki pieza bat partekatzearen emaitza
Comment[fi]=Tulos yhden sisältöosasen jakamisesta
Comment[fr]=Le résultat du partage d'une partie de contenu.
Comment[gl]=O resultado de compartir un fragmento de contido.
Comment[he]=תוצאת שיתוף פיסת תוכן
Comment[hu]=Tartalom megosztásának eredménye
Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto
@@ -296,6 +307,7 @@ 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[pt_BR]=O resultado de compartilhar um conteúdo
Comment[sl]=Rezultat deljenega kosa vsebine
Comment[sv]=Resultatet av att dela innehåll
Comment[ta]=எதையோ பகிர்ந்த‍தன் விளைவு

View File

@@ -15,9 +15,6 @@
<entry name="OpenRoom" type="String">
<label>Latest opened room</label>
</entry>
<entry name="ColorScheme" type="String">
<label>Color scheme</label>
</entry>
<entry name="Blur" type="bool">
<label>Make NeoChat blurry</label>
<default>false</default>
@@ -89,6 +86,10 @@
<label>Show deleted messages in the timeline</label>
<default>false</default>
</entry>
<entry name="HideImages" type="bool">
<label>Hide images in the timeline</label>
<default>false</default>
</entry>
<entry name="ShowLinkPreview" type="bool">
<label>Show preview of the links in the chat messages</label>
</entry>
@@ -187,5 +188,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

@@ -1,9 +0,0 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: LGPL-2.1-or-later
File=neochatconfig.kcfg
ClassName=NeoChatConfig
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Singleton=true

View File

@@ -9,14 +9,12 @@
#include "controller.h"
#include "jobs/neochatchangepasswordjob.h"
#include "jobs/neochatdeactivateaccountjob.h"
#include "linkpreviewer.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
@@ -25,6 +23,7 @@
#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>
@@ -45,6 +44,7 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -52,6 +52,7 @@ NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
: Connection(server, parent)
, m_threePIdModel(new ThreePIdModel(this))
{
m_linkPreviewers.setMaxCost(20);
connectSignals();
}
@@ -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);
@@ -543,14 +564,31 @@ LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
return nullptr;
}
auto previewer = m_linkPreviewers.value(link, nullptr);
auto previewer = m_linkPreviewers.object(link);
if (previewer != nullptr) {
return previewer;
}
previewer = new LinkPreviewer(link, this);
m_linkPreviewers[link] = previewer;
m_linkPreviewers.insert(link, previewer);
return previewer;
}
#if Quotient_VERSION_MINOR > 8
KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path)
{
KeyImport keyImport;
auto result = keyImport.exportKeys(passphrase, this);
if (!result.has_value()) {
return result.error();
}
QUrl url(path);
QFile file(url.toLocalFile());
file.open(QFile::WriteOnly);
file.write(result.value());
file.close();
return KeyImport::Success;
}
#endif
#include "moc_neochatconnection.cpp"

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