Compare commits

..

272 Commits

Author SHA1 Message Date
ivan tkachenko
259b9884c7 Set WrapMode on all TextArea components, and fix up some of the Labels
Upstream styles do not provide any wrapping by default, and thus we
should not rely on deceptively convenient override in qqc2-desktop-style.

See https://invent.kde.org/frameworks/qqc2-desktop-style/-/merge_requests/331
2023-11-28 22:26:57 +00:00
l10n daemon script
2980dc49e4 GIT_SILENT Sync po/docbooks with svn 2023-11-28 02:11:26 +00:00
Tobias Fella
5e12f50899 Wrap text in poll delegates
BUG: 477629
2023-11-27 20:44:19 +00:00
James Graham
79940f707f By default only show leave/join state events
As discussed in network/neochat#609
2023-11-27 18:15:48 +00:00
l10n daemon script
e7024a270c GIT_SILENT Sync po/docbooks with svn 2023-11-27 02:12:56 +00:00
Tobias Fella
1a561f6649 Fix linking on windows 2023-11-26 17:40:50 +01:00
Tobias Fella
2cbb6ed65c Enable windows qt6 CI 2023-11-26 15:22:26 +00:00
James Graham
ab4639926a Fix isEdit Check MessageEditComponent
Stop the console being pinged with qrc:/org/kde/neochat/qml/MessageEditComponent.qml:158: TypeError: Cannot read property 'isEditing' of undefined.

Fixes network/neochat#629
2023-11-26 15:21:18 +00:00
Tobias Fella
89b6c54f25 Fix state delegate spacing 2023-11-26 15:06:35 +01:00
James Graham
27c9c62564 ResolveResource
Use ResolveResource rather than calling individual functions like visit user and room
2023-11-26 12:23:28 +00:00
l10n daemon script
bb8ffb02d1 GIT_SILENT Sync po/docbooks with svn 2023-11-26 02:48:22 +00:00
James Graham
201aa82c04 Remove controller::joinRoom
Remove controller::joinRoom in favour of using RoomManager::OpenResource
2023-11-25 15:07:15 +00:00
l10n daemon script
704430db7a GIT_SILENT Sync po/docbooks with svn 2023-11-25 02:08:53 +00:00
Albert Astals Cid
dcbbbd9296 GIT_SILENT Upgrade release service version to 24.01.80. 2023-11-25 00:51:46 +01:00
Tobias Fella
410258c478 Join rooms when visiting them
BUG: 477261
2023-11-24 19:11:15 +01:00
Tobias Fella
6baf2e4888 Fix crash for invalid media uris
BUG: 476698
2023-11-24 18:50:30 +01:00
Tobias Fella
dd6eaac556 Don't crash when sticker pack room doesn't exist
BUG: 476923
2023-11-24 16:43:16 +01:00
l10n daemon script
840903128c GIT_SILENT Sync po/docbooks with svn 2023-11-24 02:10:11 +00:00
l10n daemon script
68f0ca96da GIT_SILENT Sync po/docbooks with svn 2023-11-23 02:13:47 +00:00
l10n daemon script
faf7af06fe GIT_SILENT Sync po/docbooks with svn 2023-11-22 02:09:37 +00:00
l10n daemon script
4ef67c3e0d GIT_SILENT Sync po/docbooks with svn 2023-11-21 02:09:50 +00:00
James Graham
5efd17d370 Loading and End of Timeline Delegates
Add delegate for showing the user a loading indicator and for the beginning of the timeline.

BUG: 455045
BUG: 465285
2023-11-20 17:10:56 +00:00
l10n daemon script
0dbef58ff2 GIT_SILENT Sync po/docbooks with svn 2023-11-20 02:44:00 +00:00
Tobias Fella
6a3df8baf4 Fix initial visual focus in welcomepage 2023-11-19 14:09:08 +01:00
l10n daemon script
2fc973f218 GIT_SILENT Sync po/docbooks with svn 2023-11-19 02:26:37 +00:00
l10n daemon script
7d16999c44 GIT_SILENT Sync po/docbooks with svn 2023-11-18 02:09:37 +00:00
Laurent Montel
f9ba31f2dc Remove unused variables 2023-11-17 10:23:58 +00:00
Laurent Montel
d4ad773ff1 Remove unused "this" argument 2023-11-17 09:36:35 +00:00
Laurent Montel
822a4dc500 Use nullptr 2023-11-17 07:34:46 +00:00
l10n daemon script
bfd1d431c1 GIT_SILENT Sync po/docbooks with svn 2023-11-17 02:12:16 +00:00
Carl Schwan
94bf2481f0 Fix spacing in login screen 2023-11-16 22:43:45 +01:00
Tobias Fella
5c32520c35 Add appium test for opening the user details sheet and fix some accessibility problems 2023-11-16 19:47:13 +01:00
l10n daemon script
de47597f6e GIT_SILENT Sync po/docbooks with svn 2023-11-16 02:21:25 +00:00
l10n daemon script
7780a72888 GIT_SILENT Sync po/docbooks with svn 2023-11-15 02:13:48 +00:00
Carl Schwan
1f8c07fedf Make room timeline non interactible
Currently trying to scroll with this scrollbar is probablematic and it's
unlikely we will find an easy solution to fix it, so make it non interactible.
2023-11-14 19:16:02 +00:00
Tobias Fella
877575b4d3 Fix kirigami name 2023-11-14 19:18:02 +01:00
l10n daemon script
1181df4db2 GIT_SILENT Sync po/docbooks with svn 2023-11-14 02:12:52 +00:00
Joshua Goins
5be2113b32 Add matrix: URL to push notifications 2023-11-13 20:02:07 +00:00
l10n daemon script
7ad7af56e8 GIT_SILENT Sync po/docbooks with svn 2023-11-13 02:12:13 +00:00
Joshua Goins
d3148f8c8b Don't start the entire NeoChat backend when receiving push notifications
This adds a dedicated "set up for push notifications only" function in
Controller, which only sets up the KUnifiedPush connector for receiving
the message and then quitting right afterward. If the user tries to
open a notification, then it will quit and open the main client.
2023-11-12 17:08:15 -05:00
Joshua Goins
100f595026 Add handler for KUnifiedPush messageReceived events 2023-11-12 21:46:33 +00:00
Joshua Goins
15ba6d58e2 Only attempt to setup push notifications on the first syncDone 2023-11-12 15:53:15 -05:00
James Graham
61ad892732 Handle multiple line names
Add function to get the display name for an author on a single line as nothing stops there being linebreaks.

BUG:476731
2023-11-12 18:46:04 +00:00
James Graham
2a3e1dfcd7 Read Marker Hidden
Only show the read marker if there is a non-hidden event earlier in the timeline

fixes network/neochat#615
2023-11-12 16:32:08 +00:00
Joshua Goins
d1dc6fc4ed Fix typo preventing APK from building 2023-11-12 15:36:38 +00:00
James Graham
6dc30a9ca7 Fix hover action position when wide
Fix the positioning of the hover actions when the window is wide by using the content x pos
2023-11-12 14:20:46 +00:00
James Graham
ae0c5ffaef Improve State Text Translatability
Make the state state strings less ambiguous for the purpose of translation.

BUG: 476358
2023-11-12 14:08:36 +00:00
Tobias Fella
fc546d4a43 Add notifications view 2023-11-12 13:16:09 +00:00
Tobias Fella
96e62e3ebe Disable system tray integration by default 2023-11-12 12:46:16 +00:00
Joshua Goins
d979cd2fbc Add Snap Store link in README 2023-11-12 04:01:26 +00:00
l10n daemon script
22298181cb GIT_SILENT Sync po/docbooks with svn 2023-11-12 02:22:14 +00:00
Tobias Fella
1312fde470 Cleanup code 2023-11-12 01:46:23 +01:00
Tobias Fella
7fe2feb1e4 Make NeoChat not shut down when disabling close-to-tray 2023-11-12 01:46:06 +01:00
Ingo Klöcker
67fb5d0824 Add option used on Binary Factory 2023-11-11 22:06:59 +00:00
Ingo Klöcker
359114bd3d Tell Craft that we need Qt 6 and master of the dependencies 2023-11-11 22:06:59 +00:00
Ingo Klöcker
3db9f1198b Add CI template for building APKs 2023-11-11 22:06:59 +00:00
Tobias Fella
2435a6b953 Fix opening account editor 2023-11-11 23:06:26 +01:00
Tobias Fella
e6c8b3fa4b Simplify some android code 2023-11-11 22:59:27 +01:00
Tobias Fella
2d55dca508 Don't allow changing the power level for users with a power level higher or equal to ours
The server won't allow it anyway
2023-11-11 18:20:54 +00:00
Tobias Fella
bbb0bc3092 Improve and fix powerlevels dialog 2023-11-11 17:37:27 +00:00
James Graham
4065aa6a2e Fix linkpreview tooltip
Fix linkpreview tooltip so that the correct text is always shown.

BUG: 467106
2023-11-11 17:29:15 +00:00
Tobias Fella
5942eac5ed Force plaintext in permissions settings 2023-11-11 18:09:03 +01:00
Tobias Fella
85c2b7dada Fix crash on shutdown 2023-11-11 16:03:35 +01:00
Carl Schwan
86ef921cdb Fix alignment user info
Otherwise we end up with two times largeSpacing as right margin for the
user info.
2023-11-11 15:40:15 +01:00
James Graham
aab69c5bae Suggested rooms spaces
Add the ability to set and show suggested rooms for spaces. 

This is just adding the basic functionality, we can do more things with it later like sort/filter the space home for example.
2023-11-11 13:32:19 +00:00
Carl Schwan
624578ec77 Improve welcome page
- Don't use card for welcome message and instead use same layout as
  kwordquiz
- Add separator between login and register button
- Add type annotation in functions
2023-11-11 13:12:56 +00:00
l10n daemon script
197ff984fd GIT_SILENT Sync po/docbooks with svn 2023-11-11 02:19:28 +00:00
James Graham
a26337d5f4 Fix ServerListModel
The server list needs to be populated on connection change as this is instantiated from QML
2023-11-10 20:32:51 +00:00
Joshua Goins
31b4eefadd Move closeDialog signal from Loading to LoginStep
This prevents errors because we try to connect to a non-existent signal
if the step is anything but Loading.
2023-11-10 18:40:37 +00:00
Joshua Goins
b8e592f8ba Fix wrong argument being passed into maximize component creation
It's supposed to refer to the attached property on QQC2.Overlay, not
QQC2.ApplicationWindow
2023-11-10 18:36:49 +00:00
Joshua Goins
555d23863e Port to QEvent::ApplicationPaletteChange
The paletteChanged on QGuiApplication is deprecated in Qt6
2023-11-10 18:30:27 +00:00
Joshua Goins
ffa2d5dc0e Fix push notification registration
Now it should regularly happen after login
2023-11-10 18:08:52 +00:00
Joshua Goins
9987edbaf2 Note that profileTag is intentionally left empty 2023-11-10 18:08:52 +00:00
Joshua Goins
d90298392d Only run setupPushNotification once for new accounts 2023-11-10 18:08:52 +00:00
Joshua Goins
600dbd0603 Remove unnecessary activeConnection() call in setupPushNotifications 2023-11-10 18:08:52 +00:00
Joshua Goins
2179e2cc35 Fix QCoro headers 2023-11-10 18:08:52 +00:00
Joshua Goins
8119ea3ccb Use auto in NeoChatConnection::setupPushNotifications 2023-11-10 18:08:52 +00:00
Joshua Goins
3fc125a798 Move push notification setup to NeoChatConnection 2023-11-10 18:08:52 +00:00
Joshua Goins
99f6778df4 Register for push notifications
This sets it up so the homeserver will give us push notifications, but
we aren't handling them yet.
2023-11-10 18:08:52 +00:00
Joshua Goins
f7bd24db34 Depend on QCoro::Network
For awaiting on QNetworkReply
2023-11-10 18:08:52 +00:00
Joshua Goins
41c2f9c4d5 Re-arrange main.cpp and add no gui option when DBus activated
We don't want the QML engine initialized at all when launching on
notifications, it'll be useless there.
2023-11-10 18:08:52 +00:00
Joshua Goins
56d5f3036b Change name of notify argument to dbus-activated 2023-11-10 18:08:52 +00:00
Tobias Fella
21bb7dce21 Fix background color of UserInfo
Previously, this would change when the window lost focus, which looks strange
2023-11-10 16:16:29 +01:00
l10n daemon script
11081719a7 GIT_SILENT Sync po/docbooks with svn 2023-11-10 02:22:55 +00:00
Joshua Goins
980de7d85d Use go.kde.org for the Matrix link, mention Matrix wiki page
I moved NeoChat up next to Element on go.kde.org, so it's good marketing
;-)
2023-11-09 16:53:36 -05:00
Joshua Goins
7735313b0c Add DBus service for NeoChat and link to KUnifiedPush if found 2023-11-09 21:40:09 +00:00
Joshua Goins
1a3055df86 Add optional KUnifiedPush dependency 2023-11-09 21:40:09 +00:00
Tobias Fella
84cad630cd Refactor and fix ChatBox layouting
BUG: 474616
2023-11-09 20:19:09 +00:00
James Graham
0beb5df08d Hide the search field for room members in a direct chat.
Hide the search field for room members in a direct chat as there are no members to search.
2023-11-09 20:11:38 +00:00
Tobias Fella
4ef44b8e93 Track online status per connection 2023-11-09 19:38:43 +01:00
l10n daemon script
59153be006 GIT_SILENT Sync po/docbooks with svn 2023-11-09 02:12:54 +00:00
l10n daemon script
50be96f762 GIT_SILENT Sync po/docbooks with svn 2023-11-08 09:27:31 +00:00
Tobias Fella
819d88e18c Assorted Qt6 android fixes 2023-11-07 23:17:38 +01:00
James Graham
dbbad2cf13 Restricted Room Security
Create the required ux to allow the restricted room security setting to be re-enabled

BUG: 471307
2023-11-07 20:43:49 +00:00
Albert Astals Cid
08b84c6592 GIT_SILENT Upgrade release service version to 24.01.75. 2023-11-07 21:11:26 +01:00
l10n daemon script
b8abf0540d GIT_SILENT Sync po/docbooks with svn 2023-11-07 03:22:56 +00:00
Tobias Fella
7f9e709559 Improve initial active connection handling
If the last active connection is not reachable (server down, keychain problems,
token revoked, etc.), NeoChat currently fails to load at all, with the only fix
being to delete a line from the config file. This is surprisingly hard to fix with
a nice UX as long as we stick to the principle of loading the user's last active
connection automatically.

This patch thus drops that principle; instead, the user is always asked to choose
the connection to continue with.
2023-11-06 12:26:25 +00:00
l10n daemon script
c4f6abee9d GIT_SILENT Sync po/docbooks with svn 2023-11-06 02:17:00 +00:00
Joshua Goins
1a4947b98a Set accessible press actions for RoomDelegate 2023-11-05 18:09:20 -05:00
Tobias Fella
6ba2b715c3 Port away from Controller::activeConnectionIndex 2023-11-05 21:03:04 +00:00
Joshua Goins
e9e1e223f7 Allow right-clicking on a room without selecting it
Same idea as the fix I did for spaces, we introduce a new signal called
selected instead of using ItemDelegate's button signals.
2023-11-05 20:43:08 +00:00
Joshua Goins
006da1fb16 Show color scheme option on Android
This works on Android now, and the rest of the code changed to enable it
anyway.
2023-11-05 20:38:33 +00:00
James Graham
3aff1795c8 Accounts Popup Close on Hide
Make sure that the accounts popup is closed when the UserInfo component is hidden
2023-11-05 19:57:15 +00:00
Joshua Goins
576b1f928f Set RoomManager connection when opening a room
This was only set for the Controller, but it needed to be set for the
RoomManager too before opening a room. This could cause NeoChat to crash
when activating a notification, for example.
2023-11-05 14:48:55 -05:00
l10n daemon script
fc3ab50701 GIT_SILENT Sync po/docbooks with svn 2023-11-05 14:37:01 +00:00
James Graham
e7fa3ad524 Explicit room parameter DirectChatDrawerHeader
Create explicit room parameter and reference it in DirectChatDrawerHeader
2023-11-05 13:49:13 +00:00
James Graham
5adffddbd8 Fix InviteUserPage
Port the InviteUserPage to Qt6 and fix close action

fixes network/neochat#614
2023-11-05 13:45:10 +00:00
Tobias Fella
1d95d5aa15 Fix Runner and MatrixImageProvider singletons 2023-11-05 14:35:28 +01:00
Carl Schwan
dabd6291a5 roomlist: Fix ListSectionDelegate usage
The default property of ListSectionDelegate is already an alias to the
RowLayout.data and trying to use RoomLayout.children didn't work as
expected.
2023-11-05 13:08:29 +00:00
Tobias Fella
0e55c3b38f Port some things away from Controller::activeConnection 2023-11-05 13:02:18 +00:00
Carl Schwan
ff4cf86ea5 Add profile picture in about data 2023-11-05 12:17:26 +01:00
l10n daemon script
f4d5ccbf12 GIT_SILENT Sync po/docbooks with svn 2023-11-05 11:14:20 +00:00
l10n daemon script
192601d358 GIT_SILENT Sync po/docbooks with svn 2023-11-05 01:41:24 +00:00
l10n daemon script
d7c432119e GIT_SILENT Sync po/docbooks with svn 2023-11-04 17:24:07 +00:00
Tobias Fella
eddb2b73c2 Use new notification action api 2023-11-04 16:18:00 +00:00
Tobias Fella
0c60bfdb83 Don't crash on quit
BUG: 474386
2023-11-04 16:03:54 +00:00
James Graham
2bfb2fa1f9 Improve ChatDocumentHandler handling of null properties
Improve ChatDocumentHandler handling of null properties
2023-11-04 14:43:31 +00:00
l10n daemon script
d1aac971bf GIT_SILENT Sync po/docbooks with svn 2023-11-04 12:51:19 +00:00
Heiko Becker
284cadf305 GIT_SILENT Update Appstream for new release
(cherry picked from commit 753e879556)
2023-11-04 12:33:19 +01:00
l10n daemon script
4697b7fcf1 GIT_SILENT Sync po/docbooks with svn 2023-11-04 02:10:15 +00:00
Tobias Fella
9f356912c9 Add placeholder message to devices page 2023-11-03 22:06:33 +00:00
Tobias Fella
1cf891f845 Fix device logout dialog 2023-11-03 22:06:14 +00:00
Joshua Goins
52e2d636b9 Adjust tests to blockquote formatting changes 2023-11-03 21:52:47 +00:00
Joshua Goins
dc6b539ddf Add somewhat better blockquote styling
Quotes are now "quoted" and also have a different background.
2023-11-03 21:52:47 +00:00
Joshua Goins
e3cf85aa8c Yank "Qt5" out of the description for libQuotient
We aren't using thr Qt5 version anymore, and it's not really a point in
describing which exact Qt version we use with libQuotient in a user
facing string like this anyway.
2023-11-03 17:39:02 -04:00
James Graham
2065eb6684 Mobile explore component
Create a mobile version of explore component and place it at the bottom for single handed use. This also refactors the UserInfo component so it can be at the top on mobile as well as the bottom on dektop.

This should have no effect on desktop and should be identical.

![image](/uploads/9b3133fbde74ca27069d6b039efb1079/image.png)
2023-11-03 17:22:57 +00:00
Tobias Fella
9cac2a8abd Delete KeywordNotificationRuleModel
It's unused
2023-11-03 17:39:10 +01:00
Tobias Fella
feb87e6f70 Show QR codes in UserDetailDialog 2023-11-03 16:23:54 +00:00
l10n daemon script
a0057b8a49 GIT_SILENT Sync po/docbooks with svn 2023-11-03 02:12:15 +00:00
Tobias Fella
dbb0269354 Fix sending locations 2023-11-02 19:46:06 +01:00
Tobias Fella
7fdb617b33 Port RoundButtons in TimelineView to FloatingButton
BUG: 476124
2023-11-02 18:15:57 +01:00
Tobias Fella
d798b0dec9 HTML-escape mentions 2023-11-02 17:04:26 +00:00
James Graham
95cf23eb5b Remove GridLayout ReplyComponent
Stop using GridLayout in ReplyComponent as they are notoriously terribleRemove
2023-11-02 17:04:18 +00:00
James Graham
7e3db20229 Remove isEdit from ChatDocumentHandler
Remove isEdit from ChatDocumentHandler as it was made redundant by the ChatCache rework.
2023-11-02 17:00:41 +00:00
Laurent Montel
12689babfb Add missing moc 2023-11-02 08:50:43 +01:00
l10n daemon script
211407da44 GIT_SILENT Sync po/docbooks with svn 2023-11-02 02:13:36 +00:00
l10n daemon script
400a84e48d GIT_SILENT Sync po/docbooks with svn 2023-11-01 02:15:14 +00:00
Tobias Fella
7a45640e5e Fix string 2023-10-31 17:13:05 +01:00
Tobias Fella
8af20885ab Add account / device security settings page 2023-10-31 14:59:08 +01:00
Tobias Fella
4033f07272 Port away from custom format* QML functions 2023-10-31 14:56:52 +01:00
Tobias Fella
5df4fa297d Move Controller::setBlur and Controller::hasWindowSystem to WindowController 2023-10-31 09:17:44 +00:00
Tobias Fella
33c5b418d2 Move Controller::openOrCreateDirectChat to NeoChatConnection 2023-10-31 09:16:59 +00:00
l10n daemon script
69d378a17b GIT_SILENT Sync po/docbooks with svn 2023-10-31 02:11:02 +00:00
Tobias Fella
f1b1b8ce53 Remove unused include 2023-10-30 21:29:44 +01:00
Tobias Fella
526d4748e0 Remove unused includes 2023-10-30 21:27:36 +01:00
Tobias Fella
c5f93adbf4 Don't percent encode job parameter 2023-10-30 20:51:44 +01:00
Tobias Fella
61630cbe90 Rename "Security.qml" to "RoomSecurity.qml" 2023-10-30 11:07:42 +01:00
l10n daemon script
8c435e9d6d GIT_SILENT Sync po/docbooks with svn 2023-10-30 02:13:41 +00:00
James Graham
57978b1a6e EventHandler Nullptr checks
So I got lazy halfway through doing this the first time.

Add missing checks for null m_room and m_event to EventHandler and add tests for them.
2023-10-29 14:51:25 +00:00
James Graham
f3c4d9449a Move remaining tests to external Json files
Title
2023-10-29 13:12:05 +00:00
James Graham
9b37777f20 Remove getUsers
Remove getUsers as it's unused
2023-10-29 13:07:45 +00:00
l10n daemon script
d6d6c161db GIT_SILENT Sync po/docbooks with svn 2023-10-29 02:23:11 +00:00
Tobias Fella
772bca5ba6 Fix sending messages that were typed before switching rooms 2023-10-28 17:21:12 +02:00
Tobias Fella
036a60a095 Don't linkify urls in code blocks
BUG: 475301
2023-10-28 16:19:32 +02:00
Tobias Fella
b3315e1ed4 Add test for bug 475301
BUG: 475301
2023-10-28 16:19:31 +02:00
Tobias Fella
d300e9cf52 Slightly reformat cmake 2023-10-28 16:18:21 +02:00
Tobias Fella
1f4bcd150f Make SectionDelegate separator thickness consistent with other Separators 2023-10-28 16:16:37 +02:00
Tobias Fella
9ad8894983 Fix connection switching 2023-10-28 15:13:22 +02:00
Tobias Fella
9e63ca5eb7 Remove ShimmerGradient
It's unused
2023-10-28 14:08:51 +02:00
Tobias Fella
ade66242bb Remove unused import 2023-10-28 13:59:23 +02:00
Tobias Fella
2c6932b4cb Fix crash when there is an empty logging category
BUG: 475248
2023-10-28 09:12:59 +00:00
l10n daemon script
5cce9e7205 GIT_SILENT Sync po/docbooks with svn 2023-10-28 02:20:45 +00:00
Tobias Fella
7dd3ad9548 Hide info banner when changing room
BUG: 476128
2023-10-28 00:21:57 +02:00
Tobias Fella
f690b76efa Fix crash for events that are not RoomMessageEvents
BUG: 476153
2023-10-27 22:11:12 +02:00
James Graham
87b8d6710e Update Kirigami PlatformTheme
Update Kirigami PlatformTheme to new path.
2023-10-27 16:13:22 +00:00
l10n daemon script
965b890346 GIT_SILENT Sync po/docbooks with svn 2023-10-27 02:16:58 +00:00
Tobias Fella
02b4e5cc70 Port away from CheckableListItem 2023-10-26 10:07:35 +00:00
Tobias Fella
410add04fb Fix subtle erroneous event casts
Sometimes, the previous code would static_cast an EncryptedEvent to a RoomMessageEvent.
This was mostly unnoticed, since the types are similar enough.
2023-10-26 10:03:09 +00:00
l10n daemon script
b575b1e700 GIT_SILENT Sync po/docbooks with svn 2023-10-26 02:27:46 +00:00
l10n daemon script
5828ee1ada SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2023-10-26 02:17:19 +00:00
l10n daemon script
a64c80109e GIT_SILENT made messages (after extraction) 2023-10-26 01:45:07 +00:00
Tobias Fella
de47f7f2fa Remove duplicate signal 2023-10-25 20:28:14 +02:00
Laurent Montel
19adc7b9e5 GIT_SILENT: use KLocalizedString::setApplicationDomain(QByteArrayLiteral => saving one deep data copy 2023-10-25 08:25:27 +02:00
l10n daemon script
23f60a59fe GIT_SILENT Sync po/docbooks with svn 2023-10-25 02:25:42 +00:00
l10n daemon script
946ba2e56d GIT_SILENT made messages (after extraction) 2023-10-25 01:50:11 +00:00
Tobias Fella
9dcb7b49fa Use KColorScheme 2023-10-24 07:55:01 +00:00
Tobias Fella
52f5901642 Update replacement room filtering in real-time 2023-10-24 07:51:15 +00:00
Tobias Fella
d4b4a7e1ff Only hide replaced rooms if we're joined to the replacement room 2023-10-24 07:51:15 +00:00
Tobias Fella
4449678b74 Don't abort in the message logger
Let the app do whatever it would do naturally
2023-10-24 07:32:09 +00:00
Tobias Fella
19e197e0ec Fix maps
In Qt6, Map just shows a static map, while MapView allows for zooming, moving, etc.
2023-10-24 07:08:05 +00:00
l10n daemon script
cfc5202645 GIT_SILENT Sync po/docbooks with svn 2023-10-24 02:14:54 +00:00
Tobias Fella
9b80d9e7aa Minor QML cleanup 2023-10-23 22:28:34 +02:00
Tobias Fella
dc409387bd Remove check for libQuotient 0.8 2023-10-23 22:00:34 +02:00
l10n daemon script
1e73a7bda4 GIT_SILENT Sync po/docbooks with svn 2023-10-23 02:12:23 +00:00
James Graham
ef1d62d45c ActionsHandler poperties
Make sure that all low level components get ActionsHandler through actual properties rather than magically
2023-10-22 13:39:34 +00:00
James Graham
c2d82750b1 Fix opening a room in a new window.
Fix opening a room in a new window. This is done by makeing the model top level parameters in RoomPage set from RoomManager but overwritten by RoomWindow
2023-10-22 11:57:27 +00:00
l10n daemon script
c97d276b36 GIT_SILENT Sync po/docbooks with svn 2023-10-22 02:48:07 +00:00
l10n daemon script
681a0b1e93 GIT_SILENT Sync po/docbooks with svn 2023-10-21 02:16:44 +00:00
James Graham
39556f45ab Send Threaded Messages
This MR deals with only sending threaded messages. Showing threads will turn up in a follow up. This allows you to start a new thread by clicking reply in thread to a normal message. 

You can also do a threaded reply to a threaded message in the main timeline at the moment because those messages aren't shown in a separate thread timeline yet but will be in future.
2023-10-20 17:07:09 +00:00
Joshua Goins
3c7774800a Remove now unused QDataStream operators for Emoji 2023-10-20 15:27:33 +00:00
Joshua Goins
bc7530eaa1 Store last used emojis in the state config file
Instead of crumming up the main settings file with the last used emojis,
they now live in the state (where they belong.)
2023-10-20 15:27:33 +00:00
l10n daemon script
82d11f79d6 GIT_SILENT Sync po/docbooks with svn 2023-10-20 02:11:46 +00:00
Joshua Goins
25d0368d41 Compact the event cache JSON
By default, Qt will spit out indented JSON when writing to the config
file, which is useless as it wastes space and no one will read this.
2023-10-19 20:46:17 +00:00
Joshua Goins
83b7e7d121 Use KSharedConfig::openStateConfig() instead of using a "data" file
This function will automatically create a "neochatstarerc" for us, and
KConfig will decide the best place for us to place our state. It won't
always be in AppDataLocation.
2023-10-19 20:39:07 +00:00
Tobias Fella
26fd26f9fd Enable android qt6 ci 2023-10-19 20:18:15 +00:00
James Graham
0029567c3a Reorganise HoverActions Code
Move most of the external code for hover actions into the component.
2023-10-19 19:38:57 +00:00
Laurent Montel
c7614caf41 There's no QVector anymore, QList is the QVector in Qt6 2023-10-19 13:41:46 +02:00
l10n daemon script
6571dbe554 GIT_SILENT Sync po/docbooks with svn 2023-10-19 02:14:15 +00:00
l10n daemon script
b3a29068cc GIT_SILENT Sync po/docbooks with svn 2023-10-18 02:15:55 +00:00
Tobias Fella
5adda55a85 Show rooms instead of spaces in JoinRoomPage 2023-10-17 19:14:07 +00:00
l10n daemon script
d56f0d6086 GIT_SILENT Sync po/docbooks with svn 2023-10-17 02:10:40 +00:00
Tobias Fella
60772be391 Enforce symbolic icon in ExploreComponent 2023-10-16 22:12:13 +02:00
Yuri Chornoivan
27f1679741 Fix minor typo 2023-10-16 09:27:27 +03:00
l10n daemon script
838596c3ae GIT_SILENT Sync po/docbooks with svn 2023-10-16 02:10:50 +00:00
James Graham
a57744891a ChatCache
Move the functionality to cache the contents of a chat bar from the room directly and to a new ChatCache object. This works pretty much the same with a few extra check and balances, this also made it easy to put a test suite around the functionality so I did. The current functionality should be identical to what exists.

This is in prep for threads which will require managing even more caches if we create one per thread.
2023-10-15 12:55:56 +00:00
James Graham
f5417a6227 New parent dialog
Move the add new offical parent to a dialog and make sure that the join room dialog only shows spaces.
2023-10-15 11:28:17 +00:00
l10n daemon script
ac6f9ea219 GIT_SILENT Sync po/docbooks with svn 2023-10-15 02:11:25 +00:00
Tobias Fella
4b49559d39 Fix LocationHelper registration 2023-10-14 23:38:31 +02:00
Laurent Montel
baa33f1843 Remove unused import module 2023-10-14 20:27:18 +00:00
Tobias Fella
dae5718c6c Fix broken shortcut 2023-10-14 22:18:37 +02:00
Tobias Fella
60260cff3b Fix bugs in state delegates
- Don't show a dot over profile pictures.
  The dot was intended to be part of the Avatar when there is no image and the Avatar is too small to show an icon.
  Currently, it is visible over the profile picture. If we still want the dot, it should be upstreamed

- Fix avatar images in StateComponent
2023-10-14 19:33:44 +00:00
Laurent Montel
2df9a26cdc QTLOCATION_MODULE_QML_VERSION was used during qt5/qt6 support. Now version is not necessary in qt6 2023-10-14 20:42:19 +02:00
l10n daemon script
b16cd12b33 GIT_SILENT Sync po/docbooks with svn 2023-10-14 02:21:14 +00:00
Ingo Klöcker
6a3b22ef2d Remove the obsolete x-kde-os attribute for Windows screenshots 2023-10-13 14:06:47 +00:00
Tobias Fella
b28a85ff05 Fix AboutPage 2023-10-13 15:35:39 +02:00
James Graham
e480299563 Canonical Parent
So the original space parent and child stuff was technically a bit naughty in that it allowed multiple rooms to be set as the canonical parent. Because while a room can have multiple parents only one should be canonical. This adds the following:
- When adding a child or parent there is an extra check to select if the new parent should be canonical
- Any parent can be selected as the canonical one from the room settings
- All functions ensure that there is only ever one canonical parent by ensuring all others are false when a new one is set.
2023-10-13 12:00:47 +00:00
Tobias Fella
fe70e2773f Fix logging category filtering 2023-10-13 13:42:15 +02:00
l10n daemon script
e78ea4721a GIT_SILENT Sync po/docbooks with svn 2023-10-13 02:14:45 +00:00
l10n daemon script
1699dcf0c4 GIT_SILENT Sync po/docbooks with svn 2023-10-12 02:15:09 +00:00
Rohan Kumar
9d6aef6c2b Flatpak: allow talking to org.freedesktop.secrets
Previously, kwalletd5 was hard-coded. This should allow secret service
access for users with other secret service providers.
2023-10-11 09:31:30 -07:00
James Graham
a9c2428498 Manual Explore Rooms
This is an update to searching the public room list. Currently if you can't find the room you're looking for you can type a full alias of room ID into the search bar and a view/join button appears. This is hard to discover and technically broken since it was turned into a generic component for finding rooms (it kinda works but doesn't fit now as it's focussed on the joining rooms not adding new ones to spaces). It is also not very discoverable if you don't know it's there.

This patch patch updates the workflow to be truly generic and hopefully more discoverable. Instead of using the search bar if no results are found a button asking if someone wants to manually enter a room ID or alias appears. This launches a dialog where the user can type in an alias or ID and it has some basic checking to make sure the string looks as expected.

The new functionality also generically works for joining rooms and adding children to spaces.
2023-10-11 15:53:21 +00:00
l10n daemon script
0730f15e2b GIT_SILENT Sync po/docbooks with svn 2023-10-11 02:48:51 +00:00
l10n daemon script
136856f3c3 GIT_SILENT Sync po/docbooks with svn 2023-10-10 02:20:01 +00:00
l10n daemon script
763b6af076 GIT_SILENT Sync po/docbooks with svn 2023-10-09 02:21:59 +00:00
Tobias Fella
ac231320a3 Fix closing the login dialog 2023-10-08 10:01:01 +00:00
Tobias Fella
87aee162f1 Fix logout from accounts page 2023-10-08 09:52:47 +00:00
Tobias Fella
0899db31af Fix showing logout confirmation dialog 2023-10-08 09:48:27 +00:00
l10n daemon script
b4198bc13b GIT_SILENT Sync po/docbooks with svn 2023-10-08 02:10:30 +00:00
Tobias Fella
c6bfe73d26 Don't show "No Topic" 2023-10-07 16:43:07 +00:00
Tobias Fella
d490dffa36 Fix opening encryption confirmation dialog 2023-10-07 16:26:43 +00:00
Heiko Becker
43b2b71b73 GIT_SILENT Update Appstream for new release
(cherry picked from commit ffbd92317e)
2023-10-07 18:01:42 +02:00
l10n daemon script
39a51d1f35 GIT_SILENT Sync po/docbooks with svn 2023-10-06 02:14:03 +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
254 changed files with 125989 additions and 102829 deletions

10
.craft.ini Normal file
View File

@@ -0,0 +1,10 @@
; SPDX-FileCopyrightText: None
; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings]
kde/unreleased/kirigami-addons.version=master
kde/frameworks.version=master
kde/libs.version=master
kde/plasma.version=master
kde/unreleased.version=master
libs/qt.qtMajorVersion=6

View File

@@ -19,6 +19,7 @@
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [

View File

@@ -2,9 +2,12 @@
# SPDX-License-Identifier: CC0-1.0
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-qt6.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-qt6.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
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-qt6-apks.yml

View File

@@ -13,8 +13,10 @@ Dependencies:
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
@@ -25,11 +27,10 @@ Dependencies:
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kconfigwidgets': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux']
'require':

View File

@@ -41,3 +41,11 @@ License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
License: BSD-2-Clause
Files: autotests/data/*
Copyright: none
License: CC0-1.0
Files: appiumtests/data/*
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: CC0-1.0

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "23")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "01")
set(RELEASE_SERVICE_VERSION_MICRO "80")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -37,6 +37,7 @@ include(ECMAddAppIcon)
include(KDEGitCommitHooks)
include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
@@ -57,15 +58,15 @@ set_package_properties(Qt6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
if(ANDROID)
@@ -76,11 +77,17 @@ if(ANDROID)
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem StatusNotifierItem)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle 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)
@@ -111,7 +118,8 @@ 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})
ecm_find_qmlmodule(QtLocation)
ecm_find_qmlmodule(org.kde.prison)
find_package(KQuickImageEditor COMPONENTS)
set_package_properties(KQuickImageEditor PROPERTIES
@@ -121,7 +129,7 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro6 0.4 COMPONENTS Core Network REQUIRED)
qcoro_enable_coroutines()
@@ -131,6 +139,13 @@ set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL
)
find_package(KUnifiedPush QUIET)
set_package_properties(KUnifiedPush PROPERTIES
TYPE OPTIONAL
PURPOSE "Push notification support"
URL "https://invent.kde.org/libraries/kunifiedpush"
)
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)

View File

@@ -11,6 +11,7 @@ A Qt/QML based Matrix client.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
## Introduction
@@ -95,7 +96,7 @@ As is the case throughout the KDE ecosystem contributions are welcome from all.
## Contact
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org).
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://go.kde.org/matrix/#/#neochat:kde.org). See [Matrix](https://community.kde.org/Matrix) for more details.
## Acknowledgement

View File

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

View File

@@ -12,7 +12,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.4.1'
}
}
@@ -35,7 +35,7 @@ android {
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
@@ -44,17 +44,20 @@ android {
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']

View File

@@ -21,3 +21,8 @@ add_test(
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
)
add_test(
NAME openuserdetailstest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/openuserdetailstest.py
)

View File

@@ -0,0 +1,3 @@
{
"next_batch": "batch1234"
}

View File

@@ -0,0 +1,50 @@
{
"next_batch": "batch1234",
"rooms": {
"join": {
"!room_id_1234:localhost:1234": {
"state": {
"events": [
{
"type": "m.room.member",
"state_key": "@user:localhost:1234",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_0:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"avatar_url": "",
"displayname": "A Display Name",
"membership": "join",
"reason": "Nothing"
},
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"type": "m.room.message",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_1:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"body": "This is a message",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
"msgtype": "m.text"
},
"unsigned": {
"age": 1234
}
}
]
}
}
}
}
}

View File

@@ -1,10 +1,12 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import json
from flask import Flask, request, abort
import os
app = Flask(__name__)
@app.route("/_matrix/client/v3/login", methods=["GET"])
def login_get():
result = dict()
@@ -12,6 +14,13 @@ def login_get():
result["flows"][0]["type"] = "m.login.password"
return result
@app.route("/_matrix/client/v3/account/whoami", methods=["GET"])
def whoami():
result = dict()
result["device_id"] = "device_id_1234"
result["user_id"] = "@user:localhost:1234"
return result
@app.route("/_matrix/client/v3/login", methods=["POST"])
def login_post():
data = request.get_json()
@@ -19,15 +28,22 @@ def login_post():
abort(403)
print(data)
result = dict()
result["access_token"] = "token_1234"
result["access_token"] = "token_login"
result["device_id"] = "device_1234"
result["user_id"] = "@user:localhost:1234"
return result
def load_json(name):
parts = __file__.split("/")
parts.pop()
datadir = "/".join(parts)
return json.loads(open(f"{datadir}/data/{name}.json").read())
@app.route("/_matrix/client/r0/sync")
def sync():
result = dict()
result["next_batch"] = "batch1234"
result = load_json("sync_response_no_rooms") if ("login" in request.headers.get("Authorization")) else load_json("sync_response_rooms")
return result
@app.route("/.well-known/matrix/client")
@@ -37,6 +53,18 @@ def well_known():
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
return reply
@app.route("/_matrix/client/v3/profile/<id>")
def profile(id):
reply = dict()
reply["avatar_url"] = "mxc://localhost:1234/asdf1234"
reply["displayname"] = "User123"
return reply
@app.route("/_matrix/client/v3/keys/upload", methods=["POST"])
def upload_keys():
reply = dict()
return reply
if __name__ == "__main__":
app.run(ssl_context='adhoc', port=1234)

View File

@@ -4,25 +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 os
import time
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)
self.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "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
@@ -45,5 +46,6 @@ class LoginTest(unittest.TestCase):
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

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import 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 OpenUserDetailsTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors --test")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
def setUp(self):
pass
def tearDown(self):
if not self._outcome.result.wasSuccessful():
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_open_sheet(self):
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
self.driver.find_element(by=AppiumBy.NAME, value="Empty room (!room_id_1234:localhost:1234)").click()
self.driver.find_element(by=AppiumBy.NAME, value="A Display Name").click()
self.driver.find_element(by=AppiumBy.NAME, value="Account Details")
if __name__ == '__main__':
unittest.main()

View File

@@ -3,6 +3,8 @@
enable_testing()
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
@@ -32,3 +34,15 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME eventhandlertest
)
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)

View File

@@ -0,0 +1,157 @@
// 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 <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QTest>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include "chatbarcache.h"
#include "neochatroom.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 ChatBarCacheTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void edit();
void attachment();
};
void ChatBarCacheTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
QFile testMinSyncFile;
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
testMinSyncFile.open(QIODevice::ReadOnly);
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
room->update(std::move(roomData));
}
void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::noRoom()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(QLatin1String("some text"));
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
QCOMPARE(chatBarCache->relationMessage(), QString());
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
}
QTEST_MAIN(ChatBarCacheTest)
#include "chatbarcachetest.moc"

View File

@@ -0,0 +1,36 @@
// 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 "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -0,0 +1,410 @@
{
"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
}
},
{
"content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
"membership": "join"
},
"event_id": "$143273582443PhrSh:example.org",
"origin_server_ts": 1432735824659,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"state_key": "@newline:example.org",
"type": "m.room.member",
"unsigned": {
"age": 12345
}
}
]
},
"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
}
},
{
"content": {
"body": "Thread root",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
},
"event_id": "$threadroot:example.org",
"origin_server_ts": 1690821582879,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "Thread message 1",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadroot:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage1:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "Thread message 2",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$threadroot:example.org",
"m.in_reply_to": {
"event_id": "$threadmessage1:example.org"
},
"is_falling_back": true
}
},
"event_id": "$threadmessage2:example.org",
"origin_server_ts": 1690821582890,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "A message from someone who thought it was a good idea to put newlines in their display name.",
"msgtype": "m.text"
},
"event_id": "$153456889:example.org",
"origin_server_ts": 14327358246589,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1230
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,87 @@
{
"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"
}
]
},
"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
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,124 @@
{
"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"
}
]
},
"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** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -39,33 +39,63 @@ private:
Connection *connection = nullptr;
TestRoom *room = nullptr;
EventHandler eventHandler;
EventHandler emptyHandler;
EventHandler noEventHandler;
private Q_SLOTS:
void initTestCase();
void nullSetEvent();
void eventId();
void nullEventId();
void delegateType_data();
void delegateType();
void nullDelegateType();
void author();
void nullAuthor();
void authorDisplayName();
void nullAuthorDisplayName();
void singleLineSidplayName();
void nullSingleLineDisplayName();
void time();
void nullTime();
void timeString();
void nullTimeString();
void highlighted();
void nullHighlighted();
void hidden();
void nullHidden();
void body();
void nullBody();
void genericBody_data();
void genericBody();
void nullGenericBody();
void mediaInfo();
void nullMediaInfo();
void linkPreviewer();
void nullLinkPreviewer();
void reactions();
void nullReactions();
void hasReply();
void nullHasReply();
void replyId();
void nullReplyId();
void replyDelegateType();
void nullReplyDelegateType();
void replyAuthor();
void nullReplyAuthor();
void replyBody();
void nullReplyBody();
void replyMediaInfo();
void nullReplyMediaInfo();
void thread();
void nullThread();
void location();
void nullLocation();
void readMarkers();
void nullReadMarkers();
void cleanup();
};
void EventHandlerTest::initTestCase()
@@ -73,332 +103,21 @@ 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());
QFile testEventHandlerSyncFile;
testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json"));
testEventHandlerSyncFile.open(QIODevice::ReadOnly);
const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object());
room->update(std::move(roomData));
eventHandler.setRoom(room);
noEventHandler.setRoom(room);
}
void EventHandlerTest::nullSetEvent()
{
QTest::ignoreMessage(QtWarningMsg, "cannot setEvent when m_room is set to nullptr.");
emptyHandler.setEvent(room->messageEvents().at(0).get());
}
void EventHandlerTest::eventId()
@@ -408,6 +127,12 @@ void EventHandlerTest::eventId()
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
}
void EventHandlerTest::nullEventId()
{
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getId(), QString());
}
void EventHandlerTest::delegateType_data()
{
QTest::addColumn<int>("eventNum");
@@ -431,6 +156,12 @@ void EventHandlerTest::delegateType()
QCOMPARE(eventHandler.getDelegateType(), delegateType);
}
void EventHandlerTest::nullDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
}
void EventHandlerTest::author()
{
auto event = room->messageEvents().at(0).get();
@@ -448,6 +179,15 @@ void EventHandlerTest::author()
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
}
void EventHandlerTest::nullAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::authorDisplayName()
{
auto event = room->messageEvents().at(1).get();
@@ -456,6 +196,32 @@ void EventHandlerTest::authorDisplayName()
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
}
void EventHandlerTest::nullAuthorDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
auto event = room->messageEvents().at(11).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
}
void EventHandlerTest::nullSingleLineDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();
@@ -465,6 +231,16 @@ void EventHandlerTest::time()
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
}
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTime(), QDateTime());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTime(true), QDateTime());
}
void EventHandlerTest::timeString()
{
auto event = room->messageEvents().at(0).get();
@@ -486,6 +262,16 @@ void EventHandlerTest::timeString()
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
}
void EventHandlerTest::nullTimeString()
{
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getTimeString(false), QString());
eventHandler.setEvent(room->messageEvents().at(0).get());
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
}
void EventHandlerTest::highlighted()
{
auto event = room->messageEvents().at(2).get();
@@ -499,6 +285,15 @@ void EventHandlerTest::highlighted()
QCOMPARE(eventHandler.isHighlighted(), false);
}
void EventHandlerTest::nullHighlighted()
{
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHighlighted(), false);
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHighlighted(), false);
}
void EventHandlerTest::hidden()
{
auto event = room->messageEvents().at(3).get();
@@ -512,6 +307,15 @@ void EventHandlerTest::hidden()
QCOMPARE(eventHandler.isHidden(), false);
}
void EventHandlerTest::nullHidden()
{
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
QCOMPARE(emptyHandler.isHidden(), false);
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
QCOMPARE(noEventHandler.isHidden(), false);
}
void EventHandlerTest::body()
{
auto event = room->messageEvents().at(0).get();
@@ -523,6 +327,15 @@ void EventHandlerTest::body()
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullBody()
{
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getPlainBody(), QString());
}
void EventHandlerTest::genericBody_data()
{
QTest::addColumn<int>("eventNum");
@@ -545,6 +358,12 @@ void EventHandlerTest::genericBody()
QCOMPARE(eventHandler.getGenericBody(), output);
}
void EventHandlerTest::nullGenericBody()
{
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getGenericBody(), QString());
}
void EventHandlerTest::mediaInfo()
{
auto event = room->messageEvents().at(4).get();
@@ -568,6 +387,15 @@ void EventHandlerTest::mediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
}
void EventHandlerTest::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
@@ -581,6 +409,15 @@ void EventHandlerTest::linkPreviewer()
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::nullLinkPreviewer()
{
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::reactions()
{
auto event = room->messageEvents().at(0).get();
@@ -589,6 +426,15 @@ void EventHandlerTest::reactions()
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
}
void EventHandlerTest::nullReactions()
{
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReactions(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReactions(), nullptr);
}
void EventHandlerTest::hasReply()
{
auto event = room->messageEvents().at(5).get();
@@ -602,6 +448,12 @@ void EventHandlerTest::hasReply()
QCOMPARE(eventHandler.hasReply(), false);
}
void EventHandlerTest::nullHasReply()
{
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReply(), false);
}
void EventHandlerTest::replyId()
{
auto event = room->messageEvents().at(5).get();
@@ -615,6 +467,12 @@ void EventHandlerTest::replyId()
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
}
void EventHandlerTest::nullReplyId()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyId(), QString());
}
void EventHandlerTest::replyDelegateType()
{
auto event = room->messageEvents().at(5).get();
@@ -628,6 +486,15 @@ void EventHandlerTest::replyDelegateType()
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::nullReplyDelegateType()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
}
void EventHandlerTest::replyAuthor()
{
auto event = room->messageEvents().at(5).get();
@@ -651,6 +518,15 @@ void EventHandlerTest::replyAuthor()
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::nullReplyAuthor()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
}
void EventHandlerTest::replyBody()
{
auto event = room->messageEvents().at(5).get();
@@ -662,6 +538,15 @@ void EventHandlerTest::replyBody()
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
}
void EventHandlerTest::nullReplyBody()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReplyPlainBody called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyPlainBody(), QString());
}
void EventHandlerTest::replyMediaInfo()
{
auto event = room->messageEvents().at(6).get();
@@ -686,6 +571,47 @@ void EventHandlerTest::replyMediaInfo()
QCOMPARE(thumbnailInfo["height"_ls], 450);
}
void EventHandlerTest::nullReplyMediaInfo()
{
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
}
void EventHandlerTest::thread()
{
auto event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), false);
QCOMPARE(eventHandler.threadRoot(), QString());
event = room->messageEvents().at(9).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org"));
event = room->messageEvents().at(10).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.isThreaded(), true);
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
}
void EventHandlerTest::nullThread()
{
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
QCOMPARE(emptyHandler.isThreaded(), false);
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
QCOMPARE(noEventHandler.threadRoot(), QString());
}
void EventHandlerTest::location()
{
auto event = room->messageEvents().at(7).get();
@@ -696,6 +622,18 @@ void EventHandlerTest::location()
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
}
void EventHandlerTest::nullLocation()
{
QTest::ignoreMessage(QtWarningMsg, "getLatitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLatitude(), -100.0);
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLongitude(), -200.0);
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
}
void EventHandlerTest::readMarkers()
{
auto event = room->messageEvents().at(0).get();
@@ -725,5 +663,37 @@ void EventHandlerTest::readMarkers()
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
}
void EventHandlerTest::nullReadMarkers()
{
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.hasReadMarkers(), false);
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList());
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
}
void EventHandlerTest::cleanup()
{
eventHandler.setEvent(nullptr);
}
QTEST_MAIN(EventHandlerTest)
#include "eventhandlertest.moc"

View File

@@ -42,101 +42,18 @@ void NeoChatRoomTest::initTestCase()
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
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"
}
]
},
"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** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1235
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
QFile testMinSyncFile;
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
testMinSyncFile.open(QIODevice::ReadOnly);
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
room->update(std::move(roomData));
}
void NeoChatRoomTest::subtitleTextTest()
{
QCOMPARE(room->timelineSize(), 1);
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example\ntext message"));
}
void NeoChatRoomTest::eventTest()

View File

@@ -40,6 +40,7 @@ private Q_SLOTS:
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void formatBlockQuote();
void sendSimpleStringCase();
void sendSingleParaMarkup();
@@ -66,6 +67,7 @@ private Q_SLOTS:
void receiveRichEdited_data();
void receiveRichEdited();
void receiveLineSeparator();
void receiveRichCodeUrl();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
@@ -78,131 +80,11 @@ void TextHandlerTest::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"
}
]
},
"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** text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "/me This is an emote.",
"format": "org.matrix.custom.html",
"formatted_body": "This is an emote.",
"msgtype": "m.emote"
},
"event_id": "$153273582443PhrSn:example.org",
"origin_server_ts": 1532735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1231
}
},
{
"content": {
"body": "tested",
"msgtype": "m.text"
},
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
"origin_server_ts": 1680948575928,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1747776,
"m.relations": {
"m.replace": {
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
"origin_server_ts": 1680948580992,
"sender": "@example:example.org"
}
}
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
})EVENT");
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
QFile testTextHandlerSyncFile;
testTextHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-texthandler-sync.json"));
testTextHandlerSyncFile.open(QIODevice::ReadOnly);
const auto testTextHandlerSyncJson = QJsonDocument::fromJson(testTextHandlerSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testTextHandlerSyncJson.object());
room->update(std::move(roomData));
}
@@ -265,6 +147,16 @@ void TextHandlerTest::emptyCodeTags()
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}
void TextHandlerTest::formatBlockQuote()
{
auto input = QStringLiteral("<blockquote>\n<p>Lorem Ispum</p>\n</blockquote>");
auto expectedOutput = QStringLiteral("<blockquote><table><tr><td>\u201CLorem Ispum\u201D</td></tr></table></blockquote>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), expectedOutput);
}
void TextHandlerTest::sendSimpleStringCase()
{
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
@@ -577,8 +469,9 @@ void TextHandlerTest::receiveRichEdited_data()
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
QTest::newRow("blockquote")
<< QStringLiteral("<blockquote>Edited</blockquote>")
<< QStringLiteral("<blockquote><table><tr><td>\u201CEdited\u201D</td></tr></table></blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
}
void TextHandlerTest::receiveRichEdited()
@@ -647,5 +540,13 @@ void TextHandlerTest::linkPreviewsReject()
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
void TextHandlerTest::receiveRichCodeUrl()
{
auto input = QStringLiteral("<code>https://kde.org</code>");
TextHandler testTextHandler;
testTextHandler.setData(input);
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
}
QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"

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

@@ -11,6 +11,7 @@
</provides>
<name>NeoChat</name>
<name xml:lang="ar">نيوتشات</name>
<name xml:lang="ast">NeoChat</name>
<name xml:lang="az">NeoChat</name>
<name xml:lang="ca">NeoChat</name>
<name xml:lang="ca-valencia">NeoChat</name>
@@ -53,6 +54,7 @@
<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>
@@ -97,7 +99,7 @@ to provide a convergent experience across multiple platforms.</p>
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
<p xml:lang="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
@@ -282,7 +284,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">
<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>
@@ -309,7 +311,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">
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
<caption>Login screen</caption>
<caption xml:lang="ar">شاشة الدخول</caption>
@@ -341,6 +343,8 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24">
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
<description>

View File

@@ -4,6 +4,7 @@
Version=1.5
Name=NeoChat
Name[ar]=نيوتشات
Name[ast]=NeoChat
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat

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

4499
po/ast/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

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

@@ -40,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
@@ -127,6 +133,16 @@ add_library(neochat STATIC
mediasizehelper.h
eventhandler.cpp
enums/delegatetype.h
roomlastmessageprovider.cpp
roomlastmessageprovider.h
chatbarcache.cpp
chatbarcache.h
colorschemer.cpp
colorschemer.h
models/notificationsmodel.cpp
models/notificationsmodel.h
models/timelinemodel.cpp
models/timelinemodel.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -134,33 +150,33 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/main.qml
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
qml/ContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
qml/SpaceListContextMenu.qml
qml/UserInfo.qml
qml/LoadingPage.qml
qml/UserInfoDesktop.qml
qml/RoomPage.qml
qml/RoomWindow.qml
qml/JoinRoomPage.qml
qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml
qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml
qml/WelcomePage.qml
qml/General.qml
qml/Security.qml
qml/RoomSecurity.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
@@ -207,7 +223,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/Sso.qml
qml/UserDetailDialog.qml
qml/CreateRoomDialog.qml
qml/CreateSpaceDialog.qml
qml/EmojiDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
@@ -271,11 +286,26 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/RoomMedia.qml
qml/ChooseRoomDialog.qml
qml/ShareAction.qml
qml/SpaceHomePage.qml
qml/SpaceHierarchyDelegate.qml
qml/RemoveChildDialog.qml
qml/SelectParentDialog.qml
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
qml/AttachDialog.qml
qml/NotificationsView.qml
qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
ecm_qt_declare_logging_category(neochat
HEADER "messageeventmodel_logging.h"
IDENTIFIER "MessageEvent"
@@ -292,6 +322,13 @@ ecm_qt_declare_logging_category(neochat
DEFAULT_SEVERITY Info
)
ecm_qt_declare_logging_category(neochat
HEADER "chatdocumenthandler_logging.h"
IDENTIFIER "ChatDocumentHandling"
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
DEFAULT_SEVERITY Info
)
add_executable(neochat-app
main.cpp
)
@@ -312,16 +349,15 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
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)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF6::ConfigWidgets KF6::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_link_libraries(neochat PUBLIC KF6::WindowSystem ICU::uc)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
@@ -331,7 +367,28 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
endif()
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)
target_link_libraries(neochat PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
Qt::Gui
Qt::Multimedia
Qt::Network
Qt::QuickControls2
KF6::I18n
KF6::Kirigami
KF6::Notifications
KF6::ConfigCore
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::ColorScheme
KF6::ItemModels
QuotientQt6
cmark::cmark
QCoro::Core
QCoro::Network
)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -417,7 +474,9 @@ if(ANDROID)
"preferences-desktop-notification"
"computer-symbolic"
"gps"
"system-users-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
@@ -436,9 +495,18 @@ if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID)
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
endif()
endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
endif()

View File

@@ -38,45 +38,33 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
void ActionsHandler::handleNewMessage()
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
{
checkEffects(m_room->chatBoxText());
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
QUrl url(m_room->chatBoxAttachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
m_room->uploadFile(QUrl(path), m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : m_room->chatBoxText());
m_room->setChatBoxAttachmentPath({});
m_room->setChatBoxText({});
if (!chatBarCache) {
return;
}
QString handledText = m_room->chatBoxText();
handledText = handleMentions(handledText);
handleMessage(m_room->chatBoxText(), handledText);
checkEffects(chatBarCache->text());
if (!chatBarCache->attachmentPath().isEmpty()) {
QUrl url(chatBarCache->attachmentPath());
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
m_room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text());
chatBarCache->setAttachmentPath({});
chatBarCache->setText({});
return;
}
QString handledText = chatBarCache->text();
handledText = handleMentions(handledText, chatBarCache->mentions());
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
}
void ActionsHandler::handleEdit()
{
checkEffects(m_room->editText());
QString handledText = m_room->editText();
handledText = handleMentions(handledText, true);
handleMessage(m_room->editText(), handledText, true);
}
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
{
if (!m_room) {
return QString();
}
QVector<Mention> *mentions;
if (isEdit) {
mentions = m_room->editMentions();
} else {
mentions = m_room->mentions();
}
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
return a.cursor.anchor() > b.cursor.anchor();
});
@@ -87,14 +75,14 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
}
handledText = handledText.replace(mention.cursor.anchor(),
mention.cursor.position() - mention.cursor.anchor(),
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
}
mentions->clear();
return handledText;
}
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
{
if (NeoChatConfig::allowQuickEdit()) {
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
@@ -134,7 +122,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
for (const auto &action : ActionsModel::instance().allActions()) {
if (handledText.indexOf(action.prefix) == 1
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache);
if (action.messageType.has_value()) {
messageType = *action.messageType;
}
@@ -161,7 +149,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
return;
}
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : QString());
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
}
void ActionsHandler::checkEffects(const QString &text)

View File

@@ -8,6 +8,7 @@
#include <Quotient/events/roommessageevent.h>
#include "chatbarcache.h"
#include "neochatroom.h"
class NeoChatRoom;
@@ -51,21 +52,15 @@ Q_SIGNALS:
void showEffect(const QString &effect);
public Q_SLOTS:
/**
* @brief Pre-process text and send message.
* @brief Pre-process text and send message event.
*/
void handleNewMessage();
/**
* @brief Pre-process text and send edit.
*/
void handleEdit();
void handleMessageEvent(ChatBarCache *chatBarCache);
private:
NeoChatRoom *m_room = nullptr;
void checkEffects(const QString &text);
QString handleMentions(QString handledText, const bool &isEdit = false);
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
QString handleMentions(QString handledText, QList<Mention> *mentions);
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
};

View File

@@ -177,7 +177,7 @@ uint8_t *decode(const char *blurhash, int width, int height, int punch, int nCha
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
return NULL;
return nullptr;
}
return pixelArray;
}

177
src/chatbarcache.cpp Normal file
View File

@@ -0,0 +1,177 @@
// 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 "chatbarcache.h"
#include "eventhandler.h"
#include "neochatroom.h"
ChatBarCache::ChatBarCache(QObject *parent)
: QObject(parent)
{
}
QString ChatBarCache::text() const
{
return m_text;
}
void ChatBarCache::setText(const QString &text)
{
if (text == m_text) {
return;
}
m_text = text;
Q_EMIT textChanged();
}
bool ChatBarCache::isReplying() const
{
return m_relationType == Reply && !m_relationId.isEmpty();
}
QString ChatBarCache::replyId() const
{
if (m_relationType != Reply) {
return {};
}
return m_relationId;
}
void ChatBarCache::setReplyId(const QString &replyId)
{
if (m_relationType == Reply && m_relationId == replyId) {
return;
}
m_relationId = replyId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Reply;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
bool ChatBarCache::isEditing() const
{
return m_relationType == Edit && !m_relationId.isEmpty();
}
QString ChatBarCache::editId() const
{
if (m_relationType != Edit) {
return {};
}
return m_relationId;
}
void ChatBarCache::setEditId(const QString &editId)
{
if (m_relationType == Edit && m_relationId == editId) {
return;
}
m_relationId = editId;
if (m_relationId.isEmpty()) {
m_relationType = None;
} else {
m_relationType = Edit;
}
m_attachmentPath = QString();
Q_EMIT relationIdChanged();
Q_EMIT attachmentPathChanged();
}
QVariantMap ChatBarCache::relationUser() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
if (m_relationId.isEmpty()) {
return room->getUser(nullptr);
}
return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId()));
}
QString ChatBarCache::relationMessage() const
{
if (parent() == nullptr) {
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
if (m_relationId.isEmpty()) {
return {};
}
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
return {};
}
EventHandler eventhandler;
eventhandler.setRoom(room);
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
eventhandler.setEvent(&**event);
return eventhandler.getPlainBody();
}
return {};
}
bool ChatBarCache::isThreaded() const
{
return !m_threadId.isEmpty();
}
QString ChatBarCache::threadId() const
{
return m_threadId;
}
void ChatBarCache::setThreadId(const QString &threadId)
{
if (m_threadId == threadId) {
return;
}
m_threadId = threadId;
Q_EMIT threadIdChanged();
}
QString ChatBarCache::attachmentPath() const
{
return m_attachmentPath;
}
void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
{
if (attachmentPath == m_attachmentPath) {
return;
}
m_attachmentPath = attachmentPath;
m_relationType = None;
m_relationId = QString();
Q_EMIT attachmentPathChanged();
Q_EMIT relationIdChanged();
}
QList<Mention> *ChatBarCache::mentions()
{
return &m_mentions;
}
QString ChatBarCache::savedText() const
{
return m_savedText;
}
void ChatBarCache::setSavedText(const QString &savedText)
{
m_savedText = savedText;
}
#include "moc_chatbarcache.cpp"

201
src/chatbarcache.h Normal file
View File

@@ -0,0 +1,201 @@
// 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 <QTextCursor>
/**
* @brief Defines a user mention in the current chat or edit text.
*/
struct Mention {
QTextCursor cursor; /**< Contains the mention's text and position in the text. */
QString text; /**< The inserted text of the mention. */
int start = 0; /**< Start position of the mention. */
int position = 0; /**< End position of the mention. */
QString id; /**< The id the mention (used to create link when sending the message). */
};
/**
* @class ChatBarCache
*
* A class to cache data from a chat bar.
*
* A chat bar can be anything that allows users to compose or edit message, it doesn't
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
*
* This object is intended to allow the current contents of a chat bar to be cached
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
* have a separate cache for each chat bar.
*
* @note The NeoChatRoom which this component is created in is expected to be set
* as it's parent. This is necessary for certain functions which need to get
* relevant room information.
*
* @sa ChatBar, MessageEditComponent, NeoChatRoom
*/
class ChatBarCache : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief The text in the chat bar.
*
* Due to problems with QTextDocument, unlike the other properties here,
* text is *not* used to store the text when switching rooms.
*/
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
/**
* @brief Whether the chat bar is currently replying to a message.
*/
Q_PROPERTY(bool isReplying READ isReplying NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being replied to, if any.
*
* Will return empty if the RelationType is currently set to None or Edit.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString replyId READ replyId WRITE setReplyId NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is currently editing a message.
*/
Q_PROPERTY(bool isEditing READ isEditing NOTIFY relationIdChanged)
/**
* @brief The Matrix message ID of an event being edited, if any.
*
* Will return empty if the RelationType is currently set to None or Reply.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an reply or attachment.
*
* @sa RelationType
*/
Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged)
/**
* @brief Get the user for the message being replied to.
*
* 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.
*
* Returns an empty user if not replying to a message.
*
* The user QVariantMap has 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 getUser, Quotient::User
*/
Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
/**
* @brief The content of the related message.
*
* Will be QString() if no related message.
*/
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
/**
* @brief Whether the chat bar is replying in a thread.
*/
Q_PROPERTY(bool isThreaded READ isThreaded NOTIFY threadIdChanged)
/**
* @brief The Matrix message ID of thread root event, if any.
*/
Q_PROPERTY(QString threadId READ threadId WRITE setThreadId NOTIFY threadIdChanged)
/**
* @brief The local path for a file to send, if any.
*
* @note Replying, editing and attachments are exclusive so setting this will
* clear an edit or reply.
*/
Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged)
public:
/**
* @brief Describes the type of relation which relationId can refer to.
*
* A chat bar can only be relating to a single message at a time making these
* exclusive.
*/
enum RelationType {
Reply, /**< The current relation is a message being replied to. */
Edit, /**< The current relation is a message being edited. */
None, /**< There is currently no relation event */
};
Q_ENUM(RelationType)
explicit ChatBarCache(QObject *parent = nullptr);
QString text() const;
void setText(const QString &text);
bool isReplying() const;
QString replyId() const;
void setReplyId(const QString &replyId);
bool isEditing() const;
QString editId() const;
void setEditId(const QString &editId);
QVariantMap relationUser() const;
QString relationMessage() const;
bool isThreaded() const;
QString threadId() const;
void setThreadId(const QString &threadId);
QString attachmentPath() const;
void setAttachmentPath(const QString &attachmentPath);
/**
* @brief Retrieve the mentions for the current chat bar text.
*/
QList<Mention> *mentions();
/**
* @brief Get the saved chat bar text.
*/
QString savedText() const;
/**
* @brief Save the chat bar text.
*/
void setSavedText(const QString &savedText);
Q_SIGNALS:
void textChanged();
void relationIdChanged();
void threadIdChanged();
void attachmentPathChanged();
private:
QString m_text = QString();
QString m_relationId = QString();
RelationType m_relationType = RelationType::None;
QString m_threadId = QString();
QString m_attachmentPath = QString();
QList<Mention> m_mentions;
QString m_savedText;
};

View File

@@ -14,6 +14,8 @@
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Settings>
#include "chatdocumenthandler_logging.h"
class SyntaxHighlighter : public QSyntaxHighlighter
{
public:
@@ -57,11 +59,12 @@ public:
setFormat(error.first, error.second.size(), errorFormat);
}
}
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
auto room = handler->room();
if (!room) {
return;
}
auto mentions = room->mentions();
auto mentions = handler->chatBarCache()->mentions();
mentions->erase(std::remove_if(mentions->begin(),
mentions->end(),
[this](auto &mention) {
@@ -97,21 +100,16 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
, m_document(nullptr)
, m_cursorPosition(-1)
, m_highlighter(new SyntaxHighlighter(this))
, m_completionModel(new CompletionModel())
, m_completionModel(new CompletionModel(this))
{
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
m_completionModel->setRoom(m_room);
static QPointer<NeoChatRoom> previousRoom = nullptr;
if (previousRoom) {
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
}
previousRoom = m_room;
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
int start = completionStartIndex();
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
});
@@ -134,11 +132,7 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
#endif
const qsizetype cursor = cursorPosition();
const auto &text = getText();
auto start = std::min(cursor, text.size()) - 1;
@@ -152,20 +146,6 @@ int ChatDocumentHandler::completionStartIndex() const
return start;
}
bool ChatDocumentHandler::isEdit() const
{
return m_isEdit;
}
void ChatDocumentHandler::setIsEdit(bool edit)
{
if (edit == m_isEdit) {
return;
}
m_isEdit = edit;
Q_EMIT isEditChanged();
}
QQuickTextDocument *ChatDocumentHandler::document() const
{
return m_document;
@@ -215,8 +195,31 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
Q_EMIT roomChanged();
}
ChatBarCache *ChatDocumentHandler::chatBarCache() const
{
return m_chatBarCache;
}
void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
{
if (m_chatBarCache == chatBarCache) {
return;
}
m_chatBarCache = chatBarCache;
Q_EMIT chatBarCacheChanged();
}
void ChatDocumentHandler::complete(int index)
{
if (m_document == nullptr) {
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
return;
}
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
@@ -300,26 +303,20 @@ void ChatDocumentHandler::setSelectionEnd(int position)
QString ChatDocumentHandler::getText() const
{
if (!m_room) {
return QString();
}
if (m_isEdit) {
return m_room->editText();
} else {
return m_room->chatBoxText();
if (!m_chatBarCache) {
qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
return {};
}
return m_chatBarCache->text();
}
void ChatDocumentHandler::pushMention(const Mention mention) const
{
if (!m_room) {
if (!m_chatBarCache) {
qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
return;
}
if (m_isEdit) {
m_room->editMentions()->push_back(mention);
} else {
m_room->mentions()->push_back(mention);
}
m_chatBarCache->mentions()->push_back(mention);
}
QColor ChatDocumentHandler::mentionColor() const

View File

@@ -8,6 +8,7 @@
#include <QQuickTextDocument>
#include <QTextCursor>
#include "chatbarcache.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
@@ -61,14 +62,6 @@ class ChatDocumentHandler : public QObject
Q_OBJECT
QML_ELEMENT
/**
* @brief Is the instance being used to handle an edit message.
*
* This is needed to ensure that the text and mentions are saved and retrieved
* from the correct parameters in the assigned room.
*/
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
/**
* @brief The QQuickTextDocument that is being handled.
*/
@@ -95,13 +88,18 @@ class ChatDocumentHandler : public QObject
* This is typically provided to a qml component to visualise the current
* completion results.
*/
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
/**
* @brief The current room that the the text document is being handled for.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The cache for the chat bar the text document is being handled for.
*/
Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged)
/**
* @brief The color to highlight user mentions.
*/
@@ -115,9 +113,6 @@ class ChatDocumentHandler : public QObject
public:
explicit ChatDocumentHandler(QObject *parent = nullptr);
[[nodiscard]] bool isEdit() const;
void setIsEdit(bool edit);
[[nodiscard]] QQuickTextDocument *document() const;
void setDocument(QQuickTextDocument *document);
@@ -133,9 +128,11 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] ChatBarCache *chatBarCache() const;
void setChatBarCache(ChatBarCache *chatBarCache);
Q_INVOKABLE void complete(int index);
void updateCompletions();
CompletionModel *completionModel() const;
[[nodiscard]] QColor mentionColor() const;
@@ -145,11 +142,10 @@ public:
void setErrorColor(const QColor &color);
Q_SIGNALS:
void isEditChanged();
void documentChanged();
void cursorPositionChanged();
void roomChanged();
void completionModelChanged();
void chatBarCacheChanged();
void selectionStartChanged();
void selectionEndChanged();
void errorColorChanged();
@@ -158,12 +154,10 @@ Q_SIGNALS:
private:
int completionStartIndex() const;
bool m_isEdit = false;
QPointer<QQuickTextDocument> m_document;
QPointer<NeoChatRoom> m_room;
bool completionVisible = false;
QPointer<ChatBarCache> m_chatBarCache;
QColor m_mentionColor;
QColor m_errorColor;
@@ -177,7 +171,5 @@ private:
SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
CompletionModel *m_completionModel = nullptr;
};

View File

@@ -6,13 +6,8 @@
#include <qt6keychain/keychain.h>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KWindowConfig>
#ifdef HAVE_WINDOWSYSTEM
#include <KWindowEffects>
#endif
#include <QFile>
#include <QFileInfo>
@@ -47,6 +42,8 @@
#include "trayicon_sni.h"
#endif
bool testMode = false;
using namespace Quotient;
Controller::Controller(QObject *parent)
@@ -61,11 +58,22 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif
QTimer::singleShot(0, this, [this] {
invokeLogin();
});
if (!testMode) {
QTimer::singleShot(0, this, [this] {
invokeLogin();
});
} else {
auto c = new NeoChatConnection(this);
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
Q_EMIT initiated();
});
}
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [] {
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
NeoChatConfig::self()->save();
});
@@ -97,16 +105,30 @@ Controller::Controller(QObject *parent)
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
connection->setupPushNotifications(m_endpoint);
});
}
oldAccountCount = m_accountRegistry.size();
});
QTimer::singleShot(0, this, [this] {
m_pushRuleModel = new PushRuleModel;
#ifdef HAVE_KUNIFIEDPUSH
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) {
m_endpoint = endpoint;
for (auto &quotientConnection : m_accountRegistry) {
auto connection = dynamic_cast<NeoChatConnection *>(quotientConnection);
connection->setupPushNotifications(endpoint);
}
});
connector->registerClient(i18n("Receiving push notifications"));
m_endpoint = connector->endpoint();
#endif
}
Controller &Controller::instance()
@@ -175,6 +197,8 @@ void Controller::invokeLogin()
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
@@ -194,6 +218,8 @@ void Controller::invokeLogin()
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
Q_EMIT accountsLoadingChanged();
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
@@ -304,9 +330,6 @@ void Controller::setQuitOnLastWindowClosed()
m_trayIcon = nullptr;
}
}
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
#else
return;
#endif
}
@@ -330,20 +353,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &NeoChatConnection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &NeoChatConnection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
@@ -354,12 +363,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged();
Q_EMIT activeConnectionIndexChanged();
}
PushRuleModel *Controller::pushRuleModel() const
{
return m_pushRuleModel;
}
void Controller::saveWindowGeometry()
@@ -367,79 +370,25 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
bool Controller::isOnline() const
{
return m_isOnline;
}
// TODO: Remove in favor of RoomManager::joinRoom
void Controller::joinRoom(const QString &alias)
{
if (!alias.contains(":"_ls)) {
Q_EMIT errorOccured(i18n("The room id you are trying to join is not valid"));
return;
}
const auto knownServer = alias.mid(alias.indexOf(":"_ls) + 1);
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
}
void Controller::openOrCreateDirectChat(User *user)
{
const auto existing = activeConnection()->directChats();
if (existing.contains(user)) {
const auto &room = static_cast<NeoChatRoom *>(activeConnection()->room(existing.value(user)));
if (room) {
RoomManager::instance().enterRoom(room);
return;
}
}
activeConnection()->requestDirectChat(user);
}
QString Controller::formatByteSize(double size, int precision) const
{
return QLocale().formattedDataSize(size, precision);
}
QString Controller::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const
{
return KFormat().formatDuration(msecs, options);
}
void Controller::setBlur(QQuickItem *item, bool blur)
{
#ifdef HAVE_WINDOWSYSTEM
auto setWindows = [item, blur]() {
auto reg = QRect(QPoint(0, 0), item->window()->size());
KWindowEffects::enableBackgroundContrast(item->window(), blur, 1, 1, 1, reg);
KWindowEffects::enableBlurBehind(item->window(), blur, reg);
};
disconnect(item->window(), &QQuickWindow::heightChanged, this, nullptr);
disconnect(item->window(), &QQuickWindow::widthChanged, this, nullptr);
connect(item->window(), &QQuickWindow::heightChanged, this, setWindows);
connect(item->window(), &QQuickWindow::widthChanged, this, setWindows);
setWindows();
#endif
}
bool Controller::hasWindowSystem() const
{
#ifdef HAVE_WINDOWSYSTEM
return true;
#else
return false;
#endif
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
}
void Controller::listenForNotifications()
{
#ifdef HAVE_KUNIFIEDPUSH
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) {
NotificationsManager::instance().postPushNotification(data);
});
connector->registerClient(i18n("Receiving push notifications"));
#endif
}
void Controller::setApplicationProxy()
{
NeoChatConfig *cfg = NeoChatConfig::self();
@@ -470,14 +419,6 @@ void Controller::setApplicationProxy()
QNetworkProxy::setApplicationProxy(proxy);
}
int Controller::activeConnectionIndex() const
{
auto result = std::find_if(m_accountRegistry.accounts().begin(), m_accountRegistry.accounts().end(), [this](const auto &it) {
return it == m_connection;
});
return result - m_accountRegistry.accounts().begin();
}
bool Controller::isFlatpak() const
{
#ifdef NEOCHAT_FLATPAK
@@ -493,3 +434,8 @@ AccountRegistry &Controller::accounts()
}
#include "moc_controller.cpp"
void Controller::setTestMode(bool test)
{
testMode = test;
}

View File

@@ -3,18 +3,19 @@
#pragma once
#include "models/pushrulemodel.h"
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <KFormat>
#include "neochatconnection.h"
#include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
#ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h>
#endif
class NeoChatRoom;
class TrayIcon;
class QQuickTextDocument;
@@ -49,31 +50,11 @@ class Controller : public QObject
*/
Q_PROPERTY(NeoChatConnection *activeConnection READ activeConnection WRITE setActiveConnection NOTIFY activeConnectionChanged)
/**
* @brief The PushRuleModel that has the active connection's push rules.
*/
Q_PROPERTY(PushRuleModel *pushRuleModel READ pushRuleModel CONSTANT)
/**
* @brief The row number in the accounts directory of the active connection.
*/
Q_PROPERTY(int activeConnectionIndex READ activeConnectionIndex NOTIFY activeConnectionIndexChanged)
/**
* @brief Whether the OS NeoChat is running on supports sytem tray icons.
*/
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
/**
* @brief Whether KWindowSystem specific features are available.
*/
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
/**
* @brief Whether NeoChat is running as a flatpak.
*
@@ -81,6 +62,8 @@ class Controller : public QObject
*/
Q_PROPERTY(bool isFlatpak READ isFlatpak CONSTANT)
Q_PROPERTY(QStringList accountsLoading MEMBER m_accountsLoading NOTIFY accountsLoadingChanged)
public:
/**
* @brief Defines the status after an attempt to change the password on an account.
@@ -93,16 +76,15 @@ public:
Q_ENUM(PasswordStatus)
static Controller &instance();
static Controller *create(QQmlEngine *, QJSEngine *)
static Controller *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
void setActiveConnection(NeoChatConnection *connection);
[[nodiscard]] NeoChatConnection *activeConnection() const;
[[nodiscard]] PushRuleModel *pushRuleModel() const;
/**
* @brief Add a new connection to the account registry.
*/
@@ -113,34 +95,13 @@ public:
*/
void dropConnection(NeoChatConnection *c);
int activeConnectionIndex() const;
/**
* @brief Save an access token to the keychain for the given account.
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Join a room.
*/
Q_INVOKABLE void joinRoom(const QString &alias);
/**
* @brief Join a direct chat with the given user.
*
* If a direct chat with the user doesn't exist one is created and then joined.
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
[[nodiscard]] bool supportSystemTray() const;
/**
* @brief Set the background blur status of the given item.
*/
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
bool isOnline() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
@@ -150,20 +111,6 @@ public:
bool isFlatpak() const;
/**
* @brief Return a string for the input timestamp.
*
* The output format depends on the KFormat::DurationFormatOptions chosen.
*
* @sa KFormat::DurationFormatOptions
*/
Q_INVOKABLE QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options = KFormat::DefaultDuration) const;
/**
* @brief Return a human readable string for a given input number of bytes.
*/
Q_INVOKABLE QString formatByteSize(double size, int precision = 1) const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
@@ -171,8 +118,16 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
/**
* @brief Start listening for notifications in dbus-activated mode.
* These notifications will quit the application when closed.
*/
static void listenForNotifications();
Quotient::AccountRegistry &accounts();
static void setTestMode(bool testMode);
private:
explicit Controller(QObject *parent = nullptr);
@@ -183,13 +138,11 @@ private:
void loadSettings();
void saveSettings() const;
bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
QPointer<PushRuleModel> m_pushRuleModel;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
QString m_endpoint;
private Q_SLOTS:
void invokeLogin();
@@ -211,8 +164,7 @@ Q_SIGNALS:
void activeConnectionChanged();
void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url);
void isOnlineChanged(bool isOnline);
void activeConnectionIndexChanged();
void accountsLoadingChanged();
public Q_SLOTS:
void saveWindowGeometry();

View File

@@ -40,6 +40,8 @@ public:
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). */
Loading, /**< A delegate to tell the user more messages are being loaded. */
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -19,6 +19,7 @@
#include <Quotient/events/stickerevent.h>
#include <Quotient/quotient_common.h>
#include "delegatetype.h"
#include "eventhandler_logging.h"
#include "events/pollevent.h"
#include "linkpreviewer.h"
@@ -50,6 +51,10 @@ const Quotient::Event *EventHandler::getEvent() const
void EventHandler::setEvent(const Quotient::RoomEvent *event)
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "cannot setEvent when m_room is set to nullptr.";
return;
}
if (event == m_event) {
return;
}
@@ -164,6 +169,28 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
}
}
QString EventHandler::singleLineAuthorDisplayname(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 {};
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
auto displayName = m_room->htmlSafeMemberName(author->id());
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br />"), QStringLiteral(" "));
displayName.replace(u'\n', QStringLiteral(" "));
displayName.replace(u'\u2028', QStringLiteral(" "));
return displayName;
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
@@ -181,7 +208,7 @@ QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
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.";
qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
return {};
}
if (isPending && lastUpdated == QDateTime()) {
@@ -216,6 +243,15 @@ bool EventHandler::isHighlighted()
bool EventHandler::isHidden()
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
return false;
}
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
return true;
}
@@ -611,6 +647,14 @@ QString EventHandler::getGenericBody() const
QVariantMap EventHandler::getMediaInfo() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
return {};
}
return getMediaInfoForEvent(m_event);
}
@@ -641,12 +685,12 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
if (source.isValid()) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();
@@ -723,6 +767,14 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_room set to nullptr.";
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLinkPreviewer called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
return nullptr;
}
@@ -754,7 +806,7 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return {};
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
@@ -806,16 +858,33 @@ QSharedPointer<ReactionModel> EventHandler::getReactions() const
bool EventHandler::hasReply() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
return false;
}
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
}
QString EventHandler::getReplyId() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
return {};
}
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
}
DelegateType::Type EventHandler::getReplyDelegateType() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
return DelegateType::Other;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
return DelegateType::Other;
}
auto replyEvent = m_room->getReplyForEvent(*m_event);
if (replyEvent == nullptr) {
return DelegateType::Other;
@@ -901,8 +970,45 @@ QVariantMap EventHandler::getReplyMediaInfo() const
return getMediaInfoForEvent(replyPtr);
}
bool EventHandler::isThreaded() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
return false;
}
return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
|| (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
}
QString EventHandler::threadRoot() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
return {};
}
// Get the thread root ID from m.relates_to if it exists.
if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
&& m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
}
// For thread root events they have an m.relations in the unsigned part with a m.thread object.
// If so return the event ID as it is the root.
if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
return getId();
}
return {};
}
float EventHandler::getLatitude() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
return -100.0;
}
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.
@@ -913,6 +1019,11 @@ float EventHandler::getLatitude() const
float EventHandler::getLongitude() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
return -200.0;
}
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.
@@ -923,6 +1034,11 @@ float EventHandler::getLongitude() const
QString EventHandler::getLocationAssetType() const
{
if (m_event == nullptr) {
qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
return {};
}
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
if (assetType.isEmpty()) {
return {};
@@ -932,6 +1048,15 @@ QString EventHandler::getLocationAssetType() const
bool EventHandler::hasReadMarkers() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
return false;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
return false;
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
return userIds.size() > 0;
@@ -939,6 +1064,15 @@ bool EventHandler::hasReadMarkers() const
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
userIds_temp.remove(m_room->localUser()->id());
@@ -959,6 +1093,15 @@ QVariantList EventHandler::getReadMarkers(int maxMarkers) const
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
@@ -971,6 +1114,15 @@ QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
QString EventHandler::getReadMarkersString() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
return {};
}
auto userIds = m_room->userIdsAtEvent(m_event->id());
userIds.remove(m_room->localUser()->id());
@@ -986,3 +1138,5 @@ QString EventHandler::getReadMarkersString() const
readMarkersString.chop(2);
return readMarkersString;
}
#include "moc_eventhandler.cpp"

View File

@@ -111,6 +111,17 @@ public:
*/
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Get the display name of the event author but with any newlines removed.
*
* Turns out you can put newlines in your display name so we need to handle that
* primarily for the room list subtitle.
*
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString singleLineAuthorDisplayname(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/
@@ -326,6 +337,20 @@ public:
*/
QVariantMap getReplyMediaInfo() const;
/**
* @brief Whether the message is part of a thread.
*
* i.e. There is a rel_type of m.thread.
*/
bool isThreaded() const;
/**
* @brief Return the Matrix ID of the thread's root message.
*
* Empty if this not part of a thread.
*/
QString threadRoot() const;
/**
* @brief Return the latitude for the event.
*

View File

@@ -3,7 +3,7 @@
#pragma once
#include <QVector>
#include <QList>
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/stateevent.h>
@@ -60,7 +60,7 @@ public:
*
* @sa ImagePackImage
*/
QVector<ImagePackEventContent::ImagePackImage> images;
QList<ImagePackEventContent::ImagePackImage> images;
explicit ImagePackEventContent(const QJsonObject &o);

View File

@@ -12,7 +12,8 @@ class LocationHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
QML_SINGLETON
public:
/** Unite two rectanlges. */
Q_INVOKABLE static QRectF unite(const QRectF &r1, const QRectF &r2);

View File

@@ -117,7 +117,7 @@ public:
file.write(buf.constData(), buf.size());
file.flush();
if (oldHandler && (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled)) {
if (oldHandler && (!context.category || (strcmp(context.category, "quotient.e2ee") != 0 || e2eeDebugEnabled))) {
oldHandler(type, context, message);
}
}
@@ -197,7 +197,6 @@ void messageHandler(QtMsgType type, const QMessageLogContext &context, const QSt
break;
case QtFatalMsg:
sInstance()->log(QtInfoMsg, context, message);
abort();
}
}
@@ -212,7 +211,7 @@ void filter(QLoggingCategory *category)
void initLogging()
{
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtDebugMsg).isEnabled(QtDebugMsg);
e2eeDebugEnabled = QLoggingCategory("quotient.e2ee", QtInfoMsg).isEnabled(QtDebugMsg);
oldCategoryFilter = QLoggingCategory::installFilter(filter);
oldHandler = qInstallMessageHandler(messageHandler);
sInstance->setOrigHandler(oldHandler);

View File

@@ -100,7 +100,8 @@ void LoginHelper::init()
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connectSingleShot(m_connection, &Connection::syncDone, this, []() {
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
Q_EMIT loaded();
Q_EMIT Controller::instance().initiated();
});
}

View File

@@ -79,7 +79,17 @@ class LoginHelper : public QObject
Q_PROPERTY(bool isInvalidPassword READ isInvalidPassword NOTIFY isInvalidPasswordChanged)
public:
explicit LoginHelper(QObject *parent = nullptr);
static LoginHelper &instance()
{
static LoginHelper _instance;
return _instance;
}
static LoginHelper *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
Q_INVOKABLE void init();
@@ -125,6 +135,7 @@ Q_SIGNALS:
void isLoggingInChanged();
void isLoggedInChanged();
void isInvalidPasswordChanged();
void loaded();
private:
void setHomeserverReachable(bool reachable);
@@ -141,4 +152,5 @@ private:
bool m_isLoggingIn = false;
bool m_isLoggedIn = false;
bool m_invalidPassword = false;
explicit LoginHelper(QObject *parent = nullptr);
};

View File

@@ -34,69 +34,21 @@
#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 "controller.h"
#include "delegatesizehelper.h"
#include "enums/delegatetype.h"
#include "linkpreviewer.h"
#include "locationhelper.h"
#include "logger.h"
#include "login.h"
#include "matriximageprovider.h"
#include "mediasizehelper.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 "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"
#include "controller.h"
#include "logger.h"
#include "matriximageprovider.h"
#include "neochatconfig.h"
#include "roommanager.h"
#include "windowcontroller.h"
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
#include "registration.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
@@ -169,7 +121,7 @@ int main(int argc, char *argv[])
font.setHintingPreference(QFont::PreferNoHinting);
app.setFont(font);
#endif
KLocalizedString::setApplicationDomain("neochat");
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
QGuiApplication::setOrganizationName("KDE"_ls);
@@ -179,7 +131,11 @@ int main(int argc, char *argv[])
i18n("Matrix client"),
KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community"));
about.addAuthor(i18n("Carl Schwan"), i18n("Maintainer"), QStringLiteral("carl@carlschwan.eu"), QStringLiteral("https://carlschwan.eu"));
about.addAuthor(i18n("Carl Schwan"),
i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"),
QStringLiteral("https://carlschwan.eu/avatar.png"));
about.addAuthor(i18n("Tobias Fella"), i18n("Maintainer"), QStringLiteral("tobias.fella@kde.org"), QStringLiteral("https://tobiasfella.de"));
about.addAuthor(i18n("James Graham"), i18n("Maintainer"), QStringLiteral("james.h.graham@protonmail.com"));
about.addCredit(i18n("Black Hat"), i18n("Original author of Spectral"), QStringLiteral("bhat@encom.eu.org"));
@@ -188,7 +144,7 @@ int main(int argc, char *argv[])
about.setOrganizationDomain("kde.org");
about.addComponent(QStringLiteral("libQuotient"),
i18n("A Qt5 library to write cross-platform clients for Matrix"),
i18n("A Qt library to write cross-platform clients for Matrix"),
i18nc("<version number> (built against <possibly different version number>)",
"%1 (built against %2)",
Quotient::versionString(),
@@ -201,9 +157,7 @@ int main(int argc, char *argv[])
initLogging();
#if Quotient_VERSION_MINOR == 8
Connection::setEncryptionDefault(true);
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
@@ -212,23 +166,54 @@ int main(int argc, char *argv[])
QStringLiteral("/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"));
#endif
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
if (!NeoChatConfig::self()->colorScheme().isEmpty()) {
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
QCommandLineOption testOption("test"_ls, i18n("Only used for autotests"));
testOption.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(testOption);
#ifdef HAVE_KUNIFIEDPUSH
QCommandLineOption dbusActivatedOption(QStringLiteral("dbus-activated"), i18n("Internal usage only."));
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
parser.addOption(dbusActivatedOption);
#endif
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
Controller::setTestMode(parser.isSet("test"_ls));
#ifdef HAVE_KUNIFIEDPUSH
if (parser.isSet(dbusActivatedOption)) {
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
Controller::listenForNotifications();
return QCoreApplication::exec();
}
#endif
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
#endif
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());
// qmlRegisterUncreatableType<KeyVerificationSession>("org.kde.neochat", 1, 0, "KeyVerificationSession", {});
qmlRegisterUncreatableType<KeyVerificationSession>("com.github.quotient_im.libquotient", 1, 0, "KeyVerificationSession", {});
QQmlApplicationEngine engine;
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
service.connect(&service,
&KDBusService::activateRequested,
&RoomManager::instance(),
@@ -247,7 +232,7 @@ int main(int argc, char *argv[])
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().openResource(arg);
RoomManager::instance().resolveResource(arg);
}
});
#endif
@@ -256,22 +241,13 @@ int main(int argc, char *argv[])
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
if (parser.isSet("ignore-ssl-errors"_ls)) {
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
reply->ignoreSslErrors();
});
}
engine.addImageProvider(QLatin1String("mxc"), new MatrixImageProvider);
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));
@@ -284,8 +260,8 @@ int main(int argc, char *argv[])
}
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, &runner, QDBusConnection::ExportScriptableContents);
auto runner = Runner::create(&engine, &engine);
QDBusConnection::sessionBus().registerObject("/RoomRunner"_ls, runner, QDBusConnection::ExportScriptableContents);
#endif
QWindow *window = windowFromEngine(&engine);

View File

@@ -13,11 +13,13 @@
#include <KLocalizedString>
#include "controller.h"
#include "neochatconnection.h"
#include <Quotient/connection.h>
using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id))
, requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
@@ -25,6 +27,7 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
@@ -51,24 +54,24 @@ ThumbnailResponse::ThumbnailResponse(QString id, QSize size)
return;
}
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
qWarning() << "Current connection is null";
return;
}
// Execute a request on the main thread asynchronously
moveToThread(Controller::instance().activeConnection()->thread());
moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
return;
}
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == Controller::instance().activeConnection()->thread());
job = Controller::instance().activeConnection()->getThumbnail(mediaId, requestedSize);
Q_ASSERT(QThread::currentThread() == m_connection->thread());
job = m_connection->getThumbnail(mediaId, requestedSize);
// Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
@@ -118,7 +121,7 @@ QString ThumbnailResponse::errorString() const
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new ThumbnailResponse(id, requestedSize);
return new ThumbnailResponse(id, requestedSize, m_connection);
}
#include "moc_matriximageprovider.cpp"

View File

@@ -10,6 +10,8 @@
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
@@ -21,7 +23,7 @@ class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize);
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
@@ -33,6 +35,7 @@ private:
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
NeoChatConnection *m_connection;
QImage image;
QString errorStr;
@@ -51,11 +54,27 @@ private:
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider *instance = new MatrixImageProvider;
engine->setObjectOwnership(instance, QQmlEngine::CppOwnership);
return instance;
}
/**
* @brief Return a job to provide the image with the given ID.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
NeoChatConnection *m_connection = nullptr;
MatrixImageProvider() = default;
};

View File

@@ -7,10 +7,10 @@
#include <QAbstractListModel>
#include <QCoroTask>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
#include <Quotient/connection.h>

View File

@@ -3,6 +3,7 @@
#include "actionsmodel.h"
#include "chatbarcache.h"
#include "controller.h"
#include "neochatroom.h"
#include "roommanager.h"
@@ -19,7 +20,7 @@ QStringList rainbowColors{"#ff2b00"_ls, "#ff5500"_ls, "#ff8000"_ls, "#ffaa00"_ls
"#00d4ff"_ls, "#00aaff"_ls, "#007fff"_ls, "#0055ff"_ls, "#002bff"_ls, "#0000ff"_ls, "#2a00ff"_ls, "#5500ff"_ls, "#7f00ff"_ls,
"#aa00ff"_ls, "#d400ff"_ls, "#ff00ff"_ls, "#ff00d4"_ls, "#ff00aa"_ls, "#ff0080"_ls, "#ff0055"_ls, "#ff002b"_ls, "#ff0000"_ls};
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18n("Leaving this room."));
room->connection()->leaveRoom(room);
@@ -45,7 +46,7 @@ auto leaveRoomLambda = [](const QString &text, NeoChatRoom *room) {
return QString();
};
auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
auto roomNickLambda = [](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -54,10 +55,10 @@ auto roomNickLambda = [](const QString &text, NeoChatRoom *room) {
return QString();
};
QVector<ActionsModel::Action> actions{
QList<ActionsModel::Action> actions{
Action{
QStringLiteral("shrug"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("¯\\\\_(ツ)_/¯ %1").arg(message);
},
true,
@@ -67,7 +68,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("lenny"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("( ͡° ͜ʖ ͡°) %1").arg(message);
},
true,
@@ -77,7 +78,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("tableflip"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("(╯°□°)╯︵ ┻━┻ %1").arg(message);
},
true,
@@ -87,7 +88,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unflip"),
[](const QString &message, NeoChatRoom *) {
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return QStringLiteral("┬──┬ ( ゜-゜ノ) %1").arg(message);
},
true,
@@ -97,7 +98,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbow"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -106,8 +107,8 @@ QVector<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -117,7 +118,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("rainbowme"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
QString rainbowText;
for (int i = 0; i < text.length(); i++) {
rainbowText += QStringLiteral("<font color='%2'>%3</font>").arg(rainbowColors[i % rainbowColors.length()], text.at(i));
@@ -126,8 +127,8 @@ QVector<ActionsModel::Action> actions{
room->postMessage(QStringLiteral("/rainbow %1").arg(text),
rainbowText,
RoomMessageEvent::MsgType::Emote,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -137,7 +138,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("plain"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
room->postMessage(text, text.toHtmlEscaped(), RoomMessageEvent::MsgType::Text, {}, {});
return QString();
},
@@ -148,13 +149,13 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("spoiler"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
// Ideally, we would just return rainbowText and let that do the rest, but the colors don't survive markdownToHTML.
room->postMessage(QStringLiteral("/spoiler %1").arg(text),
QStringLiteral("<span data-mx-spoiler>%1</span>").arg(text),
RoomMessageEvent::MsgType::Text,
room->chatBoxReplyId(),
room->chatBoxEditId());
chatBarCache->replyId(),
chatBarCache->editId());
return QString();
},
false,
@@ -164,7 +165,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("me"),
[](const QString &text, NeoChatRoom *) {
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
true,
@@ -174,7 +175,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("notice"),
[](const QString &text, NeoChatRoom *) {
[](const QString &text, NeoChatRoom *, ChatBarCache *) {
return text;
},
true,
@@ -184,7 +185,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("invite"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -220,7 +221,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("join"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -234,7 +235,7 @@ QVector<ActionsModel::Action> actions{
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Controller::instance().joinRoom(text);
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
false,
@@ -244,7 +245,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("knock"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
QString roomName = parts[0];
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
@@ -260,7 +261,7 @@ QVector<ActionsModel::Action> actions{
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Knocking room <roomname>.", "Knocking room %1.", text));
auto connection = Controller::instance().activeConnection();
auto connection = room->connection();
const auto knownServer = roomName.mid(roomName.indexOf(":"_ls) + 1);
if (parts.length() >= 2) {
RoomManager::instance().knockRoom(connection, roomName, parts[1], QStringList{knownServer});
@@ -276,7 +277,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("j"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
QRegularExpression roomRegex(QStringLiteral(R"(^[#!][^:]+:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?)"));
auto regexMatch = roomRegex.match(text);
if (!regexMatch.hasMatch()) {
@@ -284,12 +285,12 @@ QVector<ActionsModel::Action> actions{
i18nc("'<text>' does not look like a room id or alias.", "'%1' does not look like a room id or alias.", text));
return QString();
}
if (Controller::instance().activeConnection()->room(text) || Controller::instance().activeConnection()->roomByAlias(text)) {
if (room->connection()->room(text) || room->connection()->roomByAlias(text)) {
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("You are already in room <roomname>.", "You are already in room %1.", text));
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Controller::instance().joinRoom(text);
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
false,
@@ -315,7 +316,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("nick"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
if (text.isEmpty()) {
Q_EMIT room->showMessage(NeoChatRoom::Error, i18n("No new nickname provided, no changes will happen."));
} else {
@@ -346,7 +347,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ignore"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -374,7 +375,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unignore"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -402,9 +403,8 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("react"),
[](const QString &text, NeoChatRoom *room) {
QString replyEventId = room->chatBoxReplyId();
if (replyEventId.isEmpty()) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *chatBarCache) {
if (chatBarCache->replyId().isEmpty()) {
for (auto it = room->messageEvents().crbegin(); it != room->messageEvents().crend(); it++) {
const auto &evt = **it;
if (const auto event = eventCast<const RoomMessageEvent>(&evt)) {
@@ -413,7 +413,7 @@ QVector<ActionsModel::Action> actions{
}
}
}
room->toggleReaction(replyEventId, text);
room->toggleReaction(chatBarCache->replyId(), text);
return QString();
},
false,
@@ -423,7 +423,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("ban"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -462,7 +462,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("unban"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
auto regexMatch = mxidRegex.match(text);
@@ -495,7 +495,7 @@ QVector<ActionsModel::Action> actions{
},
Action{
QStringLiteral("kick"),
[](const QString &text, NeoChatRoom *room) {
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
auto parts = text.split(QLatin1String(" "));
static const QRegularExpression mxidRegex(
QStringLiteral(R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"));
@@ -574,7 +574,7 @@ QHash<int, QByteArray> ActionsModel::roleNames() const
};
}
QVector<Action> &ActionsModel::allActions() const
QList<Action> &ActionsModel::allActions() const
{
return actions;
}

View File

@@ -7,6 +7,7 @@
#include <QAbstractListModel>
#include <Quotient/events/roommessageevent.h>
class ChatBarCache;
class NeoChatRoom;
/**
@@ -28,7 +29,7 @@ public:
/**
* @brief The function to execute when the action is triggered.
*/
std::function<QString(const QString &, NeoChatRoom *)> handle;
std::function<QString(const QString &, NeoChatRoom *, ChatBarCache *)> handle;
/**
* @brief Whether the action is a message type action.
*
@@ -87,7 +88,7 @@ public:
/**
* @brief Return a vector with all supported actions.
*/
QVector<Action> &allActions() const;
QList<Action> &allActions() const;
private:
ActionsModel() = default;

View File

@@ -15,13 +15,28 @@
using namespace Quotient;
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
{
if (connection == m_connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
fetchEmojis();
}
NeoChatConnection *CustomEmojiModel::connection() const
{
return m_connection;
}
void CustomEmojiModel::fetchEmojis()
{
if (!Controller::instance().activeConnection()) {
if (!m_connection) {
return;
}
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
if (data == nullptr) {
return;
}
@@ -53,10 +68,10 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
using namespace Quotient;
auto job = Controller::instance().activeConnection()->uploadFile(location.toLocalFile());
auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job] {
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_ls].toObject();
@@ -78,7 +93,7 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
});
json["images"_ls] = emojiData;
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
});
}
@@ -86,7 +101,7 @@ void CustomEmojiModel::removeEmoji(const QString &name)
{
using namespace Quotient;
const auto &data = Controller::instance().activeConnection()->accountData("im.ponies.user_emotes"_ls);
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
Q_ASSERT(data);
auto json = data->contentJson();
const QString _name = name.mid(1).chopped(1);
@@ -109,24 +124,25 @@ void CustomEmojiModel::removeEmoji(const QString &name)
emojiData.remove(_name);
json["emoticons"_ls] = emojiData;
}
Controller::instance().activeConnection()->setAccountData("im.ponies.user_emotes"_ls, json);
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
}
CustomEmojiModel::CustomEmojiModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
if (!Controller::instance().activeConnection()) {
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
if (!m_connection) {
return;
}
CustomEmojiModel::fetchEmojis();
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &id) {
connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
if (id != QStringLiteral("im.ponies.user_emotes")) {
return;
}
fetchEmojis();
});
});
CustomEmojiModel::fetchEmojis();
}
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const

View File

@@ -8,6 +8,8 @@
#include <QRegularExpression>
#include <memory>
class NeoChatConnection;
struct CustomEmoji {
QString name; // with :semicolons:
QString url; // mxc://
@@ -31,6 +33,8 @@ class CustomEmojiModel : public QAbstractListModel
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
public:
/**
* @brief Defines the model roles.
@@ -51,8 +55,9 @@ public:
static CustomEmojiModel _instance;
return _instance;
}
static CustomEmojiModel *create(QQmlEngine *, QJSEngine *)
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
@@ -97,9 +102,16 @@ public:
*/
Q_INVOKABLE void removeEmoji(const QString &name);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
Q_SIGNALS:
void connectionChanged();
private:
explicit CustomEmojiModel(QObject *parent = nullptr);
QList<CustomEmoji> m_emojis;
NeoChatConnection *m_connection = nullptr;
void fetchEmojis();
};

View File

@@ -24,8 +24,8 @@ DevicesModel::DevicesModel(QObject *parent)
void DevicesModel::fetchDevices()
{
if (Controller::instance().activeConnection()) {
auto job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
if (m_connection) {
auto job = m_connection->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel();
m_devices = job->devices();
@@ -102,7 +102,7 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
for (index = 0; m_devices[index].deviceId != deviceId; index++)
;
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
auto job = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
auto onSuccess = [this, index]() {
@@ -117,9 +117,9 @@ void DevicesModel::logout(const QString &deviceId, const QString &password)
authData["session"_ls] = replyData["session"_ls];
authData["password"_ls] = password;
authData["type"_ls] = "m.login.password"_ls;
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, Controller::instance().activeConnection()->user()->id()}};
QJsonObject identifier = {{"type"_ls, "m.id.user"_ls}, {"user"_ls, m_connection->user()->id()}};
authData["identifier"_ls] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
auto *innerJob = m_connection->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, onSuccess);
} else {
onSuccess();
@@ -132,7 +132,7 @@ void DevicesModel::setName(const QString &deviceId, const QString &name)
int index;
for (index = 0; m_devices[index].deviceId != deviceId; index++);
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
auto job = m_connection->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
QString oldName = m_devices[index].displayName;
beginResetModel();
m_devices[index].displayName = name;
@@ -160,7 +160,7 @@ void DevicesModel::setConnection(Connection *connection)
connect(m_connection, &Connection::sessionVerified, this, [this](const QString &userId, const QString &deviceId) {
Q_UNUSED(deviceId);
if (userId == Controller::instance().activeConnection()->userId()) {
if (userId == m_connection->userId()) {
fetchDevices();
}
});

View File

@@ -98,6 +98,6 @@ Q_SIGNALS:
private:
void fetchDevices();
QVector<Quotient::Device> m_devices;
QList<Quotient::Device> m_devices;
QPointer<Quotient::Connection> m_connection;
};

View File

@@ -14,6 +14,8 @@
EmojiModel::EmojiModel(QObject *parent)
: QAbstractListModel(parent)
, m_config(KSharedConfig::openStateConfig())
, m_configGroup(KConfigGroup(m_config, QStringLiteral("Editor")))
{
if (_emojis.isEmpty()) {
#include "emojis.h"
@@ -61,9 +63,9 @@ QHash<int, QByteArray> EmojiModel::roleNames() const
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
}
QVariantList EmojiModel::history() const
QStringList EmojiModel::lastUsedEmojis() const
{
return m_settings.value(QStringLiteral("Editor/emojis"), QVariantList()).toList();
return m_configGroup.readEntry(QStringLiteral("lastUsedEmojis"), QStringList());
}
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
@@ -93,19 +95,21 @@ QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
void EmojiModel::emojiUsed(const QVariant &modelData)
{
QVariantList list = history();
auto list = lastUsedEmojis();
const auto emoji = modelData.value<Emoji>();
auto it = list.begin();
while (it != list.end()) {
if ((*it).value<Emoji>().unicode == modelData.value<Emoji>().unicode) {
if (*it == emoji.shortName) {
it = list.erase(it);
} else {
it++;
}
}
list.push_front(modelData);
m_settings.setValue(QStringLiteral("Editor/emojis"), list);
list.push_front(emoji.shortName);
m_configGroup.writeEntry(QStringLiteral("lastUsedEmojis"), list);
Q_EMIT historyChanged();
}
@@ -113,11 +117,11 @@ void EmojiModel::emojiUsed(const QVariant &modelData)
QVariantList EmojiModel::emojis(Category category) const
{
if (category == History) {
return history();
return emojiHistory();
}
if (category == HistoryNoCustom) {
QVariantList list;
for (const auto &e : history()) {
for (const auto &e : emojiHistory()) {
auto emoji = qvariant_cast<Emoji>(e);
if (!emoji.isCustom) {
list.append(e);
@@ -217,4 +221,19 @@ QVariantList EmojiModel::categoriesWithCustom() const
return cats;
}
QVariantList EmojiModel::emojiHistory() const
{
QVariantList list;
for (const auto &historicEmoji : lastUsedEmojis()) {
for (const auto &emojiCategory : _emojis) {
for (const auto &emoji : emojiCategory) {
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
list.append(emoji);
}
}
}
}
return list;
}
#include "moc_emojimodel.cpp"

View File

@@ -3,10 +3,11 @@
#pragma once
#include <KConfigGroup>
#include <KSharedConfig>
#include <QAbstractListModel>
#include <QObject>
#include <QQmlEngine>
#include <QSettings>
struct Emoji {
Emoji(QString unicode, QString shortname, bool isCustom = false)
@@ -23,21 +24,6 @@ struct Emoji {
}
Emoji() = default;
friend QDataStream &operator<<(QDataStream &arch, const Emoji &object)
{
arch << object.unicode;
arch << object.shortName;
return arch;
}
friend QDataStream &operator>>(QDataStream &arch, Emoji &object)
{
arch >> object.unicode;
arch >> object.shortName;
object.isCustom = object.unicode.startsWith(QStringLiteral("image://"));
return arch;
}
QString unicode;
QString shortName;
QString description;
@@ -63,11 +49,6 @@ class EmojiModel : public QAbstractListModel
QML_ELEMENT
QML_SINGLETON
/**
* @brief Return a list of recently used emojis.
*/
Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged)
/**
* @brief Return a list of emoji categories.
*
@@ -86,8 +67,9 @@ public:
static EmojiModel _instance;
return _instance;
}
static EmojiModel *create(QQmlEngine *, QJSEngine *)
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
{
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
return &instance();
}
@@ -176,7 +158,11 @@ public:
*/
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
Q_INVOKABLE QVariantList history() const;
/**
* @brief Return a list of the last used emoji shortnames
*/
QStringList lastUsedEmojis() const;
QVariantList categories() const;
QVariantList categoriesWithCustom() const;
@@ -189,7 +175,10 @@ public Q_SLOTS:
private:
static QHash<Category, QVariantList> _emojis;
// TODO: Port away from QSettings
QSettings m_settings;
/// Returns QVariants containing the last used Emojis
QVariantList emojiHistory() const;
KSharedConfig::Ptr m_config;
KConfigGroup m_configGroup;
EmojiModel(QObject *parent = nullptr);
};

View File

@@ -102,6 +102,9 @@ void ImagePacksModel::reloadImages()
}
auto packs = rooms[roomId].toObject();
const auto &stickerRoom = m_room->connection()->room(roomId);
if (!stickerRoom) {
continue;
}
for (const auto &packKey : packs.keys()) {
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
const auto packContent = pack->content();
@@ -151,7 +154,7 @@ void ImagePacksModel::setShowEmoticons(bool showEmoticons)
m_showEmoticons = showEmoticons;
Q_EMIT showEmoticonsChanged();
}
QVector<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
{
if (index < 0 || index >= m_events.size()) {
return {};

View File

@@ -5,9 +5,9 @@
#include "events/imagepackevent.h"
#include <QAbstractListModel>
#include <QList>
#include <QPointer>
#include <QQmlEngine>
#include <QVector>
class NeoChatRoom;
@@ -86,7 +86,7 @@ public:
/**
* @brief Return a vector of the images in the pack at the given index.
*/
[[nodiscard]] QVector<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
Q_SIGNALS:
void roomChanged();
@@ -96,7 +96,7 @@ Q_SIGNALS:
private:
QPointer<NeoChatRoom> m_room;
QVector<Quotient::ImagePackEventContent> m_events;
QList<Quotient::ImagePackEventContent> m_events;
bool m_showStickers = true;
bool m_showEmoticons = true;
void reloadImages();

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