Compare commits

..

191 Commits

Author SHA1 Message Date
Carl Schwan
7309cf7249 Add dbus addons on Windows
BUG: 474678
2023-10-05 09:13:34 +00:00
Carl Schwan
2eb26ffbb3 Fix typo 2023-10-05 11:13:09 +02:00
Carl Schwan
87ef55215f Allow reporting others
Instead of only being able to report yourself

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

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

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

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

This replaces the tray icon with a new one redrawn to adhere to icon guidelines and proportions closer
to the colour icon.
2023-09-27 23:43:55 -04:00
l10n daemon script
58a73c0208 GIT_SILENT Sync po/docbooks with svn 2023-09-28 02:11:52 +00:00
Joshua Goins
852110debd Make it clear that the session is broken when the keys are lost
If you use your private keys (like when deleting the quotient database)
your session is broken as you have differing keys on the server. While
it is possible to work your way out of it, it's better to warn users to
bite the bullet and log in again.
2023-09-27 15:13:06 -04:00
Joshua Goins
6b71d3c78d Make the key verification message horizontally centered 2023-09-27 15:13:00 -04:00
Christophe Marin
f3a0adee39 Fix manpage installation 2023-09-27 16:23:29 +02:00
l10n daemon script
6e7b6c9ce0 GIT_SILENT Sync po/docbooks with svn 2023-09-27 02:12:21 +00:00
James Graham
f67cd7deb5 Remove the now unused author ID role from MessageEventModel
Remove the now unused author ID role from `MessageEventModel`. This can be obtained from the author roles object.
2023-09-26 20:21:08 +00:00
l10n daemon script
931b4b1f9a GIT_SILENT Sync po/docbooks with svn 2023-09-26 02:22:24 +00:00
l10n daemon script
167ed4eca3 GIT_SILENT made messages (after extraction) 2023-09-26 01:46:11 +00:00
l10n daemon script
7d5b2c1b6a GIT_SILENT Sync po/docbooks with svn 2023-09-25 02:15:50 +00:00
l10n daemon script
be7b1e49b4 GIT_SILENT Sync po/docbooks with svn 2023-09-24 02:09:06 +00:00
Tobias Fella
957419070a Remove unused includes 2023-09-23 22:43:48 +02:00
Carl Schwan
f22107c8ab Colorful emoji in reaction
Use ICU to determine if the string contains only emojis
2023-09-23 22:16:11 +02:00
Tobias Fella
3a4f71de7f Port to declarative type registration 2023-09-23 14:05:50 +00:00
Tobias Fella
4ed4f3f628 Silence invalid user id warnings 2023-09-23 11:42:21 +02:00
Tobias Fella
ba24f1272f Fix crash in completion 2023-09-23 09:23:25 +00:00
Carl Schwan
443661d113 Port away from BasicListItem 2023-09-23 09:07:05 +00:00
Tobias Fella
091c8806db Fix opening account editor 2023-09-23 10:19:44 +02:00
l10n daemon script
041c719a2e GIT_SILENT Sync po/docbooks with svn 2023-09-23 02:10:02 +00:00
James Graham
83a9bfa974 Remove timeline container as it has been replaced. 2023-09-22 23:50:29 +00:00
James Graham
e35a6f7257 Remove the import version from Bubble.qml, TimelineDelegate.qml and TextDelegate.qml 2023-09-22 18:46:17 +01:00
James Graham
6d56251f6f Fix the timeline Part 2: Bubble Rework
This reworks the bubble as a separate component and makes some fixes to prevent the console being spammed with polish loop warnings.
2023-09-22 17:12:56 +00:00
l10n daemon script
486fae9c10 GIT_SILENT Sync po/docbooks with svn 2023-09-22 02:10:59 +00:00
Tobias Fella
1c26d9b811 Remove QML import versions 2023-09-21 19:38:10 +02:00
Tobias Fella
6d7ae99c94 Don't crash when editing pending message 2023-09-21 16:48:09 +00:00
Carl Schwan
442a343097 Rework context menu RoomList
- Finish port to qt6 and replace icon by icon.name
- Use RoundedItemDelegate
2023-09-21 16:43:27 +00:00
Tobias Fella
f0a7216b4b Don't crash when trying to leave empty room
BUG: 474490
2023-09-21 16:38:49 +00:00
James Graham
e926b22524 Fix the Timeline Part 1
This introduces a new base delegate that handles sizing the content of delegate in the timeline, i.e. it handles all the size helper stuff. This is then used for all the other main delegates:
- messages
- state
- read marker

This means they now all have identical base code to do the sizing (read marker still had legacy code).

Because the new base delegate is called `TimelineDelegate` both `TimelineContainer` and `MessageDelegate` have been renamed:
- MessageDelegate -> TextDelegate - this never made sense before images, videos, etc are all technically messages in Matrix parlance
- TimelineContainer -> MessageDelegate - this has always really been the base for messages

Note - this is mostly groundwork for dealing with the layout polish loop spam which will hopefully be fixed in part 2 with a bubble rework.
2023-09-21 16:26:34 +00:00
Carl Schwan
69087c2117 Add top spacing for Room ListView 2023-09-21 15:07:55 +00:00
Carl Schwan
4d2104b54b Decrease top margin in SpaceDrawer
Make it the same as the left margin of the delegate for an increased
consistency.
2023-09-21 15:07:55 +00:00
l10n daemon script
3f85a359e1 GIT_SILENT Sync po/docbooks with svn 2023-09-21 02:18:35 +00:00
ivan tkachenko
3084913940 Opt-out of Kirigami.SpellCheck when a custom QTextDocument handler is used
Uses Kirigami.SpellCheck's new shorter name.

See https://invent.kde.org/frameworks/kirigami/-/merge_requests/1261
2023-09-21 02:53:32 +03:00
Joshua Goins
e2670cd6ba Use Qt::ArrowType in FormCard.FormArrow usage on AccountsPage 2023-09-20 14:59:11 -04:00
l10n daemon script
1b6fc3dde5 GIT_SILENT Sync po/docbooks with svn 2023-09-20 02:15:06 +00:00
l10n daemon script
69a19effa2 GIT_SILENT Sync po/docbooks with svn 2023-09-19 02:17:37 +00:00
l10n daemon script
4abdf1f920 GIT_SILENT Sync po/docbooks with svn 2023-09-18 02:21:13 +00:00
l10n daemon script
0b1a6a3f6b GIT_SILENT made messages (after extraction) 2023-09-18 01:59:17 +00:00
l10n daemon script
45544c79bb GIT_SILENT Sync po/docbooks with svn 2023-09-17 02:54:29 +00:00
l10n daemon script
c4dddf6e02 GIT_SILENT Sync po/docbooks with svn 2023-09-16 02:18:05 +00:00
Tobias Fella
7f3f628b7d Fix missing connection 2023-09-15 17:41:08 +00:00
James Graham
6bf552398e Fix getting connection in message edit component 2023-09-15 18:35:23 +01:00
James Graham
78f676d71a Fix the room list menu
As title
2023-09-15 16:22:59 +00:00
James Graham
33c0cae64c Message menu rework
Rework the file menu so that it no longer relies on having a reference to the media delegate to manage a download for either opening externally or copying to clipboard. This allows the menus to be moved out of the delegates and maximize components and have them accessed through RoomManager. This reduces duplication and reduces the number of components in an already heavy delegate.
2023-09-15 13:57:40 +00:00
James Graham
14cdd096cf Fix Delegate Menu in NeochatMaximizeComponent
Make sure NeochatMaximizeComponent provides all the required properties to the delegate menu
2023-09-15 11:00:40 +00:00
James Graham
c04ddfde26 EventSource Refactor
Move showing th event source to a call to RoomManager. This means the SourceRole is no longer required in the message and search models
2023-09-15 10:54:04 +00:00
l10n daemon script
ec4c156a8c GIT_SILENT Sync po/docbooks with svn 2023-09-15 02:14:09 +00:00
Tobias Fella
17ff5b4c56 Add button to reject invitation and ignore user
BUG: 474274
2023-09-14 15:34:19 +00:00
Tobias Fella
0e2275e415 Always require passing tests 2023-09-14 10:23:52 +00:00
l10n daemon script
12fd1875b5 GIT_SILENT Sync po/docbooks with svn 2023-09-14 02:13:41 +00:00
l10n daemon script
10e50804c7 GIT_SILENT made messages (after extraction) 2023-09-14 01:54:51 +00:00
James Graham
c01c638a49 Fix file menu
Pass file menu eventSource rather than source and htmlText
2023-09-13 18:46:41 +00:00
Tobias Fella
399151eb1d Fix opening context menus 2023-09-13 15:48:39 +02:00
Tobias Fella
5e80715898 Cleanup connection handling in QML
Consistently pass connection objects to files that need them instead of randomly using Controller.activeConnection in some of them
2023-09-13 13:12:01 +00:00
Tobias Fella
6439fa48f9 Adapt to QML api changes 2023-09-13 12:57:27 +00:00
Tobias Fella
823f3cdd4e Remove QML Qt5/Qt6 compatibility hacks 2023-09-13 12:57:27 +00:00
Tobias Fella
f542d0b9fd Change rowStride in runner to qsizetype 2023-09-13 14:53:07 +02:00
Tobias Fella
a43990559b Rename source property of MessageDelegateContextMenu
Starting with Qt6.6, this collides with a FINAL property of Loader and thus doesn't run
2023-09-13 14:16:48 +02:00
Tobias Fella
feb2dbc9fb Don't try finding KStatusNotifierItem on android 2023-09-13 12:05:40 +00:00
Tobias Fella
f299d5a245 Fix KF6 crash in emoji completion 2023-09-13 13:41:08 +02:00
Tobias Fella
d69b8fbf8c Fix compilation on FreeBSD 2023-09-13 13:27:33 +02:00
Tobias Fella
234e5c49c4 Re-enable FreeBSD CI
Was accidentally disabled during a merge
2023-09-13 13:23:45 +02:00
Tobias Fella
cee72b6d48 Remove Qt5 compatibility ifdefs 2023-09-13 11:16:02 +00:00
Tobias Fella
539fdcaf2e Remove compatibility with Qt5 in CMake 2023-09-13 11:09:41 +00:00
Joshua Goins
32b3861c3e Only show the "View Source" message action when dev tools are enabled
This is a very developer-oriented action, and other uses of this dialog
(such as interacting with room state events under a room debug page) is
also locked behind this option.
2023-09-13 07:12:18 +00:00
l10n daemon script
f1076a5ced GIT_SILENT Sync po/docbooks with svn 2023-09-13 02:24:16 +00:00
Joshua Goins
bf8f5705d0 Fix message event source dialog not showing any data 2023-09-12 17:10:23 -04:00
Tobias Fella
2656a93ee7 Disable Qt5 CI 2023-09-12 15:39:25 +00:00
l10n daemon script
cbab810a2e GIT_SILENT Sync po/docbooks with svn 2023-09-12 01:47:54 +00:00
Tobias Fella
e78dfaec34 Use view background color for SpaceDrawer 2023-09-11 20:20:49 +00:00
ivan tkachenko
3cfa773820 Settings: Port to AvatarButton and fix OpenFileDialog lifespan
The dialog won't show again after being closed first time, so we need
to dynamically destroy it on both accept and reject. It helps clear out
any state on its way.
2023-09-11 22:47:33 +03:00
Tobias Fella
94c0e8b6cd Port ChatBar actions to Button
Cleans up the code and improves accessibility
2023-09-11 19:23:13 +00:00
Tobias Fella
35b1f24cb7 Fix roomlist when switching accounts
Fixes #605
2023-09-11 19:03:30 +00:00
Tobias Fella
de3072125e Change all remaining QML file ids to "root" 2023-09-11 20:47:32 +02:00
Tobias Fella
f7d2ffac66 Move Controller::createRoom and Controller::createSpace to NeoChatConnection 2023-09-11 17:55:52 +00:00
James Graham
ff0990bb7c Emit Room Selected from JoinRoomPage
Change the `JoinRoomPage` so that it emits a `roomSelected` signal with the selected delegate's info instead of calling the functions to join or enter the room itself. This is so that the `JoinRoomPage` can be used for other purposes like selecting an existing child to add to a space.
2023-09-11 17:16:12 +00:00
James Graham
8285961c42 Restore compact mode background
Restore compact mode using the darker/lighter depending on color scheme background (i.e. the same as the bubble background) it broke somewhere in the refactor of roompage
2023-09-11 07:19:32 +00:00
James Graham
cd39d5b129 Shutup LinkPreviewDelegate
Make sure that the console isn't spammed having `linkpreviewer` undefined in `LinkPreviewDelegate`, even though it shouldn't ever be and I couldn't find any case where it was but my console was still intermittently getting spammed.
2023-09-11 06:27:13 +00:00
l10n daemon script
83b3fefbf5 GIT_SILENT Sync po/docbooks with svn 2023-09-11 01:48:45 +00:00
l10n daemon script
3718bd716a GIT_SILENT made messages (after extraction) 2023-09-11 00:46:51 +00:00
Akseli Lahtinen
a18257ee17 Taskbar badge highlight counter
Instead of the taskbar badge showing count for all notifications, only show the count for total highlights.
2023-09-10 20:57:39 +00:00
Laurent Montel
def46d90a8 Fix qml signal argument warning 2023-09-10 20:19:32 +02:00
ivan tkachenko
8afd7b2892 Give "+" tool button a proper a11y role and an RTL-friendly modal menu 2023-09-10 20:40:23 +03:00
ivan tkachenko
deb11367cb ChatBox: Fix jittery ChatBar
ChatBar animated its implicit height, and there is no way for us to
force it animating on integer values only. It is contained in a Layout
type, and QtQuick.Layouts are known to round up size of their items and
position them on an integer scale. All in all, is caused bottom of chat
bar (action icons row and the whole bottom edge) to flicker during
height animation.
2023-09-10 20:40:23 +03:00
ivan tkachenko
ffd1b06a82 RoomSettings: Port disabled text fields to readOnly
Improves visuals, and allows to select & copy text. Also hides Save
button when there's no chance it would get enabled.

Depends on very recently merged MR in kirigami-addons:
https://invent.kde.org/libraries/kirigami-addons/-/merge_requests/153
2023-09-10 20:40:22 +03:00
ivan tkachenko
2597f0aec9 RoomSettings: Make room avatar bigger
This is literally the page about the room, we are not short on space
here. Make it shine.
2023-09-10 20:40:22 +03:00
James Graham
33ca72efd9 Fix ImageDelegate
Restore the animated mediaInfo to eventHandler and make sure ImageDelegate uses the media helper size for the sourceSize.
2023-09-10 16:22:14 +00:00
ivan tkachenko
2fbf659eb4 RoomInformation: Fix sizing of ListView header
Header is parented to ListView's contentItem (flickable) which is
subject to padding due to vertical scrollbar.
2023-09-10 17:07:20 +03:00
Antonio Rojas
763198b2c3 Add cmake check for qtlocation QML module
So that packagers are notified that it is a dependency
2023-09-10 10:12:52 +00:00
q234 rty
50551541ed Use shortcode as a fallback for body when sending stickers 2023-09-10 09:41:46 +00:00
James Graham
323cd4962e Explore rooms page RoundedItemDelegate
Move the explore rooms page to using the new RoundedItemDelegate
2023-09-10 09:00:57 +00:00
l10n daemon script
b2f592afeb GIT_SILENT Sync po/docbooks with svn 2023-09-10 01:45:20 +00:00
Tobias Fella
f7f98c43e2 Fix fix for crash in push rules 2023-09-09 12:13:33 +00:00
l10n daemon script
06134f96eb GIT_SILENT Sync po/docbooks with svn 2023-09-09 01:46:10 +00:00
Laurent Montel
bb62849a19 Use explicit qml signal argument in ShareAction 2023-09-08 07:44:31 +00:00
Laurent Montel
d05385caed Remove unused qml module 2023-09-08 08:28:03 +02:00
l10n daemon script
209ca747b5 GIT_SILENT Sync po/docbooks with svn 2023-09-08 01:46:47 +00:00
James Graham
2c6ab498ac Event Handler
Similar to text handler, pull out the disparate array of functions which format information from an event ready for display in the UI and put in a handler class with a test suite.

requires https://github.com/quotient-im/libQuotient/pull/686
2023-09-07 19:02:50 +00:00
Laurent Montel
597633f824 Add explicit signal argument 2023-09-07 13:42:45 +00:00
Laurent Montel
65da416b8e Disable add button when text is empty 2023-09-07 08:16:02 +00:00
l10n daemon script
c528929b30 GIT_SILENT Sync po/docbooks with svn 2023-09-07 01:46:10 +00:00
l10n daemon script
8650ce1755 GIT_SILENT Sync po/docbooks with svn 2023-09-06 01:44:38 +00:00
Tobias Fella
8936b23da9 Remove "Apply" Button in security settings
It doesn't do anything. All settings are instant-apply.
2023-09-05 12:58:57 +00:00
l10n daemon script
cf72fbea2e GIT_SILENT Sync po/docbooks with svn 2023-09-05 01:45:00 +00:00
Laurent Montel
0d5929b4bc Fix enable/disable save button when image.source is empty. Otherwise it will crash 2023-09-04 19:03:06 +00:00
Ingo Klöcker
d43cc9a044 Terminate the mock server after the test 2023-09-04 17:47:34 +02:00
Ingo Klöcker
682e3967ba Increase timeout per test to 90 seconds
The Appium test (including the setup) takes more than the default 60 s.
2023-09-04 17:14:02 +02:00
Ingo Klöcker
ceebee3a56 Look for "login-server.py" next to "logintest.py" 2023-09-04 16:46:20 +02:00
Ingo Klöcker
52ab052aef Choose the Login option before trying to enter the login credentials 2023-09-04 16:11:20 +02:00
Ingo Klöcker
e6a2b2d125 Disable outdated test for now 2023-09-04 16:01:58 +02:00
Ingo Klöcker
92dadc2cc9 Use sys.executable instead of a hardcoded executable name/path of Python 2023-09-04 16:01:34 +02:00
Tobias Fella
6ca9380a51 Enable linux Qt5 CI 2023-09-04 13:12:37 +00:00
Laurent Montel
51d41ab5bf Enable "add" button only if text is not empty
=> otherwise it will report error
2023-09-04 07:24:25 +02:00
l10n daemon script
f1047fa474 GIT_SILENT Sync po/docbooks with svn 2023-09-04 01:47:23 +00:00
Tobias Fella
011b11681f Remove button to toggle search in room member list 2023-09-03 16:15:36 +02:00
Tobias Fella
8baeb236bc Remove unused signals in Controller 2023-09-03 11:32:59 +00:00
Tobias Fella
b9d173d0b5 Remove Controller::accountCount in favor of AccountRegistry::accountCount 2023-09-03 11:23:39 +00:00
Tobias Fella
3790955c56 Port CreateRoomDialog to FormCard 2023-09-03 11:15:33 +00:00
James Graham
199772a013 Room Drawer Media Tab
Add a tab bar to the room drawer which includes a new media tab in addition to the room information tab. This mr completes the architecture for adding others easily later e.g. message highlights or threads.

To put this together I had to make sure things like the menus and the maximize delegate were available to both the room drawer and page so there is some rework there to put it all together.

Wide\
![image](/uploads/b7d3a3ee00016f9ede5cf6fb93e7b40c/image.png)

Mobile\
![image](/uploads/aa02e23f79b37f6cad903d3f356e0ef4/image.png)
2023-09-03 10:25:04 +00:00
l10n daemon script
54cc3ac761 GIT_SILENT Sync po/docbooks with svn 2023-09-03 02:08:34 +00:00
Tobias Fella
1675c14c92 Port GlobalNotificationsPage to FormCard 2023-09-02 17:12:46 +00:00
James Graham
56f5ef2611 MediaSizeHelper
Create a media size helper and use it to force video and images to be the correct size even in replies.
2023-09-02 16:43:05 +00:00
Tobias Fella
7ba63eb680 Port proxy page to formcard 2023-09-02 15:59:22 +00:00
Tobias Fella
ddd690f6d5 Port DevicesPage to FormCard 2023-09-02 15:59:07 +00:00
Tobias Fella
d9dc5f48bc Port appearance settings to formcard 2023-09-02 15:58:55 +00:00
Tobias Fella
a3b40a5e6d Port devtools to FormCard 2023-09-02 15:58:37 +00:00
Tobias Fella
be71a4349b Port accounts page to formcard 2023-09-02 15:50:05 +00:00
James Graham
ede2707767 New default pushrules
Add handling for new default push rules added in 1.7 also add a generic unknown handling for if any others are added in the future so they don't appear in keywords
2023-09-02 15:48:50 +00:00
Tobias Fella
5871903529 Port CreateSpaceDialog to FormCard 2023-09-02 15:46:25 +02:00
Tobias Fella
87745c360d Port EmoticonEditorPage to FormCard 2023-09-02 11:45:14 +00:00
Tobias Fella
40d2eb5aba Port general settings page to FormCard 2023-09-02 11:45:01 +00:00
Tobias Fella
cfd9f36a97 Port NotificationRuleItem to FormCard 2023-09-02 11:42:44 +00:00
Tobias Fella
48f7e06c01 Port remaining room settings to FormCard 2023-09-02 11:42:38 +00:00
Tobias Fella
43ecd188f1 Port SonnetConfigPage to FormCard 2023-09-02 11:33:03 +00:00
Tobias Fella
063056bebd Port EmoticonsPage to FormCard 2023-09-02 11:25:32 +00:00
Laurent Montel
dfba655527 Fix missing argument signal in qml 2023-09-02 11:13:51 +00:00
Laurent Montel
23178b6224 Remove unused import qml module (scripted) 2023-09-02 11:54:28 +02:00
Tobias Fella
a9a6e31b8c Use FormCard AboutKDE 2023-09-02 07:41:41 +00:00
l10n daemon script
0afd74d96a GIT_SILENT Sync po/docbooks with svn 2023-09-02 01:50:44 +00:00
Tobias Fella
c5501f2b5d Port ColorScheme to FormCard 2023-09-01 19:19:08 +00:00
Tobias Fella
68602fc177 Port ExploreComponent actions to pageStack.pushDialogLayer 2023-09-01 19:04:56 +00:00
Tobias Fella
8ce5a57499 Remove type registration for RoomMessageEvent 2023-09-01 18:54:58 +00:00
James Graham
bf3c5708c7 Remove support streaming
Remove support streaming from video delegate as it's breaking downloads and unused
2023-09-01 17:54:37 +00:00
Tobias Fella
8359c1fe90 Only set graphics API when using Qt6 2023-09-01 16:06:00 +00:00
Laurent Montel
774121ea8c Fix qml explicit signal argument 2023-09-01 17:11:35 +02:00
Tobias Fella
370bbf8a4b Set attributes for QtWebEngine 2023-09-01 11:34:33 +00:00
Tobias Fella
4fd36fb221 Fix crash for push rules without action
@nvrwhere how much sense does this fix make? i only know that it doesn't crash anymore
2023-09-01 11:33:29 +00:00
Laurent Montel
167349b9d2 Remove unused forward declaration 2023-09-01 11:51:36 +02:00
l10n daemon script
296aa9ddad GIT_SILENT Sync po/docbooks with svn 2023-09-01 01:45:50 +00:00
Tobias Fella
94c1b67ec9 Port account editor to formcard 2023-08-31 22:02:30 +00:00
Tobias Fella
a9965acdd3 Fix compilation warning in searchmodel 2023-08-31 21:52:23 +00:00
Tobias Fella
35313271a3 Cleanup registration.h 2023-08-31 21:36:56 +00:00
Tobias Fella
e40179f641 Don't load data from invalid indices in MessageFilterModel 2023-08-31 21:28:20 +00:00
Tobias Fella
dacf3ff8a0 Fix compilation warnings in registration.cpp 2023-08-31 21:21:22 +00:00
Tobias Fella
0274d70f71 Fix connection property initialization 2023-08-31 21:16:57 +00:00
Tobias Fella
0c19052f32 Port PushNotification settings page to FormCard 2023-08-31 20:40:59 +00:00
Laurent Montel
a3423e5724 Fix warning about missing argument qml signal 2023-08-31 10:02:32 +00:00
l10n daemon script
e09f31f891 GIT_SILENT Sync po/docbooks with svn 2023-08-31 01:46:27 +00:00
Tobias Fella
e05e0f5489 Improve QML types 2023-08-30 23:04:18 +00:00
Tobias Fella
002961f638 Move jobs to individual files 2023-08-30 21:10:56 +00:00
Laurent Montel
e28419ef88 Use isEmpty() here 2023-08-30 19:29:02 +02:00
Laurent Montel
0e4df58fc9 Add missing include moc file 2023-08-30 19:20:04 +02:00
Tobias Fella
21a55885b3 Only read role names from source model when there is a source model 2023-08-30 15:21:24 +00:00
l10n daemon script
6a8ac8f6e5 GIT_SILENT Sync po/docbooks with svn 2023-08-30 01:46:13 +00:00
328 changed files with 128517 additions and 110579 deletions

View File

@@ -3,12 +3,8 @@
include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -2,35 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
'require':
'frameworks/extra-cmake-modules': '@stable'
'frameworks/kcoreaddons': '@stable'
'frameworks/kirigami': '@stable'
'frameworks/ki18n': '@stable'
'frameworks/kconfig': '@stable'
'frameworks/syntax-highlighting': '@stable'
'frameworks/kitemmodels': '@stable'
'frameworks/kquickcharts': '@stable'
'frameworks/knotifications': '@stable'
'libraries/kquickimageeditor': '@stable'
'frameworks/sonnet': '@stable'
'libraries/kirigami-addons': '@latest'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/qqc2-desktop-style': '@stable'
'frameworks/kio': '@stable'
'frameworks/kwindowsystem': '@stable'
'frameworks/kconfigwidgets': '@stable'
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
'require':
'frameworks/kdbusaddons': '@stable'
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
@@ -48,20 +20,23 @@ Dependencies:
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux/Qt6', 'Linux/Qt5']
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD', 'Windows' ]
per-test-timeout: 90
require-passing-tests-on: [ '@all' ]

View File

@@ -34,7 +34,7 @@ Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
Files: src/qml/confetti.png src/qml/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0

View File

@@ -14,11 +14,8 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.105.0")
set(QT_MIN_VERSION "5.15.2")
if (ANDROID)
set(QT_MIN_VERSION "5.15.10")
endif()
set(KF_MIN_VERSION "5.240.0")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
@@ -48,28 +45,6 @@ if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
if(QT_MAJOR_VERSION STREQUAL "6")
set(BASICLISTITEM_BOLD "font.bold")
set(OVERLAYSHEET_OPEN "onOpened")
set(QTQUICK_MODULE_QML_VERSION "")
set(QTLOCATION_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_MODULE_QML_VERSION "")
set(QTMULTIMEDIA_AUDIO "MediaPlayer")
# in Audio qt6 we don't have it but we disable it in qt5 => it seems ok
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "")
# In Video qml qt6 we don't have it.
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "")
else()
set(BASICLISTITEM_BOLD "bold")
set(OVERLAYSHEET_OPEN "onSheetOpenChanged")
set(QTQUICK_MODULE_QML_VERSION "2.15")
set(QTLOCATION_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_MODULE_QML_VERSION "5.15")
set(QTMULTIMEDIA_AUDIO "Audio")
set(QTMULTIMEDIA_AUDIO_AUTOLOAD "autoLoad: false")
set(QTMULTIMEDIA_VIDEO_FLUSHMODE "flushMode: VideoOutput.FirstFrame")
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_setup_version(${PROJECT_VERSION}
@@ -77,25 +52,21 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
set_package_properties(Qt6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
if(QT_MAJOR_VERSION STREQUAL "6")
find_package(KF6StatusNotifierItem ${KF_MIN_VERSION} REQUIRED)
endif()
set_package_properties(KF6Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if(ANDROID)
find_package(OpenSSL)
@@ -104,24 +75,26 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
find_package(ICU 61.0 COMPONENTS uc)
set_package_properties(ICU PROPERTIES
TYPE REQUIRED
PURPOSE "Unicode library"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
if (NOT ANDROID AND NOT APPLE)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
if(QT_MAJOR_VERSION STREQUAL "6" AND NOT ANDROID AND NOT WIN32)
set(QUOTIENT_SUFFIX "Qt6")
endif()
find_package(Quotient${QUOTIENT_SUFFIX} 0.7)
set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
find_package(QuotientQt6 0.7)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
URL "https://github.com/quotient-im/libQuotient/"
@@ -144,6 +117,7 @@ set_package_properties(cmark PROPERTIES
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
ecm_find_qmlmodule(QtLocation ${QTLOCATION_MODULE_QML_VERSION})
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -153,12 +127,12 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
qcoro_enable_coroutines()
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
find_package(KF6DocTools ${KF_MIN_VERSION})
set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
"Tools to generate documentation"
TYPE OPTIONAL
)
@@ -180,12 +154,12 @@ add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
add_subdirectory(appiumtests)
endif()
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
if(KF6DocTools_FOUND)
kdoctools_install(po)
add_subdirectory(doc)
endif()

View File

@@ -18,6 +18,6 @@ if(NOT SeleniumWebDriverATSPI_FOUND)
endif()
add_test(
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
)

View File

@@ -4,23 +4,26 @@
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
import time
import os
import subprocess
import sys
import unittest
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class LoginTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(self):
desired_caps = {}
desired_caps["app"] = "neochat --ignore-ssl-errors"
desired_caps["timeouts"] = {'implicit': 10000}
self.driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723',
desired_capabilities=desired_caps)
subprocess.Popen(["python","login-server.py"])
def setUpClass(cls):
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
def setUp(self):
pass
@@ -31,14 +34,18 @@ class LoginTest(unittest.TestCase):
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_login(self):
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
self.driver.find_element(by=AppiumBy.NAME, value="Join some rooms to get started").click()
if __name__ == '__main__':
unittest.main()

View File

@@ -20,3 +20,15 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME delegatesizehelpertest
)
ecm_add_test(
mediasizehelpertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME mediasizehelpertest
)
ecm_add_test(
eventhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)

View File

@@ -0,0 +1,729 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "eventhandler.h"
#include <KFormat>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatroom.h"
#include "utils.h"
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class EventHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
EventHandler eventHandler;
private Q_SLOTS:
void initTestCase();
void eventId();
void delegateType_data();
void delegateType();
void author();
void authorDisplayName();
void time();
void timeString();
void highlighted();
void hidden();
void body();
void genericBody_data();
void genericBody();
void mediaInfo();
void linkPreviewer();
void reactions();
void hasReply();
void replyId();
void replyDelegateType();
void replyAuthor();
void replyBody();
void replyMediaInfo();
void location();
void readMarkers();
};
void EventHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
const auto json = QJsonDocument::fromJson(R"EVENT({
"account_data": {
"events": [
{
"content": {
"tags": {
"u.work": {
"order": 0.9
}
}
},
"type": "m.tag"
},
{
"content": {
"custom_config_key": "custom_config_value"
},
"type": "org.example.custom.room.config"
}
]
},
"ephemeral": {
"events": [
{
"content": {
"user_ids": [
"@alice:matrix.org",
"@bob:example.com"
]
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"type": "m.typing"
},
{
"content": {
"$153456789:example.org": {
"m.read": {
"@alice:matrix.org": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@bob:example.com": {
"ts": 1436451550453
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tim:example.com": {
"ts": 1436451550454
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@jeff:example.com": {
"ts": 1436451550455
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@tina:example.com": {
"ts": 1436451550456
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@sally:example.com": {
"ts": 1436451550457
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1532735824654:example.org": {
"m.read": {
"@fred:example.com": {
"ts": 1436451550458
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"summary": {
"m.heroes": [
"@alice:example.com",
"@bob:example.com"
],
"m.invited_member_count": 0,
"m.joined_member_count": 2
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example\ntext message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example<br>text message</b>",
"msgtype": "m.text"
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"avatar_url": "mxc://kde.org/123456",
"displayname": "after",
"membership": "join"
},
"origin_server_ts": 1690651134736,
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$1234567890:example.org",
"prev_content": {
"avatar_url": "mxc://kde.org/12345",
"displayname": "before",
"membership": "join"
},
"prev_sender": "@example:example.orgg",
"age": 1234
},
"event_id": "$143273583553PhrSn:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$1532735824654:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545182,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:matrix.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$163456789:example.org",
"age": 390159120
},
{
"age": 4926305285,
"content": {
"body": "video caption",
"filename": "video.mp4",
"info": {
"duration": 10,
"h": 1080,
"mimetype": "video/mp4",
"size": 62650636,
"w": 1920,
"thumbnail_info": {
"h": 450,
"mimetype": "image/jpeg",
"size": 382249,
"w": 800
},
"thumbnail_url": "mxc://kde.org/2234567"
},
"msgtype": "m.video",
"url": "mxc://kde.org/1234567"
},
"event_id": "$263456789:example.org",
"origin_server_ts": 1685793783330,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 4926305285
},
"user_id": "@example:example.org"
},
{
"content": {
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$153456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965572,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456789:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"content": {
"body": "> <@example:example.org> video caption\n\nreply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$263456789:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1690725965573,
"sender": "@alice:matrix.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$154456799:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1544567999:example.org",
"origin_server_ts": 1690821582876,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
}
void EventHandlerTest::eventId()
{
eventHandler.setEvent(room->messageEvents().at(0).get());
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<DelegateType::Type>("delegateType");
QTest::newRow("message") << 0 << DelegateType::Message;
QTest::newRow("state") << 1 << DelegateType::State;
QTest::newRow("message 2") << 2 << DelegateType::Message;
QTest::newRow("reaction") << 3 << DelegateType::Other;
QTest::newRow("video") << 4 << DelegateType::Video;
QTest::newRow("location") << 7 << DelegateType::Location;
}
void EventHandlerTest::delegateType()
{
QFETCH(int, eventNum);
QFETCH(DelegateType::Type, delegateType);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
auto author = room->user(event->senderId());
eventHandler.setEvent(event);
auto eventHandlerAuthor = eventHandler.getAuthor();
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::authorDisplayName()
{
auto event = room->messageEvents().at(1).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::timeString()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
KFormat format;
QCOMPARE(eventHandler.getTimeString(false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(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)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(eventHandler.getTimeString(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)),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::highlighted()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
auto event = room->messageEvents().at(3).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
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"));
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
QTest::addColumn<QString>("output");
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
}
void EventHandlerTest::genericBody()
{
QFETCH(int, eventNum);
QFETCH(QString, output);
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
eventHandler.setEvent(event);
auto mediaInfo = eventHandler.getMediaInfo();
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->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(event->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::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
}
void EventHandlerTest::hasReply()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), true);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
auto replyEvent = room->messageEvents().at(0).get();
auto replyAuthor = room->user(replyEvent->senderId());
eventHandler.setEvent(event);
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
auto event = room->messageEvents().at(5).get();
eventHandler.setEvent(event);
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::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
auto replyEvent = room->messageEvents().at(4).get();
eventHandler.setEvent(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::location()
{
auto event = room->messageEvents().at(7).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::readMarkers()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
auto readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 1);
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.hasReadMarkers(), true);
readMarkers = eventHandler.getReadMarkers();
QCOMPARE(readMarkers.size(), 5);
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include <qglobal.h>
#include <qtestcase.h>
#include <cmath>
#include "mediasizehelper.h"
#include "neochatconfig.h"
class MediaSizeHelperTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void uninitialized();
void limits_data();
void limits();
};
void MediaSizeHelperTest::uninitialized()
{
MediaSizeHelper mediasizehelper;
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
}
void MediaSizeHelperTest::limits_data()
{
QTest::addColumn<qreal>("mediaWidth");
QTest::addColumn<qreal>("mediaHeight");
QTest::addColumn<qreal>("contentMaxWidth");
QTest::addColumn<qreal>("contentMaxHeight");
QTest::addColumn<QSize>("currentSize");
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
QTest::newRow("limit by content width tall media")
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
QTest::newRow("limit by content height wide media")
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
}
void MediaSizeHelperTest::limits()
{
QFETCH(qreal, mediaWidth);
QFETCH(qreal, mediaHeight);
QFETCH(qreal, contentMaxWidth);
QFETCH(qreal, contentMaxHeight);
QFETCH(QSize, currentSize);
MediaSizeHelper mediasizehelper;
mediasizehelper.setMediaWidth(mediaWidth);
mediasizehelper.setMediaHeight(mediaHeight);
mediasizehelper.setContentMaxWidth(contentMaxWidth);
mediasizehelper.setContentMaxHeight(contentMaxHeight);
QCOMPARE(mediasizehelper.currentSize(), currentSize);
}
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
#include "mediasizehelpertest.moc"

View File

@@ -363,6 +363,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");
}
void TextHandlerTest::receiveRichInPlainOut()

View File

@@ -2,4 +2,4 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})

View File

@@ -53,13 +53,16 @@
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
@@ -159,6 +162,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">투표 - MSC3381</li>
<li xml:lang="nl">Polls - MSC3381</li>
<li xml:lang="nn">Avstemmingar  MSC3381</li>
<li xml:lang="pl">Ankiety - MSC3381</li>
<li xml:lang="pt">Inquéritos - MSC3381</li>
<li xml:lang="sl">Polls - MSC3381</li>
<li xml:lang="sv">Polls - MSC3381</li>
@@ -183,6 +187,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">스티커 팩 - MSC2545</li>
<li xml:lang="nl">Sticker Packs - MSC2545</li>
<li xml:lang="nn">Klistremerke-pakkar  MSC2545</li>
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
<li xml:lang="sl">Sticker Packs - MSC2545</li>
<li xml:lang="sv">Sticker Packs - MSC2545</li>
@@ -207,6 +212,7 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
<li xml:lang="nn">Posisjonshendingar  MSC3488</li>
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
<li xml:lang="sl">Location Events - MSC3488</li>
<li xml:lang="sv">Location Events - MSC3488</li>
@@ -277,7 +283,7 @@ to provide a convergent experience across multiple platforms.</p>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot x-kde-os="windows">
<screenshot environment="windows" x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
<caption>Main view with room list, chat, and room information</caption>
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
@@ -289,11 +295,13 @@ to provide a convergent experience across multiple platforms.</p>
<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="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>
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
@@ -302,7 +310,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
</screenshot>
<screenshot x-kde-os="windows">
<screenshot environment="windows" x-kde-os="windows">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
@@ -314,11 +322,13 @@ to provide a convergent experience across multiple platforms.</p>
<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="ia">Schermo de accesso</caption>
<caption xml:lang="it">Schermata di accesso</caption>
<caption xml:lang="ka">შესვლის ეკრანი</caption>
<caption xml:lang="ko">로그인 화면</caption>
<caption xml:lang="nl">Aanmeldscherm</caption>
<caption xml:lang="nn">Innloggingsbilete</caption>
<caption xml:lang="pl">Ekran logowania</caption>
<caption xml:lang="pt">Ecrã de autenticação</caption>
<caption xml:lang="sl">Prijavni zaslon</caption>
<caption xml:lang="sv">Inloggningsfönster</caption>

View File

@@ -1 +1,8 @@
<svg width="22" height="22" fill="none" version="1.1" id="svg13" xmlns="http://www.w3.org/2000/svg"><style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M3 3H19V14H8.68787L4 18.1019V14H3V3ZM4 13H5V15.8981L8.31213 13H18V4H4V13Z" fill="currentColor"/>
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M17 15.2929L14.8536 13.1465L14.1465 13.8536L18 17.7071V13.5H17V15.2929Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 6H15V7H5V6Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 8H13V9H5V8Z" fill="currentColor"/>
<path class="ColorScheme-Text" d="M5 10H11V11H5V10Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 752 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>Vegeu també</title>
<simplelist>
<member
>Una llista de les preguntes més freqüents quan a Matrix <ulink url="https://matrix.org/faq/"
>Una llista de les preguntes més freqüents quant a Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member

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

4359
po/eo/neochat.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,6 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
configure_file(qml/Page/RoomList/RoomDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Page/RoomList/RoomDelegate.qml)
configure_file(qml/Component/QuickSwitcher.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/QuickSwitcher.qml)
configure_file(qml/Dialog/PowerLevelDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Dialog/PowerLevelDialog.qml)
configure_file(qml/Component/Timeline/AudioDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/AudioDelegate.qml)
configure_file(qml/Component/Timeline/VideoDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/VideoDelegate.qml)
configure_file(qml/Component/Timeline/OsmLocationPlugin.qml ${CMAKE_CURRENT_BINARY_DIR}/qml/Component/Timeline/OsmLocationPlugin.qml)
configure_file(res.qrc ${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc)
add_library(neochat STATIC
controller.cpp
controller.h
@@ -49,6 +40,12 @@ add_library(neochat STATIC
models/userfiltermodel.h
models/publicroomlistmodel.cpp
models/publicroomlistmodel.h
models/spacechildrenmodel.cpp
models/spacechildrenmodel.h
models/spacechildsortfiltermodel.cpp
models/spacechildsortfiltermodel.h
models/spacetreeitem.cpp
models/spacetreeitem.h
models/userdirectorylistmodel.cpp
models/userdirectorylistmodel.h
models/pushrulemodel.cpp
@@ -64,8 +61,8 @@ add_library(neochat STATIC
models/devicesmodel.cpp
models/devicesmodel.h
models/devicesproxymodel.cpp
filetypesingleton.cpp
filetypesingleton.h
filetype.cpp
filetype.h
login.cpp
login.h
models/webshortcutmodel.cpp
@@ -126,6 +123,167 @@ add_library(neochat STATIC
registration.cpp
neochatconnection.cpp
neochatconnection.h
jobs/neochatdeactivateaccountjob.cpp
jobs/neochatdeactivateaccountjob.h
jobs/neochatdeletedevicejob.cpp
jobs/neochatdeletedevicejob.h
jobs/neochatchangepasswordjob.cpp
jobs/neochatchangepasswordjob.h
mediasizehelper.cpp
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
roomlastmessageprovider.cpp
roomlastmessageprovider.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
QML_FILES
qml/main.qml
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
qml/SpaceListContextMenu.qml
qml/UserInfo.qml
qml/LoadingPage.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/Security.qml
qml/PushNotification.qml
qml/Categories.qml
qml/Permissions.qml
qml/NeochatMaximizeComponent.qml
qml/FancyEffectsContainer.qml
qml/TypingPane.qml
qml/ShimmerGradient.qml
qml/QuickSwitcher.qml
qml/HoverActions.qml
qml/ChatBox.qml
qml/ChatBar.qml
qml/AttachmentPane.qml
qml/ReplyPane.qml
qml/CompletionMenu.qml
qml/PieProgressBar.qml
qml/QuickFormatBar.qml
qml/RoomData.qml
qml/ServerData.qml
qml/EmojiPicker.qml
qml/TimelineDelegate.qml
qml/ReplyComponent.qml
qml/StateDelegate.qml
qml/RichLabel.qml
qml/MessageDelegate.qml
qml/Bubble.qml
qml/SectionDelegate.qml
qml/VideoDelegate.qml
qml/ReactionDelegate.qml
qml/LinkPreviewDelegate.qml
qml/AudioDelegate.qml
qml/FileDelegate.qml
qml/ImageDelegate.qml
qml/EncryptedDelegate.qml
qml/EventDelegate.qml
qml/TextDelegate.qml
qml/ReadMarkerDelegate.qml
qml/PollDelegate.qml
qml/MimeComponent.qml
qml/StateComponent.qml
qml/MessageEditComponent.qml
qml/AvatarFlow.qml
qml/LoginStep.qml
qml/Login.qml
qml/Homeserver.qml
qml/Username.qml
qml/RegisterPassword.qml
qml/Captcha.qml
qml/Terms.qml
qml/Email.qml
qml/Password.qml
qml/LoginRegister.qml
qml/Loading.qml
qml/LoginMethod.qml
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/EmojiDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
qml/ConfirmLogoutDialog.qml
qml/PowerLevelDialog.qml
qml/Message.qml
qml/EmojiItem.qml
qml/EmojiRow.qml
qml/EmojiSas.qml
qml/ConfirmDeactivateAccountDialog.qml
qml/VerificationCanceled.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
qml/MessageDelegateContextMenu.qml
qml/FileDelegateContextMenu.qml
qml/MessageSourceSheet.qml
qml/ReportSheet.qml
qml/SettingsPage.qml
qml/ThemeRadioButton.qml
qml/ColorScheme.qml
qml/GeneralSettingsPage.qml
qml/EmoticonsPage.qml
qml/EmoticonEditorPage.qml
qml/EmoticonFormCard.qml
qml/GlobalNotificationsPage.qml
qml/NotificationRuleItem.qml
qml/AppearanceSettingsPage.qml
qml/AccountsPage.qml
qml/AccountEditorPage.qml
qml/DevicesPage.qml
qml/DeviceDelegate.qml
qml/DevicesCard.qml
qml/About.qml
qml/AboutKDE.qml
qml/SonnetConfigPage.qml
qml/NetworkProxyPage.qml
qml/DevtoolsPage.qml
qml/ConfirmEncryptionDialog.qml
qml/RemoveSheet.qml
qml/BanSheet.qml
qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml
qml/EmojiGrid.qml
qml/SearchPage.qml
qml/LocationDelegate.qml
qml/LocationChooser.qml
qml/TimelineView.qml
qml/InvitationView.qml
qml/AvatarTabButton.qml
qml/SpaceDrawer.qml
qml/OsmLocationPlugin.qml
qml/LiveLocationDelegate.qml
qml/FullScreenMap.qml
qml/LocationsPage.qml
qml/LocationMapItem.qml
qml/RoomDrawer.qml
qml/RoomDrawerPage.qml
qml/DirectChatDrawerHeader.qml
qml/GroupChatDrawerHeader.qml
qml/RoomInformation.qml
qml/RoomMedia.qml
qml/ChooseRoomDialog.qml
qml/ShareAction.qml
qml/SpaceHomePage.qml
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
ecm_qt_declare_logging_category(neochat
@@ -137,9 +295,15 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling"
CATEGORY_NAME "org.kde.neochat.eventhandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app
main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
)
if(TARGET Qt::WebView)
@@ -161,28 +325,24 @@ if(NOT ANDROID)
target_sources(neochat PRIVATE colorschemer.cpp colorschemer.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
if(QT_MAJOR_VERSION STREQUAL "6")
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
endif()
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem ICU::uc)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_sources(neochat-app PRIVATE res_desktop.qrc)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
target_sources(neochat PRIVATE runner.cpp)
else()
target_sources(neochat-app PRIVATE res_android.qrc)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF6::I18n KF6::Kirigami2 KF6::Notifications KF6::ConfigCore KF6::ConfigGui KF6::CoreAddons KF6::SonnetCore KF6::ItemModels QuotientQt6 cmark::cmark QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -270,7 +430,7 @@ if(ANDROID)
"gps"
)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
@@ -278,12 +438,12 @@ if(NOT ANDROID)
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
endif()
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
if(TARGET KF6::DBusAddons)
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()

View File

@@ -4,12 +4,12 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <Quotient/events/roommessageevent.h>
#include "neochatroom.h"
class CustomEmojiModel;
class NeoChatRoom;
/**
@@ -33,6 +33,7 @@ class NeoChatRoom;
class ActionsHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The room that messages will be sent to.

View File

@@ -134,7 +134,7 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#if !defined(Q_OS_ANDROID)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();

View File

@@ -4,13 +4,13 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QTextCursor>
#include "models/completionmodel.h"
#include "neochatroom.h"
class QTextDocument;
class NeoChatRoom;
class SyntaxHighlighter;
@@ -59,6 +59,7 @@ class SyntaxHighlighter;
class ChatDocumentHandler : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Is the instance being used to handle an edit message.

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
class QClipboard;
class QImage;
@@ -18,6 +19,8 @@ class QImage;
class Clipboard : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Whether the current clipboard content is an image.

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
class QAbstractItemModel;
class KColorSchemeManager;
@@ -19,6 +20,8 @@ class KColorSchemeManager;
class ColorSchemer : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief A QAbstractItemModel of all available color schemes.

View File

@@ -4,11 +4,7 @@
#include "controller.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <qt5keychain/keychain.h>
#else
#include <qt6keychain/keychain.h>
#endif
#include <KConfig>
#include <KConfigGroup>
@@ -97,8 +93,6 @@ Controller::Controller(QObject *parent)
}
#endif
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, &Controller::activeConnectionIndexChanged);
static int oldAccountCount = 0;
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
@@ -165,15 +159,14 @@ void Controller::addConnection(NeoChatConnection *c)
c->sync();
Q_EMIT connectionAdded(c);
Q_EMIT accountCountChanged();
}
void Controller::dropConnection(NeoChatConnection *c)
{
Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection");
m_accountRegistry.drop(c);
Q_EMIT connectionDropped(c);
Q_EMIT accountCountChanged();
}
void Controller::invokeLogin()
@@ -297,29 +290,6 @@ bool Controller::supportSystemTray() const
#endif
}
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}
int Controller::accountCount() const
{
return m_accountRegistry.count();
}
void Controller::setQuitOnLastWindowClosed()
{
#ifndef Q_OS_ANDROID
@@ -397,44 +367,6 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}
void Controller::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, QString(), name, topic, QStringList());
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Room creation failed: %1", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
void Controller::createSpace(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::UnpublishRoom,
{},
name,
topic,
QStringList(),
{},
{},
false,
{},
{},
QJsonObject{
{"type"_ls, "m.space"_ls},
});
connect(createRoomJob, &CreateRoomJob::failure, this, [this, createRoomJob] {
Q_EMIT errorOccured(i18n("Space creation failed: %1", createRoomJob->errorString()));
});
connectSingleShot(this, &Controller::roomAdded, &RoomManager::instance(), &RoomManager::enterRoom, Qt::QueuedConnection);
}
bool Controller::isOnline() const
{
return m_isOnline;

View File

@@ -5,6 +5,7 @@
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KFormat>
@@ -16,7 +17,6 @@
class NeoChatRoom;
class TrayIcon;
class QWindow;
class QQuickTextDocument;
namespace Quotient
@@ -41,11 +41,8 @@ class ReadPasswordJob;
class Controller : public QObject
{
Q_OBJECT
/**
* @brief The number of logged in accounts.
*/
Q_PROPERTY(int accountCount READ accountCount NOTIFY accountCountChanged)
QML_ELEMENT
QML_SINGLETON
/**
* @brief The current connection for the rest of NeoChat to use.
@@ -96,8 +93,11 @@ public:
Q_ENUM(PasswordStatus)
static Controller &instance();
[[nodiscard]] int accountCount() const;
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
@@ -121,16 +121,6 @@ public:
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Create new room for a group chat.
*/
Q_INVOKABLE void createRoom(const QString &name, const QString &topic);
/**
* @brief Create new space.
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic);
/**
* @brief Join a room.
*/
@@ -216,42 +206,15 @@ Q_SIGNALS:
void syncDone();
void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection);
void accountCountChanged();
void initiated();
void notificationClicked(const QString &_t1, const QString &_t2);
void quitOnLastWindowClosedChanged();
void unreadCountChanged();
void activeConnectionChanged();
void passwordStatus(Controller::PasswordStatus _t1);
void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
void isOnlineChanged(bool isOnline);
void keyVerificationRequest(int timeLeft, NeoChatConnection *connection, const QString &transactionId, const QString &deviceId);
void keyVerificationStart();
void keyVerificationAccept(const QString &commitment);
void keyVerificationKey(const QString &sas);
void activeConnectionIndexChanged();
void roomAdded(NeoChatRoom *room);
public Q_SLOTS:
void saveWindowGeometry();
};
// TODO libQuotient 0.7: Drop
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class DelegateSizeHelper
@@ -23,6 +24,7 @@
class DelegateSizeHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The width of the component's parent.

46
src/enums/delegatetype.h Normal file
View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class DelegateType
*
* This class is designed to define the DelegateType enumeration.
*/
class DelegateType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum Type {
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);
};

990
src/eventhandler.cpp Normal file
View File

@@ -0,0 +1,990 @@
// 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 "eventhandler.h"
#include <QMovie>
#include <KLocalizedString>
#include <Quotient/eventitem.h>
#include <Quotient/events/encryptionevent.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roomcanonicalaliasevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/roompowerlevelsevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
#include "models/reactionmodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "texthandler.h"
#include "utils.h"
using namespace Quotient;
const NeoChatRoom *EventHandler::getRoom() const
{
return m_room;
}
void EventHandler::setRoom(const NeoChatRoom *room)
{
if (room == m_room) {
return;
}
m_room = room;
}
const Quotient::Event *EventHandler::getEvent() const
{
return m_event;
}
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (event == m_event) {
return;
}
m_event = event;
}
QString EventHandler::getId() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
return {};
}
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
}
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
{
if (auto e = eventCast<const RoomMessageEvent>(event)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(*event)) {
return DelegateType::Sticker;
}
if (event->isStateEvent()) {
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(*event)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(*event)) {
const auto pollEvent = eventCast<const PollStartEvent>(event);
if (pollEvent->isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return DelegateType::Other;
}
DelegateType::Type EventHandler::getDelegateType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
return getDelegateTypeForEvent(m_event);
}
QVariantMap EventHandler::getAuthor(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->getUser(author);
}
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 (previousDisplayName.isEmpty()) {
previousDisplayName = m_event->senderId();
}
return previousDisplayName;
} else {
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
return m_room->htmlSafeMemberName(author->id());
}
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime 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 {};
}
return isPending ? lastUpdated : m_event->originTimestamp();
}
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
return {};
}
auto ts = getTime(isPending, lastUpdated);
if (ts.isValid()) {
if (relative) {
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
}
return {};
}
bool EventHandler::isHighlighted()
{
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);
}
bool EventHandler::isHidden()
{
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return true;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return true;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return true;
}
}
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
return true;
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty()) {
return true;
}
}
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
return true;
}
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return true;
}
}
if (m_room->connection()->isIgnored(m_room->user(m_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()) {
return true;
}
return false;
}
QString EventHandler::getRichBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::RichText, stripNewlines);
}
QString EventHandler::getPlainBody(bool stripNewlines) const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
return {};
}
return getBody(m_event, Qt::PlainText, stripNewlines);
}
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
{
if (event->isRedacted()) {
auto reason = event->redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
}
const bool prettyPrint = (format == Qt::RichText);
return switchOnType(
*event,
[this, format, stripNewlines](const RoomMessageEvent &event) {
return getMessageBody(event, format, stripNewlines);
},
[](const StickerEvent &e) {
return e.body();
},
[this, prettyPrint](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = m_room->htmlSafeMemberName(e.userId());
if (e.membership() == Membership::Leave) {
if (e.prevContent() && e.prevContent()->displayName) {
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
}
}
if (prettyPrint) {
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
}
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
}
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");
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName);
} else {
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
}
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
},
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic")
: i18n("set the topic to: %1",
prettyPrint ? Quotient::prettyPrint(e.topic())
: stripNewlines ? e.topic().replace(u'\n', u' ')
: e.topic());
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.contentJson().isEmpty()) {
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
}
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
}
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return e.contentJson()["description"_ls].toString();
}
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
},
[](const PollStartEvent &e) {
return e.question();
},
i18n("Unknown event"));
}
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
{
TextHandler textHandler;
if (event.hasFileContent()) {
auto fileCaption = event.content()->fileInfo()->originalName;
if (fileCaption.isEmpty()) {
fileCaption = event.plainBody();
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
fileCaption = event.plainBody() + " | "_ls + fileCaption;
}
textHandler.setData(fileCaption);
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
}
QString body;
if (event.hasTextContent() && event.content()) {
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
} else {
body = event.plainBody();
}
textHandler.setData(body);
Qt::TextFormat inputFormat;
if (event.mimeType().name() == "text/plain"_ls) {
inputFormat = Qt::PlainText;
} else {
inputFormat = Qt::RichText;
}
if (format == Qt::RichText) {
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
} else {
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
}
}
QString EventHandler::getGenericBody() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
return {};
}
if (m_event->isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return switchOnType(
*m_event,
[](const RoomMessageEvent &e) {
Q_UNUSED(e)
return i18n("sent a message");
},
[](const StickerEvent &e) {
Q_UNUSED(e)
return i18n("sent a sticker");
},
[](const RoomMemberEvent &e) {
switch (e.membership()) {
case Membership::Invite:
if (e.repeatsState()) {
return i18n("reinvited someone to the room");
}
Q_FALLTHROUGH();
case Membership::Join: {
QString text{};
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
}
if (!text.isEmpty()) {
return text;
}
// Part 2: profile changes of joined members
if (e.isRename()) {
if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name");
} else {
text = i18nc("their refers to a singular user", "changed their display name");
}
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
if (!e.newAvatarUrl()) {
text += i18nc("their refers to a singular user", "cleared their avatar");
} else if (!e.prevContent()->avatarUrl) {
text += i18n("set an avatar");
} else {
text += i18nc("their refers to a singular user", "updated their avatar");
}
}
if (text.isEmpty()) {
text = i18nc("<user> changed nothing", "changed nothing");
}
return text;
}
case Membership::Leave:
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
}
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
}
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
case Membership::Ban:
if (e.senderId() != e.userId()) {
return i18n("banned a user from the room");
} else {
return i18n("self-banned from the room");
}
case Membership::Knock: {
return i18n("requested an invite");
}
default:;
}
return i18n("made something unknown");
},
[](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
},
[](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
},
[](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
},
[](const RoomAvatarEvent &) {
return i18n("changed the room avatar");
},
[](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption");
},
[](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
},
[](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room");
},
[](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room");
}
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
return i18n("added a widget");
}
if (e.contentJson().isEmpty()) {
return i18n("removed a widget");
}
return i18n("configured a widget");
}
return i18n("updated the state");
},
[](const PollStartEvent &e) {
Q_UNUSED(e);
return i18n("started a poll");
},
i18n("Unknown event"));
}
QVariantMap EventHandler::getMediaInfo() const
{
return getMediaInfoForEvent(m_event);
}
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
{
QString eventId = event->id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
if (event->is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
fileInfo = roomMessageEvent->content()->fileInfo();
} else if (event->is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(event);
fileInfo = &stickerEvent->image();
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
}
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
{
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration;
}
}
return mediaInfo;
}
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
QString text;
auto event = eventCast<const RoomMessageEvent>(m_event);
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
} else {
return nullptr;
}
}
QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
return nullptr;
}
auto eventId = m_event->id();
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
return nullptr;
};
QMap<QString, QList<User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId()));
}
}
if (reactions.isEmpty()) {
return nullptr;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_room->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (res.size() > 0) {
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
} else {
return nullptr;
}
}
bool EventHandler::hasReply() const
{
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::getReplyId() const
{
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
DelegateType::Type EventHandler::getReplyDelegateType() const
{
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return DelegateType::Other;
}
return getDelegateTypeForEvent(replyEvent);
}
QVariantMap EventHandler::getReplyAuthor() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
return {};
}
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
return m_room->getUser(nullptr);
}
auto replyPtr = m_room->getReplyForEvent(*m_event);
if (replyPtr) {
auto replyUser = m_room->user(replyPtr->senderId());
return m_room->getUser(replyUser);
} else {
return m_room->getUser(nullptr);
}
}
QString EventHandler::getReplyRichBody(bool stripNewlines) const
{
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);
}
float EventHandler::getLatitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
}
float EventHandler::getLongitude() const
{
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
}
QString EventHandler::getLocationAssetType() const
{
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
}
bool EventHandler::hasReadMarkers() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
}
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > maxMarkers) {
userIds = userIds.mid(0, maxMarkers);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
users += m_room->getUser(user);
}
return users;
}
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
if (userIds.count() > maxMarkers) {
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
} else {
return QString();
}
}
QString EventHandler::getReadMarkersString() const
{
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_room->user(userId);
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

400
src/eventhandler.h Normal file
View File

@@ -0,0 +1,400 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <KFormat>
#include <Quotient/eventitem.h>
#include <Quotient/events/roomevent.h>
#include <Quotient/events/roommessageevent.h>
#include "enums/delegatetype.h"
class LinkPreviewer;
class NeoChatRoom;
class ReactionModel;
/**
* @class EventHandler
*
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
* in a form ready for the NeoChat UI.
*
* To use this properly both the room and the event should be set (and the event should
* be from the given room).
*
* @note EventHandler will always try to return something even when not properly
* initialised, this is usually the best empty value it can create with available
* information. This is to minimize warnings from QML especially during startup
* and room changes.
*/
class EventHandler : public QObject
{
Q_OBJECT
public:
/**
* @brief Return the current room the EventHandler is using.
*/
const NeoChatRoom *getRoom() const;
/**
* @brief Set the current room the EventHandler to using.
*/
void setRoom(const NeoChatRoom *room);
/**
* @brief Return the current event the EventHandler is using.
*/
const Quotient::Event *getEvent() const;
/**
* @brief Set the current event the EventHandler to using.
*/
void setEvent(const Quotient::RoomEvent *event);
/**
* @brief Return the Matrix ID of the event.
*/
QString getId() const;
/**
* @brief Return the DelegateType of the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getDelegateType() const;
/**
* @brief Get the author of the event in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @param isPending if the event is pending, i.e. has not been confirmed by
* the server.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap getAuthor(bool isPending = false) const;
/**
* @brief Get the display name of the event author.
*
* This method is separate from getAuthor() and 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.
*
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @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 relative whether the string is realtive to the current date, i.e.
* Yesterday or Wednesday, etc.
* @param format the QLocale::FormatType to use.
* @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.
*/
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
/**
* @brief Whether the event should be highlighted in the timeline.
*
* @note Messages in direct chats are never highlighted.
*/
bool isHighlighted();
/**
* @brief Whether the event should be hidden in the timeline.
*
* This could be for numerous reasons, e.g. if it's a replacement event, if the
* user has hidden all state events or if the sender has been ignored by the local
* user.
*/
bool isHidden();
/**
* @brief Output a string for the message content 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 getRichBody(bool stripNewlines = false) const;
/**
* @brief Output a string for the message content 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 getPlainBody(bool stripNewlines = false) const;
/**
* @brief Output a generic string for the message content ready for display.
*
* The output string is dependant upon the event type.
*
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
* is the same for all events of the same type.
*
* E.g. For a message the text will be:
* "sent a message"
*
* @sa getRichBody(), getPlainBody()
*/
QString getGenericBody() const;
/**
* @brief Return the media info for the event.
*
* 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.
*/
QVariantMap getMediaInfo() const;
/**
* @brief Return a LinkPreviewer object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty LinkPreviewer provided if null.
*
* @sa LinkPreviewer
*/
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
/**
* @brief Return a ReactionModel object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty QVariantList (or other suitable
* empty mode) provided if null.
*/
QSharedPointer<ReactionModel> getReactions() const;
/**
* @brief Whether the event is a reply to another in the timeline.
*/
bool hasReply() const;
/**
* @brief Return the Matrix ID of the event replied to.
*/
QString getReplyId() const;
/**
* @brief Return the DelegateType of the event replied to.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
DelegateType::Type getReplyDelegateType() const;
/**
* @brief Get the author of the event replied to in context of the room.
*
* This is different to getting a Quotient::User object
* as neither of those can provide details like the displayName or avatarMediaId
* without the room context as these can vary from room to room. This function
* uses the room context and outputs the result as QVariantMap.
*
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
* but empty values) will be returned if the room has been set but not an event.
*
* @return a QVariantMap for the user with the following properties:
* - isLocalUser - Whether the user is the local user.
* - id - The matrix ID of the user.
* - displayName - Display name in the context of this room.
* - avatarSource - The mxc URL for the user's avatar in the current room.
* - avatarMediaId - Avatar id in the context of this room.
* - color - Color for the user.
* - object - The Quotient::User object for the user.
*
* @sa Quotient::User
*/
QVariantMap 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.
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Return the latitude for the event.
*
* 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;
/**
* @brief Return the longitude for the event.
*
* 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;
/**
* @brief Return the type of location marker for the event.
*/
QString getLocationAssetType() const;
/**
* @brief Whether the event has any read marker for other users.
*/
bool hasReadMarkers() const;
/**
* @brief Returns a list of user read marker for the event.
*
* @param maxMarkers the maximum number of users to return. Usually the number
* of user read makers shown is limited to not clutter the UI.
* This needs to be the same as used in getNumberExcessReadMarkers
* so that the markers line up with the number displayed, i.e.
* the number of users shown plus the excess number will be
* the total number of other user read markers at an event.
*/
QVariantList getReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns the number of excess user read markers for the event.
*
* This returns a string in the form "+ x" ready for use in the UI.
*
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
* be the same as used in getReadMarkers so that the value lines
* up with the number displayed, i.e. the number of users shown
* plus the excess number will be the total number of other user
* read markers at an event.
*/
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
/**
* @brief Returns a string with the names of the read markers at the event.
*
* This is in the form "x users: name 1, name 2, ...".
*/
QString getReadMarkersString() const;
private:
const NeoChatRoom *m_room = nullptr;
const Quotient::RoomEvent *m_event = nullptr;
KFormat m_format;
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
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) const;
};

116
src/filetype.cpp Normal file
View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
#include "filetype.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for (const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypePrivate
{
Q_DECLARE_PUBLIC(FileType)
Q_DISABLE_COPY(FileTypePrivate)
public:
FileTypePrivate(FileType *qq);
FileType *const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypePrivate::FileTypePrivate(FileType *qq)
: q_ptr(qq)
{
}
FileType::FileType(QObject *parent)
: QObject(parent)
, d_ptr(new FileTypePrivate(this))
{
}
FileType::~FileType() noexcept
{
}
QMimeType FileType::mimeTypeForName(const QString &nameOrAlias) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileType::mimeTypeForFile(const QString &fileName, MatchMode mode) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileType::mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileType::mimeTypesForFileName(const QString &fileName) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileType::mimeTypeForData(const QByteArray &data) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileType::mimeTypeForData(QIODevice *device) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileType::mimeTypeForUrl(const QUrl &url) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileType::mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileType::mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const
{
Q_D(const FileType);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileType::suffixForFileName(const QString &fileName) const
{
Q_D(const FileType);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileType::supportedImageFormats() const
{
Q_D(const FileType);
return d->supportedImageFormats;
}
QStringList FileType::supportedAnimatedImageFormats() const
{
Q_D(const FileType);
return d->supportedAnimatedImageFormats;
}
#include "moc_filetype.cpp"

View File

@@ -8,9 +8,10 @@
#include <QFileInfo>
#include <QMimeDatabase>
#include <QObject>
#include <QQmlEngine>
#include <qqml.h>
class FileTypeSingletonPrivate;
class FileTypePrivate;
/**
* @class FileTypeSingleton
@@ -19,9 +20,11 @@ class FileTypeSingletonPrivate;
*
* @sa QMimeDatabase
*/
class FileTypeSingleton : public QObject
class FileType : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief List of supported image formats.
@@ -37,12 +40,9 @@ class FileTypeSingleton : public QObject
*/
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
QML_NAMED_ELEMENT(FileType)
QML_SINGLETON
public:
explicit FileTypeSingleton(QObject *parent = nullptr);
~FileTypeSingleton();
explicit FileType(QObject *parent = nullptr);
~FileType();
/**
* @brief Returns a MIME type for nameOrAlias or an invalid one if none found.
@@ -59,14 +59,14 @@ public:
*
* @sa QMimeDatabase
*/
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, FileTypeSingleton::MatchMode mode = MatchDefault) const;
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, FileType::MatchMode mode = MatchDefault) const;
/**
* @brief Returns a MIME type for fileInfo.
*
* @sa QMimeDatabase
*/
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, FileTypeSingleton::MatchMode mode = MatchDefault) const;
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, FileType::MatchMode mode = MatchDefault) const;
/**
* @brief Returns the MIME types for the file name fileName.
@@ -121,9 +121,7 @@ public:
QStringList supportedAnimatedImageFormats() const;
private:
const QScopedPointer<FileTypeSingletonPrivate> d_ptr;
Q_DECLARE_PRIVATE(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingleton)
const QScopedPointer<FileTypePrivate> d_ptr;
Q_DECLARE_PRIVATE(FileType)
Q_DISABLE_COPY(FileType)
};
QML_DECLARE_TYPE(FileTypeSingleton)

View File

@@ -1,116 +0,0 @@
// SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
#include "filetypesingleton.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for (const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypeSingletonPrivate
{
Q_DECLARE_PUBLIC(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingletonPrivate)
public:
FileTypeSingletonPrivate(FileTypeSingleton *qq);
FileTypeSingleton *const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypeSingletonPrivate::FileTypeSingletonPrivate(FileTypeSingleton *qq)
: q_ptr(qq)
{
}
FileTypeSingleton::FileTypeSingleton(QObject *parent)
: QObject(parent)
, d_ptr(new FileTypeSingletonPrivate(this))
{
}
FileTypeSingleton::~FileTypeSingleton() noexcept
{
}
QMimeType FileTypeSingleton::mimeTypeForName(const QString &nameOrAlias) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QString &fileName, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileTypeSingleton::mimeTypesForFileName(const QString &fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileTypeSingleton::mimeTypeForData(const QByteArray &data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileTypeSingleton::mimeTypeForData(QIODevice *device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileTypeSingleton::mimeTypeForUrl(const QUrl &url) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileTypeSingleton::suffixForFileName(const QString &fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileTypeSingleton::supportedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedImageFormats;
}
QStringList FileTypeSingleton::supportedAnimatedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedAnimatedImageFormats;
}
#include "moc_filetypesingleton.cpp"

View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatchangepasswordjob.h"
using namespace Quotient;
NeochatChangePasswordJob::NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), "/_matrix/client/r0/account/password")
{
QJsonObject _data;
addParam<>(_data, QStringLiteral("new_password"), newPassword);
addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices);
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(_data);
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatChangePasswordJob : public Quotient::BaseJob
{
public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeactivateaccountjob.h"
using namespace Quotient;
NeoChatDeactivateAccountJob::NeoChatDeactivateAccountJob(const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Post, QStringLiteral("DisableDeviceJob"), "_matrix/client/v3/account/deactivate")
{
QJsonObject data;
addParam<IfNotEmpty>(data, QStringLiteral("auth"), auth);
setRequestData(data);
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeoChatDeactivateAccountJob : public Quotient::BaseJob
{
public:
explicit NeoChatDeactivateAccountJob(const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatdeletedevicejob.h"
using namespace Quotient;
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId).toLatin1())
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
#include <Quotient/omittable.h>
class NeochatDeleteDeviceJob : public Quotient::BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Quotient::Omittable<QJsonObject> &auth = Quotient::none);
};

View File

@@ -13,7 +13,7 @@
using namespace Quotient;
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
: QObject(parent)
, m_currentRoom(room)
, m_loaded(false)
@@ -93,4 +93,9 @@ void LinkPreviewer::loadUrlPreview()
}
}
bool LinkPreviewer::empty() const
{
return m_url.isEmpty();
}
#include "moc_linkpreviewer.cpp"

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
class NeoChatRoom;
@@ -19,6 +20,8 @@ class NeoChatRoom;
class LinkPreviewer : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The URL to get the preview for.
*/
@@ -44,8 +47,15 @@ class LinkPreviewer : public QObject
*/
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
/**
* @brief Whether the there is a link to preview.
*
* A linkPreviwer is empty if the URL is empty.
*/
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public:
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
[[nodiscard]] QUrl url() const;
void setUrl(QUrl);
@@ -53,9 +63,10 @@ public:
[[nodiscard]] QString title() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QUrl imageSource() const;
[[nodiscard]] bool empty() const;
private:
NeoChatRoom *m_currentRoom = nullptr;
const NeoChatRoom *m_currentRoom = nullptr;
bool m_loaded;
QString m_title = QString();
@@ -71,5 +82,6 @@ Q_SIGNALS:
void descriptionChanged();
void imageSourceChanged();
void urlChanged();
void emptyChanged();
};
Q_DECLARE_METATYPE(LinkPreviewer *)

View File

@@ -2,14 +2,17 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "linkpreviewer.h"
#include <QMetaType>
#include <QObject>
#include <QQmlEngine>
#include <QRectF>
/** Location related helper functions for QML. */
class LocationHelper
class LocationHelper : public QObject
{
Q_GADGET
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);

View File

@@ -13,13 +13,13 @@
using namespace Quotient;
Login::Login(QObject *parent)
LoginHelper::LoginHelper(QObject *parent)
: QObject(parent)
{
init();
}
void Login::init()
void LoginHelper::init()
{
m_homeserverReachable = false;
m_connection = new NeoChatConnection();
@@ -31,7 +31,7 @@ void Login::init()
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &Login::matrixIdChanged, this, [this]() {
connect(this, &LoginHelper::matrixIdChanged, this, [this]() {
setHomeserverReachable(false);
QRegularExpression validator(QStringLiteral("^\\@?[a-zA-Z0-9\\._=\\-/]+\\:[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*(\\:[0-9]+)?$"));
if (!validator.match(m_matrixId).hasMatch()) {
@@ -105,23 +105,23 @@ void Login::init()
});
}
void Login::setHomeserverReachable(bool reachable)
void LoginHelper::setHomeserverReachable(bool reachable)
{
m_homeserverReachable = reachable;
Q_EMIT homeserverReachableChanged();
}
bool Login::homeserverReachable() const
bool LoginHelper::homeserverReachable() const
{
return m_homeserverReachable;
}
QString Login::matrixId() const
QString LoginHelper::matrixId() const
{
return m_matrixId;
}
void Login::setMatrixId(const QString &matrixId)
void LoginHelper::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if (!m_matrixId.startsWith(QLatin1Char('@'))) {
@@ -130,30 +130,30 @@ void Login::setMatrixId(const QString &matrixId)
Q_EMIT matrixIdChanged();
}
QString Login::password() const
QString LoginHelper::password() const
{
return m_password;
}
void Login::setPassword(const QString &password)
void LoginHelper::setPassword(const QString &password)
{
setInvalidPassword(false);
m_password = password;
Q_EMIT passwordChanged();
}
QString Login::deviceName() const
QString LoginHelper::deviceName() const
{
return m_deviceName;
}
void Login::setDeviceName(const QString &deviceName)
void LoginHelper::setDeviceName(const QString &deviceName)
{
m_deviceName = deviceName;
Q_EMIT deviceNameChanged();
}
void Login::login()
void LoginHelper::login()
{
m_isLoggingIn = true;
Q_EMIT isLoggingInChanged();
@@ -164,22 +164,22 @@ void Login::login()
m_connection->loginWithPassword(username, m_password, m_deviceName, QString());
}
bool Login::supportsPassword() const
bool LoginHelper::supportsPassword() const
{
return m_supportsPassword;
}
bool Login::supportsSso() const
bool LoginHelper::supportsSso() const
{
return m_supportsSso;
}
QUrl Login::ssoUrl() const
QUrl LoginHelper::ssoUrl() const
{
return m_ssoUrl;
}
void Login::loginWithSso()
void LoginHelper::loginWithSso()
{
m_connection->resolveServer(m_matrixId);
connectSingleShot(m_connection, &Connection::loginFlowsChanged, this, [this]() {
@@ -189,28 +189,28 @@ void Login::loginWithSso()
});
}
bool Login::testing() const
bool LoginHelper::testing() const
{
return m_testing;
}
bool Login::isLoggingIn() const
bool LoginHelper::isLoggingIn() const
{
return m_isLoggingIn;
}
bool Login::isLoggedIn() const
bool LoginHelper::isLoggedIn() const
{
return m_isLoggedIn;
}
void Login::setInvalidPassword(bool invalid)
void LoginHelper::setInvalidPassword(bool invalid)
{
m_invalidPassword = invalid;
Q_EMIT isInvalidPasswordChanged();
}
bool Login::isInvalidPassword() const
bool LoginHelper::isInvalidPassword() const
{
return m_invalidPassword;
}

View File

@@ -4,18 +4,21 @@
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
class NeoChatConnection;
/**
* @class Login
* @class LoginHelper
*
* A helper class for logging into a Matrix account.
*/
class Login : public QObject
class LoginHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Whether the home server for the account is reachable.
@@ -76,7 +79,7 @@ class Login : public QObject
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit Login(QObject *parent = nullptr);
explicit LoginHelper(QObject *parent = nullptr);
Q_INVOKABLE void init();

View File

@@ -34,72 +34,25 @@
#include "neochat-version.h"
#include <Quotient/accountregistry.h>
#include <Quotient/keyverificationsession.h>
#include <Quotient/networkaccessmanager.h>
#include <Quotient/room.h>
#include <Quotient/user.h>
#include <Quotient/util.h>
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatdocumenthandler.h"
#include "clipboard.h"
#include "controller.h"
#include "delegatesizehelper.h"
#include "filetypesingleton.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "models/accountemoticonmodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/imagepacksmodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/reactionmodel.h"
#include "models/roomlistmodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/sortfilterroomlistmodel.h"
#include "models/sortfilterspacelistmodel.h"
#include "models/statefiltermodel.h"
#include "models/stickermodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/userlistmodel.h"
#include "models/webshortcutmodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "pollhandler.h"
#include "roommanager.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "windowcontroller.h"
#ifdef HAVE_COLORSCHEME
#include "colorschemer.h"
#endif
#include "models/completionmodel.h"
#include "models/statemodel.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -107,6 +60,8 @@
using namespace Quotient;
void qml_register_types_org_kde_neochat();
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
QNetworkAccessManager *create(QObject *) override
@@ -138,14 +93,12 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif
#ifdef Q_OS_ANDROID
@@ -215,98 +168,18 @@ int main(int argc, char *argv[])
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
#endif
Clipboard clipboard;
auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
Login *login = new Login();
UrlHelper urlHelper;
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
qmlRegisterSingletonInstance<ColorSchemer>("org.kde.neochat", 1, 0, "ColorSchemer", &colorScheme);
if (!config->colorScheme().isEmpty()) {
colorScheme.apply(config->colorScheme());
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
#endif
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "NotificationsManager", &NotificationsManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", &EmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<ReactionModel>("org.kde.neochat", 1, 0, "ReactionModel");
qmlRegisterType<MediaMessageFilterModel>("org.kde.neochat", 1, 0, "MediaMessageFilterModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<UserFilterModel>("org.kde.neochat", 1, 0, "UserFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<ServerListModel>("org.kde.neochat", 1, 0, "ServerListModel");
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
qmlRegisterType<DevicesProxyModel>("org.kde.neochat", 1, 0, "DevicesProxyModel");
qmlRegisterType<LinkPreviewer>("org.kde.neochat", 1, 0, "LinkPreviewer");
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<StateFilterModel>("org.kde.neochat", 1, 0, "StateFilterModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterType<LiveLocationsModel>("org.kde.neochat", 1, 0, "LiveLocationsModel");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
qmlRegisterType<PushRuleModel>("org.kde.neochat", 1, 0, "PushRuleModel");
qmlRegisterType<StickerModel>("org.kde.neochat", 1, 0, "StickerModel");
qmlRegisterType<ImagePacksModel>("org.kde.neochat", 1, 0, "ImagePacksModel");
qmlRegisterType<AccountEmoticonModel>("org.kde.neochat", 1, 0, "AccountEmoticonModel");
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
qmlRegisterUncreatableType<PushNotificationAction>("org.kde.neochat", 1, 0, "PushNotificationAction", "ENUM"_ls);
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM"_ls);
qmlRegisterUncreatableType<User>("org.kde.neochat", 1, 0, "User", {});
qmlRegisterUncreatableType<NeoChatRoom>("org.kde.neochat", 1, 0, "NeoChatRoom", {});
qmlRegisterUncreatableType<NeoChatConnection>("org.kde.neochat", 1, 0, "NeoChatConnection", {});
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
qRegisterMetaType<User *>("User*");
qRegisterMetaType<User *>("const User*");
qRegisterMetaType<User *>("const Quotient::User*");
qRegisterMetaType<Room *>("Room*");
qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<User *>("User*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
qRegisterMetaType<KeyVerificationSession *>("KeyVerificationSession*");
qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qRegisterMetaType<QVector<EmojiEntry>>("QVector<EmojiEntry>");
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(KAboutData::applicationData());
});
qmlRegisterSingletonType(QUrl("qrc:/OsmLocationPlugin.qml"_ls), "org.kde.neochat", 1, 0, "OsmLocationPlugin");
qmlRegisterSingletonType("org.kde.neochat", 1, 0, "LocationHelper", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
return engine->toScriptValue(LocationHelper());
});
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Emoji>();
#endif
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
QQmlApplicationEngine engine;
@@ -357,12 +230,12 @@ int main(int argc, char *argv[])
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
if (engine.rootObjects().isEmpty()) {
return -1;
}
if (parser.positionalArguments().length() > 0) {
if (!parser.positionalArguments().isEmpty()) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}

View File

@@ -10,11 +10,6 @@
#include <QReadWriteLock>
namespace Quotient
{
class Connection;
}
/**
* @class ThumbnailResponse
*

163
src/mediasizehelper.cpp Normal file
View File

@@ -0,0 +1,163 @@
// 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 "mediasizehelper.h"
#include "neochatconfig.h"
MediaSizeHelper::MediaSizeHelper(QObject *parent)
: QObject(parent)
{
}
qreal MediaSizeHelper::contentMaxWidth() const
{
return m_contentMaxWidth;
}
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
{
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
m_contentMaxWidth = -1.0;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
return;
}
m_contentMaxWidth = contentMaxWidth;
Q_EMIT contentMaxWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::contentMaxHeight() const
{
return m_contentMaxHeight;
}
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
{
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
m_contentMaxHeight = -1.0;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
return;
}
m_contentMaxHeight = contentMaxHeight;
Q_EMIT contentMaxHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaWidth() const
{
return m_mediaWidth;
}
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
{
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
m_mediaWidth = -1.0;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
return;
}
m_mediaWidth = mediaWidth;
Q_EMIT mediaWidthChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::mediaHeight() const
{
return m_mediaHeight;
}
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
{
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
m_mediaHeight = -1.0;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
return;
}
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
return;
}
m_mediaHeight = mediaHeight;
Q_EMIT mediaHeightChanged();
Q_EMIT currentSizeChanged();
}
qreal MediaSizeHelper::resolvedMediaWidth() const
{
if (m_mediaWidth > 0.0) {
return m_mediaWidth;
}
return widthLimit();
}
qreal MediaSizeHelper::resolvedMediaHeight() const
{
if (m_mediaHeight > 0.0) {
return m_mediaHeight;
}
return widthLimit() / 16.0 * 9.0;
}
qreal MediaSizeHelper::aspectRatio() const
{
return resolvedMediaWidth() / resolvedMediaHeight();
}
bool MediaSizeHelper::limitWidth() const
{
// If actual data isn't available we'll be using a placeholder that is width
// limited so return true.
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
return true;
}
return m_mediaWidth >= m_mediaHeight;
}
qreal MediaSizeHelper::widthLimit() const
{
if (m_contentMaxWidth < 0.0) {
return NeoChatConfig::self()->mediaMaxWidth();
}
return std::min(m_contentMaxWidth, qreal(NeoChatConfig::self()->mediaMaxWidth()));
}
qreal MediaSizeHelper::heightLimit() const
{
if (m_contentMaxHeight < 0.0) {
return NeoChatConfig::self()->mediaMaxHeight();
}
return std::min(m_contentMaxHeight, qreal(NeoChatConfig::self()->mediaMaxHeight()));
}
QSize MediaSizeHelper::currentSize() const
{
if (limitWidth()) {
qreal width = std::min(widthLimit(), resolvedMediaWidth());
qreal height = width / aspectRatio();
if (height > heightLimit()) {
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
}
return QSize(qRound(width), qRound(height));
} else {
qreal height = std::min(heightLimit(), resolvedMediaHeight());
qreal width = height * aspectRatio();
if (width > widthLimit()) {
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
}
return QSize(qRound(width), qRound(height));
}
}
#include "moc_mediasizehelper.cpp"

105
src/mediasizehelper.h Normal file
View File

@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QSize>
/**
* @class MediaSizeHelper
*
* A class to help calculate the current width of a media item within a chat delegate.
*
* The only realistic way to guarantee that a media item (e.g. an image or video)
* is the correct size in QML is to calculate the size manually.
*
* The rules for this component work as follows:
* - The output will always try to keep the media size if no limits are breached.
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
* calcualated from either the configured max width or the contentMaxWidth, whichever
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
* - The current size will never be larger than any of the limits in either direction.
* - If any limit is breached the image size will be reduced while maintaining aspect
* ration, i.e. no stretching or squashing. This can mean that the width or height
* is reduced even if that parameter doesn't breach the limit itself.
*/
class MediaSizeHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The maximum width (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
/**
* @brief The maximum height (in px) the media can be.
*
* This is the upper limit placed upon the media by the delegate.
*/
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
/**
* @brief The base width (in px) of the media.
*/
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
/**
* @brief The base height (in px) of the media.
*/
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
/**
* @brief The size (in px) of the component based on the current input.
*
* Will always try to return a value even if some of the inputs are not set to
* account for being called before the parameters are intialised. For any parameters
* not set these will just be left out of the calcs.
*
* If no input values are provided a default placeholder value will be returned.
*/
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
public:
explicit MediaSizeHelper(QObject *parent = nullptr);
qreal contentMaxWidth() const;
void setContentMaxWidth(qreal contentMaxWidth);
qreal contentMaxHeight() const;
void setContentMaxHeight(qreal contentMaxHeight);
qreal mediaWidth() const;
void setMediaWidth(qreal mediaWidth);
qreal mediaHeight() const;
void setMediaHeight(qreal mediaHeight);
QSize currentSize() const;
Q_SIGNALS:
void contentMaxWidthChanged();
void contentMaxHeightChanged();
void mediaWidthChanged();
void mediaHeightChanged();
void currentSizeChanged();
private:
qreal m_contentMaxWidth = -1.0;
qreal m_contentMaxHeight = -1.0;
qreal m_mediaWidth = -1.0;
qreal m_mediaHeight = -1.0;
qreal resolvedMediaWidth() const;
qreal resolvedMediaHeight() const;
qreal aspectRatio() const;
bool limitWidth() const;
qreal widthLimit() const;
qreal heightLimit() const;
};

View File

@@ -9,12 +9,11 @@
#include <QCoroTask>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
#include <Quotient/connection.h>
class ImagePacksModel;
/**
* @class AccountEmoticonModel
*
@@ -25,6 +24,8 @@ class ImagePacksModel;
class AccountEmoticonModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The connection to get emoticons from.
*/

View File

@@ -87,8 +87,11 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
if (role == IconNameRole) {
return QStringLiteral("mxc://%1")
.arg(m_roomListModel->connection()->makeMediaUrl(m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toUrl()).toString());
auto mediaId = m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
if (mediaId.isEmpty()) {
return QVariant();
}
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
}
}
if (m_autoCompletionType == Emoji) {

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include <QSortFilterProxyModel>
#include "roomlistmodel.h"
@@ -24,6 +25,7 @@ class RoomListModel;
class CompletionModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current text to search for completions.

View File

@@ -22,11 +22,7 @@ bool CompletionProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &so
&& sourceModel()
->data(sourceModel()->index(sourceRow, 0), secondaryFilterRole())
.toString()
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
.startsWith(QStringView(m_filterText).sliced(1), Qt::CaseInsensitive));
#else
.startsWith(m_filterText.midRef(1), Qt::CaseInsensitive));
#endif
}
bool CompletionProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const

View File

@@ -4,6 +4,7 @@
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QRegularExpression>
#include <memory>
@@ -27,6 +28,8 @@ struct CustomEmoji {
class CustomEmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
/**
@@ -48,6 +51,11 @@ public:
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Get the given role value at the given index.

View File

@@ -4,6 +4,7 @@
#include "devicesmodel.h"
#include "controller.h"
#include "jobs/neochatdeletedevicejob.h"
#include <QDateTime>
#include <QLocale>

View File

@@ -5,6 +5,7 @@
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <Quotient/csapi/definitions/client_device.h>
@@ -25,6 +26,7 @@ class Connection;
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current connection that the model is getting its devices from.

View File

@@ -3,11 +3,14 @@
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
class DevicesProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int type READ type WRITE setType NOTIFY typeChanged)
public:

View File

@@ -5,6 +5,7 @@
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
#include <QSettings>
struct Emoji {
@@ -59,6 +60,8 @@ Q_DECLARE_METATYPE(Emoji)
class EmojiModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of recently used emojis.
@@ -83,6 +86,11 @@ public:
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
/**
* @brief Defines the model roles.

View File

@@ -3,6 +3,7 @@
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
@@ -14,6 +15,7 @@
class EmoticonFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether stickers should be shown

View File

@@ -6,6 +6,7 @@
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
class NeoChatRoom;
@@ -21,6 +22,7 @@ class NeoChatRoom;
class ImagePacksModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current room that the model is being used in.

View File

@@ -8,13 +8,9 @@
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QRectF>
namespace Quotient
{
class RoomMessageEvent;
}
struct LiveLocationData {
QString eventId;
QString senderId;
@@ -29,6 +25,8 @@ bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs);
class LiveLocationsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatRoom *room MEMBER m_room NOTIFY roomChanged)
/** The event id of the beacon start event, ie. the one all suspequent
* events use to relate to the same beacon.

View File

@@ -5,6 +5,7 @@
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QRectF>
#include "neochatroom.h"
@@ -15,6 +16,7 @@
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
enum Roles {

View File

@@ -2,20 +2,26 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "mediamessagefiltermodel.h"
#include "models/messageeventmodel.h"
#include <Quotient/room.h>
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent)
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
MediaMessageFilterModel::MediaMessageFilterModel(QObject *parent, MessageFilterModel *sourceMediaModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMediaModel);
setSourceModel(sourceMediaModel);
}
bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
return true;
}
return false;
@@ -24,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
{
if (role == SourceRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
if (progressInfo.completed()) {
@@ -42,10 +48,10 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
}
if (role == TypeRole) {
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
return 0;
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
return MediaType::Image;
} else {
return 1;
return MediaType::Video;
}
}
if (role == CaptionRole) {
@@ -57,6 +63,13 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
if (role == SourceHeightRole) {
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("height")].toFloat();
}
// We need to catch this one and return true if the next media object was
// on a different day.
if (role == MessageEventModel::ShowSectionRole) {
const auto day = mapToSource(index).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay;
}
return sourceModel()->data(mapToSource(index), role);
}

View File

@@ -3,10 +3,13 @@
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
#include "models/messagefiltermodel.h"
class MessageFilterModel;
/**
* @class MediaMessageFilterModel
*
@@ -17,7 +20,15 @@
class MediaMessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
public:
enum MediaType {
Image = 0,
Video,
};
Q_ENUM(MediaType)
/**
* @brief Defines the model roles.
*/
@@ -31,7 +42,7 @@ public:
};
Q_ENUM(Roles)
explicit MediaMessageFilterModel(QObject *parent = nullptr);
explicit MediaMessageFilterModel(QObject *parent = nullptr, MessageFilterModel *sourceMediaModel = nullptr);
/**
* @brief Custom filter to show only image and video messages.
@@ -43,7 +54,7 @@ public:
*
* @sa QSortFilterProxyModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns a mapping from Role enum values to role names.

View File

@@ -8,25 +8,19 @@
#include <Quotient/connection.h>
#include <Quotient/csapi/rooms.h>
#include <Quotient/events/reactionevent.h>
#include <Quotient/events/redactionevent.h>
#include <Quotient/events/roomavatarevent.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/events/simplestateevents.h>
#include <Quotient/user.h>
#include "events/pollevent.h"
#include <Quotient/events/stickerevent.h>
#include <Quotient/user.h>
#include <QDebug>
#include <QGuiApplication>
#include <QMovie>
#include <QTimeZone>
#include <KLocalizedString>
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "models/reactionmodel.h"
#include "texthandler.h"
using namespace Quotient;
@@ -37,6 +31,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[PlainText] = "plainText";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[SectionRole] = "section";
roles[AuthorRole] = "author";
roles[ContentRole] = "content";
@@ -48,8 +43,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[MediaInfoRole] = "mediaInfo";
roles[IsReplyRole] = "isReply";
roles[ReplyAuthor] = "replyAuthor";
roles[ReplyRole] = "reply";
roles[ReplyIdRole] = "replyId";
roles[ReplyDelegateTypeRole] = "replyDelegateType";
roles[ReplyDisplayRole] = "replyDisplay";
roles[ReplyMediaInfoRole] = "replyMediaInfo";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
@@ -59,11 +55,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowReadMarkersRole] = "showReadMarkers";
roles[ReactionRole] = "reaction";
roles[ShowReactionsRole] = "showReactions";
roles[SourceRole] = "jsonSource";
roles[MimeTypeRole] = "mimeType";
roles[AuthorIdRole] = "authorId";
roles[VerifiedRole] = "verified";
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
roles[AuthorDisplayNameRole] = "authorDisplayName";
roles[IsRedactedRole] = "isRedacted";
roles[GenericDisplayRole] = "genericDisplay";
@@ -97,7 +89,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (m_currentRoom) {
m_currentRoom->disconnect(this);
m_linkPreviewers.clear();
qDeleteAll(m_reactionModels);
m_reactionModels.clear();
}
@@ -107,10 +98,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
room->setDisplayed();
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
createLinkPreviewerForEvent(e);
createReactionModelForEvent(e);
}
createEventObjects(&*event->viewAs<RoomMessageEvent>());
}
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
@@ -123,16 +111,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (row == -1) {
return;
}
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
});
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
if (message != nullptr) {
createLinkPreviewerForEvent(message);
createReactionModelForEvent(message);
createEventObjects(message);
if (message != nullptr) {
if (NeoChatConfig::self()->showFancyEffects()) {
QString planBody = message->plainBody();
// snowflake
@@ -169,10 +157,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
if (message) {
createLinkPreviewerForEvent(message);
createReactionModelForEvent(message);
}
createEventObjects(message);
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
@@ -241,7 +226,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
const auto eventIt = m_currentRoom->findInTimeline(eventId);
if (eventIt != m_currentRoom->historyEdge()) {
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
}
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
});
@@ -340,7 +325,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
return -1;
}
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) {
row++;
}
}
@@ -458,6 +443,10 @@ 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;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(&evt);
if (role == Qt::DisplayRole) {
if (evt.isRedacted()) {
auto reason = evt.redactedBecause()->reason();
@@ -465,74 +454,23 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return m_currentRoom->eventToString(evt, Qt::RichText);
return eventHandler.getRichBody();
}
if (role == GenericDisplayRole) {
if (evt.isRedacted()) {
return i18n("<i>[This message was deleted]</i>");
}
return m_currentRoom->eventToGenericString(evt);
return eventHandler.getGenericBody();
}
if (role == PlainText) {
return m_currentRoom->eventToString(evt);
}
if (role == SourceRole) {
return QJsonDocument(evt.fullJson()).toJson();
return eventHandler.getPlainBody();
}
if (role == DelegateTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
return DelegateType::Emote;
case MessageEventType::Notice:
return DelegateType::Notice;
case MessageEventType::Image:
return DelegateType::Image;
case MessageEventType::Audio:
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
if (e->hasFileContent()) {
return DelegateType::File;
}
return DelegateType::Message;
}
if (is<const StickerEvent>(evt)) {
return DelegateType::Sticker;
}
if (evt.isStateEvent()) {
if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
return DelegateType::LiveLocation;
}
return DelegateType::State;
}
if (is<const EncryptedEvent>(evt)) {
return DelegateType::Encrypted;
}
if (is<PollStartEvent>(evt)) {
if (evt.isRedacted()) {
return DelegateType::Message;
}
return DelegateType::Poll;
}
return DelegateType::Other;
return eventHandler.getDelegateType();
}
if (role == AuthorRole) {
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->getUser(author);
return eventHandler.getAuthor(isPending);
}
if (role == ContentRole) {
@@ -558,21 +496,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == HighlightRole) {
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
}
if (role == MimeTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e || !e->hasFileContent()) {
return QVariant();
}
return e->content()->fileInfo()->mimeType.name();
}
if (auto e = eventCast<const StickerEvent>(&evt)) {
return e->image().mimeType.name();
}
return eventHandler.isHighlighted();
}
if (role == SpecialMarksRole) {
@@ -585,46 +509,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return pendingIt->deliveryStatus();
}
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return EventStatus::Hidden;
}
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
return EventStatus::Hidden;
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
&& !NeoChatConfig::self()->showAvatarUpdate()) {
return EventStatus::Hidden;
}
}
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(&evt))
if (!e->replacedEvent().isEmpty())
return EventStatus::Hidden;
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
return EventStatus::Hidden;
}
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
return EventStatus::Hidden;
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
return EventStatus::Hidden;
}
}
if (m_currentRoom->connection()->isIgnored(m_currentRoom->user(evt.senderId()))) {
return EventStatus::Hidden;
}
// hide ending live location beacons
if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) {
if (eventHandler.isHidden()) {
return EventStatus::Hidden;
}
@@ -632,7 +517,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == EventIdRole) {
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
return eventHandler.getId();
}
if (role == ProgressInfoRole) {
@@ -646,9 +531,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
}
if (role == TimeRole || role == SectionRole) {
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat);
if (role == TimeRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTime(isPending, lastUpdated);
}
if (role == TimeStringRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == SectionRole) {
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
}
if (role == ShowLinkPreviewRole) {
@@ -657,85 +552,38 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
if (role == LinkPreviewRole) {
if (m_linkPreviewers.contains(evt.id())) {
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]);
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()].data());
} else {
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
}
}
if (role == MediaInfoRole) {
return getMediaInfoForEvent(evt);
return eventHandler.getMediaInfo();
}
if (role == IsReplyRole) {
return !evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
return eventHandler.hasReply();
}
if (role == ReplyIdRole) {
return evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
return eventHandler.getReplyId();
}
if (role == ReplyDelegateTypeRole) {
return eventHandler.getReplyDelegateType();
}
if (role == ReplyAuthor) {
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
return eventHandler.getReplyAuthor();
}
if (replyPtr) {
auto replyUser = m_currentRoom->user(replyPtr->senderId());
return m_currentRoom->getUser(replyUser);
} else {
return m_currentRoom->getUser(nullptr);
}
if (role == ReplyDisplayRole) {
return eventHandler.getReplyRichBody();
}
if (role == ReplyMediaInfoRole) {
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (!replyPtr) {
return {};
}
return getMediaInfoForEvent(*replyPtr);
}
if (role == ReplyRole) {
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
if (!replyPtr) {
return {};
}
DelegateType type;
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
type = DelegateType::Emote;
break;
case MessageEventType::Notice:
type = DelegateType::Notice;
break;
case MessageEventType::Image:
type = DelegateType::Image;
break;
case MessageEventType::Audio:
type = DelegateType::Audio;
break;
case MessageEventType::Video:
type = DelegateType::Video;
break;
default:
if (e->hasFileContent()) {
type = DelegateType::File;
break;
}
type = DelegateType::Message;
}
} else if (is<const StickerEvent>(*replyPtr)) {
type = DelegateType::Sticker;
} else {
type = DelegateType::Other;
}
return QVariantMap{
{"display"_ls, m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
{"type"_ls, type},
};
return eventHandler.getReplyMediaInfo();
}
if (role == ShowAuthorRole) {
@@ -745,7 +593,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == DelegateType::State
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
}
@@ -771,87 +619,36 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (role == LatitudeRole) {
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
return latitude.toFloat();
return eventHandler.getLatitude();
}
if (role == LongitudeRole) {
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
if (geoUri.isEmpty()) {
return {};
}
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
return latitude.toFloat();
return eventHandler.getLongitude();
}
if (role == AssetRole) {
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
}
return assetType;
return eventHandler.getLocationAssetType();
}
if (role == ReadMarkersRole) {
auto userIds_temp = room()->userIdsAtEvent(evt.id());
userIds_temp.remove(m_currentRoom->localUser()->id());
auto userIds = userIds_temp.values();
if (userIds.count() > 5) {
userIds = userIds.mid(0, 5);
}
QVariantList users;
users.reserve(userIds.size());
for (const auto &userId : userIds) {
auto user = m_currentRoom->user(userId);
users += m_currentRoom->getUser(user);
}
return users;
return eventHandler.getReadMarkers();
}
if (role == ExcessReadMarkersRole) {
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
if (userIds.count() > 5) {
return QStringLiteral("+ %1").arg(userIds.count() - 5);
} else {
return QString();
}
return eventHandler.getNumberExcessReadMarkers();
}
if (role == ReadMarkersStringRole) {
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
/**
* The string ends up in the form
* "x users: user1DisplayName, user2DisplayName, etc."
*/
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
for (const auto &userId : userIds) {
auto user = m_currentRoom->user(userId);
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
}
readMarkersString.chop(2);
return readMarkersString;
return eventHandler.getReadMarkersString();
}
if (role == ShowReadMarkersRole) {
auto userIds = room()->userIdsAtEvent(evt.id());
userIds.remove(m_currentRoom->localUser()->id());
return userIds.size() > 0;
return eventHandler.hasReadMarkers();
}
if (role == ReactionRole) {
if (m_reactionModels.contains(evt.id())) {
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()].data());
} else {
return QVariantList();
}
@@ -861,10 +658,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_reactionModels.contains(evt.id());
}
if (role == AuthorIdRole) {
return evt.senderId();
}
if (role == VerifiedRole) {
if (evt.originalEvent()) {
auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent());
@@ -874,22 +667,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return false;
}
if (role == DisplayNameForInitialsRole) {
auto user = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id()));
}
if (role == AuthorDisplayNameRole) {
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"_ls]["displayname"_ls].isNull() && evt.stateKey() == evt.senderId()) {
auto previousDisplayName = evt.unsignedJson()["prev_content"_ls]["displayname"_ls].toString().toHtmlEscaped();
if (previousDisplayName.isEmpty()) {
previousDisplayName = evt.senderId();
}
return previousDisplayName;
} else {
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
return m_currentRoom->htmlSafeMemberName(author->id());
}
return eventHandler.getAuthorDisplayName(isPending);
}
if (role == IsRedactedRole) {
@@ -913,195 +692,27 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
}
QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const
{
QVariantMap mediaInfo;
QString eventId = event.id();
// Get the file info for the event.
const EventContent::FileInfo *fileInfo;
if (event.is<RoomMessageEvent>()) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event);
if (!roomMessageEvent->hasFileContent()) {
return {};
}
fileInfo = roomMessageEvent->content()->fileInfo();
} else if (event.is<StickerEvent>()) {
auto stickerEvent = eventCast<const StickerEvent>(&event);
fileInfo = &stickerEvent->image();
} else {
return {};
}
return getMediaInfoFromFileInfo(fileInfo, eventId);
}
QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
{
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
}
}
auto mimeType = fileInfo->mimeType;
// Add the MIME type for the media if available.
mediaInfo["mimeType"_ls] = mimeType.name();
// Add the MIME type icon if available.
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
// Add media size if available.
mediaInfo["size"_ls] = fileInfo->payloadSize;
// Add parameter depending on media type.
if (mimeType.name().contains(QStringLiteral("image"))) {
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("video"))) {
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
mediaInfo["width"_ls] = castInfo->imageSize.width();
mediaInfo["height"_ls] = castInfo->imageSize.height();
mediaInfo["duration"_ls] = castInfo->duration;
if (!isThumbnail) {
QVariantMap tempInfo;
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
tempInfo = thumbnailInfo;
} else {
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
if (blurhash.isEmpty()) {
tempInfo["source"_ls] = QUrl();
} else {
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
}
}
mediaInfo["tempInfo"_ls] = tempInfo;
}
}
}
if (mimeType.name().contains(QStringLiteral("audio"))) {
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
mediaInfo["duration"_ls] = castInfo->duration;
}
}
return mediaInfo;
}
void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event)
{
if (m_linkPreviewers.contains(event->id())) {
return;
} else {
QString text;
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl());
}
}
}
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return;
}
auto eventId = event->id();
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
return;
};
QMap<QString, QList<Quotient::User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_currentRoom->user(e->senderId()));
}
}
EventHandler eventHandler;
eventHandler.setRoom(m_currentRoom);
eventHandler.setEvent(event);
if (reactions.isEmpty()) {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
return;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_currentRoom->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (m_reactionModels.contains(eventId)) {
m_reactionModels[eventId]->setReactions(res);
} else if (res.size() > 0) {
m_reactionModels[eventId] = new ReactionModel(this, res, m_currentRoom->localUser());
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
m_linkPreviewers[eventId] = linkPreviewer;
} else {
if (m_reactionModels.contains(eventId)) {
delete m_reactionModels[eventId];
m_reactionModels.remove(eventId);
}
m_linkPreviewers.remove(eventId);
}
if (auto reactionModel = eventHandler.getReactions()) {
m_reactionModels[eventId] = reactionModel;
} else {
m_reactionModels.remove(eventId);
}
}

View File

@@ -5,6 +5,7 @@
#include <KFormat>
#include <QAbstractListModel>
#include <QQmlEngine>
#include "linkpreviewer.h"
#include "neochatroom.h"
@@ -25,6 +26,7 @@ class ReactionModel;
class MessageEventModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current room that the model is getting its messages from.
@@ -32,32 +34,6 @@ class MessageEventModel : public QAbstractListModel
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
/**
* @brief The type of delegate that is needed for the event.
*
* @note While similar this is not the matrix event or message type. This is
* to tell a QML ListView what delegate to show for each event. So while
* similar to the spec it is not the same.
*/
enum DelegateType {
Emote, /**< A message that begins with /me. */
Notice, /**< A notice event. */
Image, /**< A message that is an image. */
Audio, /**< A message that is an audio recording. */
Video, /**< A message that is a video. */
File, /**< A message that is a file. */
Message, /**< A text message. */
Sticker, /**< A message that is a sticker. */
State, /**< A state event in the room. */
Encrypted, /**< An encrypted message that cannot be decrypted. */
ReadMarker, /**< The local user read marker. */
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(DelegateType)
/**
* @brief Defines the model roles.
*/
@@ -65,7 +41,8 @@ public:
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
PlainText, /**< Plain text representation of the message. */
EventIdRole, /**< The matrix event ID of the event. */
TimeRole, /**< The timestamp for when the event was sent. */
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */
ContentRole, /**< The full message content. */
@@ -78,13 +55,13 @@ public:
LinkPreviewRole, /**< The link preview details. */
MediaInfoRole, /**< The media info for the event. */
MimeTypeRole, /**< The mime type of the message's file or media. */
IsReplyRole, /**< Is the message a reply to another event. */
ReplyAuthor, /**< The author of the event that was replied to. */
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
ReplyDisplayRole, /**< The body of the message that was replied to. */
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
ReplyRole, /**< The content data of the message that was replied to. */
ShowAuthorRole, /**< Whether the author's name should be shown. */
ShowSectionRole, /**< Whether the section header should be shown. */
@@ -95,12 +72,8 @@ public:
ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */
ReactionRole, /**< List model for this event. */
ShowReactionsRole, /**< Whether there are any reactions to be shown. */
SourceRole, /**< The full message source JSON. */
AuthorIdRole, /**< Matrix ID of the message author. */
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
IsRedactedRole, /**< Whether an event has been deleted. */
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
@@ -154,8 +127,8 @@ private:
bool movingEvent = false;
KFormat m_format;
QMap<QString, LinkPreviewer *> m_linkPreviewers;
QMap<QString, ReactionModel *> m_reactionModels;
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
[[nodiscard]] int timelineBaseIndex() const;
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
@@ -168,10 +141,7 @@ private:
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
void createReactionModelForEvent(const Quotient::RoomMessageEvent *event);
void createEventObjects(const Quotient::RoomMessageEvent *event);
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
bool m_initialized = false;

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