Compare commits

...

101 Commits

Author SHA1 Message Date
James Graham
e8b269869c Some people don't want friends so fix 2024-01-24 16:41:13 +00:00
l10n daemon script
7fd8394253 GIT_SILENT Sync po/docbooks with svn 2024-01-24 01:17:22 +00:00
Tobias Fella
c54a447caf Add appstream developer tag and remove developer_name tag 2024-01-23 22:42:36 +01:00
l10n daemon script
61b009422d GIT_SILENT Sync po/docbooks with svn 2024-01-23 01:18:48 +00:00
l10n daemon script
5a8b0184ea GIT_SILENT Sync po/docbooks with svn 2024-01-22 01:29:04 +00:00
James Graham
f48c2a21d9 Autosearch
Make the user search automatically. This includes a timer to ensure that we aren't constantly pinging the server as the user types, the search is started 0.5s after the user stops typing. The `PublicRoomListModel` is upgraded to work in the same manner as it was architected slightly differently.
2024-01-21 11:24:40 +00:00
l10n daemon script
538cfbee8d GIT_SILENT Sync po/docbooks with svn 2024-01-21 01:17:19 +00:00
James Graham
7666f1c362 Fix the vertical alignment of the notification bubble text 2024-01-20 19:07:27 +00:00
James Graham
4b5d828bf8 The search for friendship
Add the ability to search in the user directory for friends.

This adds an option in roomlist when on the friends tab and opens a search dialog when clicked. The new search model searches the user directory for the given filter term.
2024-01-20 16:13:49 +00:00
Tobias Fella
4bd160cceb Remove workaround for QTBUG 93281
Seems to no longer be required
2024-01-20 16:13:13 +00:00
Joshua Goins
5f56fc1156 Add icon for notification state menu
In Qt6 we can (finally) add icons to QQC Menus!
2024-01-20 13:47:17 +00:00
l10n daemon script
72a2a74395 GIT_SILENT Sync po/docbooks with svn 2024-01-20 01:17:30 +00:00
James Graham
f6a5cc7c25 Generic Search Page
Pull the generic aspects from Room search and join room pages into it's own component. This is done in anticipation of using the new generic search page for a user search functionality.

- `SearchPage` is now used for the generic version with the old one being renamed `RoomSearchPage`
- `JoinRoomPage` is renamed to `ExploreRoomsPage` inline with everywhere else in NeoChat

There is also some cleanup of the code for both search pages in here.
2024-01-19 17:59:45 +00:00
l10n daemon script
80f3bd64b6 GIT_SILENT Sync po/docbooks with svn 2024-01-18 01:18:17 +00:00
Joshua Goins
1f69a96766 Hide the subtitle text for room delegates if there is none
This centers the room name label for room list items, which looks a bit
cleaner than nothing being there at all.
2024-01-17 17:29:57 +00:00
James Graham
f963e06983 Remove the option to merge the room list 2024-01-17 16:58:51 +00:00
l10n daemon script
e6980e2370 GIT_SILENT Sync po/docbooks with svn 2024-01-17 01:19:30 +00:00
James Graham
8e8105d04d Clip QuickSwitcher
Clip QuickSwitcher to stop the delegates overlapping the dialog
2024-01-16 20:08:53 +00:00
Ingo Klöcker
21d9e69712 Require master of ECM
We need the fix for APK packaging with Android NDK r25
2024-01-16 13:57:33 +01:00
l10n daemon script
e0783a3c6e GIT_SILENT Sync po/docbooks with svn 2024-01-16 01:19:20 +00:00
l10n daemon script
3b9337d2a8 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-01-16 01:12:42 +00:00
James Graham
85cda8ffa7 Current Room Messages
Make sure that message delegates are getting the room object directly rather than requiring the assumption that currentRoom is declared somewhere higher up.
2024-01-15 19:47:50 +00:00
l10n daemon script
f1efc1f17d GIT_SILENT Sync po/docbooks with svn 2024-01-15 01:19:09 +00:00
James Graham
0486fa61cd NeoChatConnection signals
Move the signal connects to a function and call from both constructors
2024-01-14 12:25:53 +00:00
Joshua Goins
2247a2a7af Make the search message dialog header way prettier, like it is in KCMs
I think I've heard of this before...
2024-01-14 01:36:59 +00:00
Joshua Goins
08a0fbfd6b Add missing thread roles in SearchModel
This fixes the message search so it works again!
2024-01-14 01:34:43 +00:00
l10n daemon script
898b993b94 GIT_SILENT Sync po/docbooks with svn 2024-01-14 01:30:05 +00:00
James Graham
77e366b179 Why can't we be friends
Update the UX to refer to structure direct chats as friends. The direct chats are pulled into their own tab in the space drawer.

The `UserDetailDialog` is also updated to check whether a direct chat already exists and if not ask to invite as friend.

![image](/uploads/67f13fa8558e704e0acaf7c60e135bbc/image.png)
2024-01-13 21:38:43 +00:00
Tobias Fella
981edc9cf7 Refactor proxy configuration and move to separate file 2024-01-13 17:39:56 +01:00
Tobias Fella
d45aa14348 Refactor some code around connection handling 2024-01-13 13:00:29 +00:00
Tobias Fella
4926488d49 Move notifications button to space drawer.
Since this means that the space drawer can no longer be hidden when there are no spaces,
also make it less empty by adding a button for creating new spaces.
More things will come in the future.

BUG: 479051
2024-01-13 11:28:25 +01:00
l10n daemon script
dcc1935150 GIT_SILENT Sync po/docbooks with svn 2024-01-13 01:24:17 +00:00
Tobias Fella
55364a8eb8 Add basic Itinerary integration
After downloading a file, the model calls the extractor and uses the
JSON to show some basic information about the content and allows to import
the data to Itinerary. This is entirely runtime-optional; no build-time dependencies
are required and nothing changes if the extractor isn't available.
2024-01-12 21:00:14 +00:00
Tobias Fella
70bb06715f Don't crash when calling directChatRemoteUser in something that isn't a direct chat
Can happen e.g. in gammaray
2024-01-12 16:32:49 +01:00
James Graham
ec4aa73e37 Readonly Room
Add readonly property to a room and use it to decide whether to show chatbar, replies and edits

BUG: 479590
2024-01-12 01:59:09 +00:00
Albert Astals Cid
c1d122a717 GIT_SILENT Upgrade release service version to 24.04.70. 2024-01-11 21:36:24 +01:00
Yifan Zhu
f75c194e7c Call signals instead of signal handlers
Directly calling signals is the supported way to send signals.
Calling signal handlers worked in the past, but will be phased out in
the future (https://bugreports.qt.io/browse/QTBUG-120573).
2024-01-11 17:23:08 +00:00
Tobias Fella
ecf93de006 Update copyright year 2024-01-11 18:21:44 +01:00
l10n daemon script
7feb02c7d8 GIT_SILENT Sync po/docbooks with svn 2024-01-11 01:17:30 +00:00
Hannah von Reth
0764fe0dd9 Use craft default targets 2024-01-10 11:47:48 +00:00
Hannah von Reth
50a7138633 Add craft ci targets 2024-01-10 11:47:48 +00:00
l10n daemon script
96a477f91c GIT_SILENT Sync po/docbooks with svn 2024-01-10 02:13:57 +00:00
Tobias Fella
5a6b0f756d Remove broken network error checks
Fixes #529
2024-01-09 19:59:23 +01:00
l10n daemon script
dbbf975f7a GIT_SILENT Sync po/docbooks with svn 2024-01-09 02:10:55 +00:00
l10n daemon script
e06c2d2f93 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-01-09 02:06:01 +00:00
Albert Astals Cid
5dea17b616 GIT_SILENT Upgrade release service version to 24.01.90. 2024-01-09 00:47:19 +01:00
Tobias Fella
63b6d7ebe0 Remove unused includes 2024-01-08 20:41:59 +01:00
l10n daemon script
652106e6a1 GIT_SILENT Sync po/docbooks with svn 2024-01-08 02:16:01 +00:00
Joshua Goins
3e158b3e60 Stop log spam because subtitleText was called without a valid event
It's up to the call site to check if the event is valid before calling
this function, and it prevents tons of log spam because we didn't check
yet.
2024-01-07 21:43:26 +00:00
Tobias Fella
b9ec33dd94 Remove activeConnection from Config
It's not used anymore
2024-01-07 21:25:55 +01:00
Tobias Fella
69bd1202ba Remove another comment 2024-01-07 21:20:04 +01:00
Tobias Fella
f75c09e130 Remove comment 2024-01-07 21:19:25 +01:00
Tobias Fella
683d216f44 Mark ReactionModel as uncreatable 2024-01-07 19:47:19 +01:00
Tobias Fella
c10bcf1764 Move userConsentRequired to NeoChatConnection 2024-01-07 19:44:37 +01:00
Tobias Fella
9e2bf0da26 Port away from Controller::saveWindowGeometry 2024-01-07 19:39:18 +01:00
James Graham
51f7de117d Refactor LinkPreviewer
Refactor `LinkPreviewer` to take an event and put the functions for getting the link in the class itself. This means the functions in `EventHandler` are no longer required.

This mr also sets up `LinkPreviewer` so that it is automatically updated when an event is edited. This includes changing the link if edited, and it can handle a message having a previous link removed or a one added when one didn't exist before.

Also adds test suite.
2024-01-07 18:07:13 +00:00
Tobias Fella
f361f4e2d8 Don't always html-escape user-specified input when serializing a state event body
We don't want this for the room list subtitle
2024-01-07 16:12:45 +00:00
l10n daemon script
acfb5ab834 GIT_SILENT Sync po/docbooks with svn 2024-01-07 02:42:26 +00:00
James Graham
8bc4500ce1 Fix by allowing RoomPage to access the interactive property of the timeline 2024-01-06 19:49:20 +00:00
Joshua Goins
b96d3dde46 Remove useless interactive set when maximizing images
This doesn't seem to do anything, and it's not easy to turn it back when
the popup is closed
2024-01-06 19:49:20 +00:00
James Graham
fad381c36f Refactor reactions
Currently we effectively create the reactions list in EventHandler then pass that data into a model. This reworks the model so that we just pass in a room and an event and it grabs it's own data. This means that:
- the functions in event handler are no longer required
- the model can update itself to add/remove reactions so no need to handle that in MessageEventModel
- MessageEventModel only needs to create new ReactionModels or remove old ones when no reactions exist anymore

A basic test suite has also been created for the ReactionModel
2024-01-06 17:50:32 +00:00
Tobias Fella
ad083f64b1 Ensure that only one RoomManager exists 2024-01-06 17:46:28 +01:00
Tobias Fella
5c78b23cc2 Slightly fix QML formatting 2024-01-06 17:34:56 +01:00
Tobias Fella
2202063641 Remove stray console log 2024-01-06 17:34:10 +01:00
Tobias Fella
5be15ffa6a Show user display names as plaintext in InviteUserPage 2024-01-06 17:33:39 +01:00
Tobias Fella
a0bafe53a0 Use Plaintext for user displaynames in startchatpage 2024-01-06 17:31:57 +01:00
l10n daemon script
1a39ce9585 GIT_SILENT Sync po/docbooks with svn 2024-01-06 02:11:50 +00:00
Tobias Fella
4a809d57f7 Fix crash when accepting/declining already accepted/declined invite
BUG: 475502
2024-01-05 14:16:21 +01:00
l10n daemon script
c01e42c972 GIT_SILENT Sync po/docbooks with svn 2024-01-05 02:13:27 +00:00
l10n daemon script
6c56f7f4ef GIT_SILENT Sync po/docbooks with svn 2024-01-04 02:16:55 +00:00
l10n daemon script
6d8d5a82c2 GIT_SILENT Sync po/docbooks with svn 2024-01-03 02:14:00 +00:00
Nicolas Fella
daa27b0333 Fix Android build 2024-01-03 01:40:14 +01:00
Nicolas Fella
f6edf0e4cc Add missing include 2024-01-03 01:29:07 +01:00
James Graham
356e8eefe0 Refactor PollHandler
Refactor PollHandler to make it more reliable. This ended up with much more code than I expected as the original intent was just to stop a crash when switching rooms.
- Using a event string was flaky, changing to using an event reference is more reliable.
- Since we're only creating them from NeoChatRoom there is no need to to be able to set properties from QML so only read properties.
- Pass from the MessageEventModel rather than an invokable method.
- Create a basic test suite
- Create properties in PollHandler to remove the need to use content in PollDelegate, this means content is no longer a required role.
2024-01-02 21:22:08 +00:00
Laurent Montel
7ad362225f Use --socket=fallback-x11 in .flatpak-manifest.json 2024-01-02 17:59:31 +01:00
Tobias Fella
d623a8c826 Don't HTML-escape invite notification title 2024-01-02 07:48:52 +00:00
l10n daemon script
b3f0d110d9 GIT_SILENT Sync po/docbooks with svn 2024-01-02 02:09:10 +00:00
James Graham
612b5d7f47 Move all the enums for push rules into their own header file 2024-01-01 16:15:40 +00:00
James Graham
7e9f206348 Clear the emoji picker search when the dialog is closed
Clear the emoji picker search when the dialog is closed

BUG: 472873
2024-01-01 16:15:29 +00:00
l10n daemon script
d5f4a3dd64 GIT_SILENT Sync po/docbooks with svn 2024-01-01 02:13:41 +00:00
James Graham
e807ad9908 Improve the unread marker behaviour
The fixes include:
- improving the timer to make it more reliable
- making sure a read marker is added when changin rooms, this is needed when the messages have already been loaded.
- increase the default timer to 10s to avoid the read marker disappearing and being re-added when a message arrive in quick succession. 

BUG: 465300
2023-12-31 17:47:27 +00:00
Tobias Fella
2d8ad834a7 Fix showing stickers 2023-12-31 18:15:43 +01:00
l10n daemon script
d82dfc7a5b GIT_SILENT Sync po/docbooks with svn 2023-12-31 02:43:10 +00:00
l10n daemon script
1e1e54d4bd 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-12-31 02:27:14 +00:00
l10n daemon script
9043b1c7a1 GIT_SILENT made messages (after extraction) 2023-12-31 01:54:25 +00:00
Tobias Fella
0739f4c661 Re-Enable Flatpak CI 2023-12-27 22:00:29 +00:00
James Graham
29321aeaf3 WindowController test
Create a WindowController test suite.

Also make sure that the class handles m_window being nullptr.
2023-12-24 15:37:56 +00:00
l10n daemon script
9fe44515a7 GIT_SILENT Sync po/docbooks with svn 2023-12-24 02:48:11 +00:00
Tobias Fella
4c3d7ab011 Move Controller::toggleWindow to WindowController 2023-12-23 14:50:36 +00:00
Tobias Fella
d02eee6daa Port away from and remove Controller::initiated 2023-12-23 13:39:41 +00:00
Tobias Fella
a613baa148 Don't crash when invited room doesn't have member state for the current room
This smells illegal, but nothing stops a server from sending that to us so we shouldn't crash.

BUG: 478903
2023-12-23 11:55:22 +00:00
James Graham
4e141e05f0 Cleanup leftover issues from moving ReplyComponent away from GridLayout
Cleanup leftover issues from moving ReplyComponent away from GridLayout.
- Remove leftover GridLayout properties or references to them or convert as required
- Remove unneeded Item wrapper
2023-12-23 11:37:01 +00:00
l10n daemon script
134f1d2ae5 GIT_SILENT Sync po/docbooks with svn 2023-12-23 02:30:01 +00:00
Tobias Fella
9ec3e5b2cc Remove more unused signals 2023-12-23 00:45:00 +01:00
Tobias Fella
15d066b3ed Remove unused variable 2023-12-23 00:39:08 +01:00
Tobias Fella
7694f6c464 Remove unused signal 2023-12-23 00:36:53 +01:00
Marius P
46da4f9777 org.kde.neochat.appdata.xml use https://bugs.kde.org/enter_bug.cgi?product=NeoChat
https://github.com/ximion/appstream/docs/xml/metainfo-component.xml says
"bugtracker - Should point to the software's bug tracking system,
for users to report new bugs.".
2023-12-22 13:10:55 +00:00
l10n daemon script
44e0ef43ac GIT_SILENT Sync po/docbooks with svn 2023-12-22 02:23:21 +00:00
l10n daemon script
5b79371c0a GIT_SILENT made messages (after extraction) 2023-12-22 01:39:20 +00:00
Joshua Goins
3caa5ad2ed Prevent KUnifiedPush-activated daemon from sticking around forever
Erroneous activations of the D-Bus service could cause the daemon to be
launched without any messageReceived signals being called (which then
hooks up the notifications to quit the app.) Now there's a five-second
timeout to prevent it from living too long.
2023-12-21 19:41:41 +00:00
l10n daemon script
a74d78439c GIT_SILENT Sync po/docbooks with svn 2023-12-21 02:47:51 +00:00
177 changed files with 24112 additions and 19929 deletions

View File

@@ -2,9 +2,6 @@
; SPDX-License-Identifier: CC0-1.0 ; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings] [BlueprintSettings]
kde/frameworks/extra-cmake-modules.version=master
kde/unreleased/kirigami-addons.version=master 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 libs/qt.qtMajorVersion=6

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat", "id": "org.kde.neochat",
"branch": "master", "branch": "master",
"runtime": "org.kde.Platform", "runtime": "org.kde.Platform",
"runtime-version": "5.15-22.08", "runtime-version": "6.6-kf6preview",
"sdk": "org.kde.Sdk", "sdk": "org.kde.Sdk",
"command": "neochat", "command": "neochat",
"tags": [ "tags": [
@@ -12,7 +12,7 @@
"finish-args": [ "finish-args": [
"--share=network", "--share=network",
"--share=ipc", "--share=ipc",
"--socket=x11", "--socket=fallback-x11",
"--socket=wayland", "--socket=wayland",
"--device=dri", "--device=dri",
"--filesystem=xdg-download", "--filesystem=xdg-download",
@@ -31,6 +31,7 @@
}, },
{ {
"name": "kquickimageeditor", "name": "kquickimageeditor",
"config-opts": [ "-DBUILD_WITH_QT6=ON" ],
"buildsystem": "cmake-ninja", "buildsystem": "cmake-ninja",
"sources": [ "sources": [
{ {
@@ -85,8 +86,8 @@
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/frankosterfeld/qtkeychain/archive/v0.13.2.tar.gz", "url": "https://github.com/frankosterfeld/qtkeychain/archive/0.14.2.tar.gz",
"sha256": "20beeb32de7c4eb0af9039b21e18370faf847ac8697ab3045906076afbc4caa5", "sha256": "cf2e972b783ba66334a79a30f6b3a1ea794a1dc574d6c3bebae5ffd2f0399571",
"x-checker-data": { "x-checker-data": {
"type": "anitya", "type": "anitya",
"project-id": 4138, "project-id": 4138,
@@ -96,6 +97,7 @@
} }
], ],
"config-opts": [ "config-opts": [
"-DBUILD_WITH_QT6=ON",
"-DCMAKE_INSTALL_LIBDIR=/app/lib", "-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DLIB_INSTALL_DIR=/app/lib", "-DLIB_INSTALL_DIR=/app/lib",
"-DBUILD_TRANSLATIONS=NO" "-DBUILD_TRANSLATIONS=NO"
@@ -113,6 +115,7 @@
} }
], ],
"config-opts": [ "config-opts": [
"-DBUILD_WITH_QT6=ON",
"-DQuotient_ENABLE_E2EE=ON", "-DQuotient_ENABLE_E2EE=ON",
"-DBUILD_TESTING=OFF" "-DBUILD_TESTING=OFF"
] ]

View File

@@ -9,5 +9,7 @@ include:
- /gitlab-templates/linux-qt6.yml - /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml - /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml - /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/flatpak.yml - /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-qt6-apks.yml - /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml

View File

@@ -8,8 +8,8 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script. # KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "01") set(RELEASE_SERVICE_VERSION_MINOR "04")
set(RELEASE_SERVICE_VERSION_MICRO "85") set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION}) project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})

View File

@@ -58,3 +58,27 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
TEST_NAME actionshandlertest TEST_NAME actionshandlertest
) )
ecm_add_test(
windowcontrollertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME windowcontrollertest
)
ecm_add_test(
pollhandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME pollhandlertest
)
ecm_add_test(
reactionmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME reactionmodeltest
)
ecm_add_test(
linkpreviewertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME linkpreviewertest
)

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "https://matrix.to/#/@alice:example.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "mxc://example.org/SEsfnsuifSDFSSEF",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "testhttps://kde.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,24 @@
{
"timeline": {
"events": [
{
"content": {
"body": "https://kde.org",
"format": "org.matrix.custom.html",
"formatted_body": "https://kde.org",
"msgtype": "m.text"
},
"origin_server_ts": 1704648567967,
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 112
},
"event_id": "$validlink:example.org",
"room_id": "!test:example.org"
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,35 @@
{
"timeline": {
"events": [
{
"content": {
"body": "* ",
"format": "org.matrix.custom.html",
"formatted_body": "no link",
"m.new_content": {
"body": "",
"format": "org.matrix.custom.html",
"formatted_body": "no link",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$validlink:example.org",
"rel_type": "m.replace"
},
"msgtype": "m.text",
"type": "m.room.message"
},
"origin_server_ts": 1704648614969,
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 65
},
"event_id": "$nolink:example.org",
"room_id": "!test:example.org"
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "www.example.org https://kde.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,38 @@
{
"timeline": {
"events": [
{
"content": {
"org.matrix.msc1767.text": "test\n1. option1\n2. option 2",
"org.matrix.msc3381.poll.start": {
"answers": [
{
"id": "option1",
"org.matrix.msc1767.text": "option1"
},
{
"id": "option2",
"org.matrix.msc1767.text": "option2"
}
],
"kind": "org.matrix.msc3381.poll.disclosed",
"max_selections": 1,
"question": {
"body": "test",
"msgtype": "m.text",
"org.matrix.msc1767.text": "test"
}
}
},
"event_id": "$153456789:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "org.matrix.msc3381.poll.start",
"unsigned": {
"age": 1232
}
}
]
}
}

View File

@@ -0,0 +1,44 @@
{
"timeline": {
"events": [
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545183,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159121
},
"event_id": "$163456790:example.org",
"age": 390159121
},
{
"content": {
"m.relates_to": {
"event_id": "$153456789:example.org",
"key": "😆",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1690322545184,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159122
},
"event_id": "$163456791:example.org",
"age": 390159122
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

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

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "https://kde.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,14 @@
{
"content": {
"body": "www.example.org",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -0,0 +1,16 @@
{
"content": {
"body": "[Rich Link](https://kde.org)",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://kde.org\">Rich Link</a>",
"msgtype": "m.text"
},
"event_id": "$validlink1:example.org",
"origin_server_ts": 1432735824654,
"room_id": "!test:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}

View File

@@ -65,10 +65,6 @@ private Q_SLOTS:
void nullSubtitle(); void nullSubtitle();
void mediaInfo(); void mediaInfo();
void nullMediaInfo(); void nullMediaInfo();
void linkPreviewer();
void nullLinkPreviewer();
void reactions();
void nullReactions();
void hasReply(); void hasReply();
void nullHasReply(); void nullHasReply();
void replyId(); void replyId();
@@ -401,45 +397,6 @@ void EventHandlerTest::nullMediaInfo()
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap()); QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
} }
void EventHandlerTest::linkPreviewer()
{
auto event = room->messageEvents().at(2).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
event = room->messageEvents().at(0).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
}
void EventHandlerTest::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();
eventHandler.setEvent(event);
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() void EventHandlerTest::hasReply()
{ {
auto event = room->messageEvents().at(5).get(); auto event = room->messageEvents().at(5).get();

View File

@@ -0,0 +1,104 @@
// 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 "linkpreviewer.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "utils.h"
#include "testutils.h"
using namespace Quotient;
class LinkPreviewerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
void linkPreviewsReject_data();
void linkPreviewsReject();
void editedLink();
};
void LinkPreviewerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:example.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("!test:example.org"));
}
void LinkPreviewerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("eventSource");
QTest::addColumn<QUrl>("testOutputLink");
QTest::newRow("plainHttps") << QStringLiteral("test-validplainlink-event.json") << QUrl("https://kde.org"_ls);
QTest::newRow("richHttps") << QStringLiteral("test-validrichlink-event.json") << QUrl("https://kde.org"_ls);
QTest::newRow("plainWww") << QStringLiteral("test-validplainwwwlink-event.json") << QUrl("www.example.org"_ls);
QTest::newRow("multipleHttps") << QStringLiteral("test-multiplelink-event.json") << QUrl("www.example.org"_ls);
}
void LinkPreviewerTest::linkPreviewsMatch()
{
QFETCH(QString, eventSource);
QFETCH(QUrl, testOutputLink);
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
auto linkPreviewer = LinkPreviewer(room, event.get());
QCOMPARE(linkPreviewer.empty(), false);
QCOMPARE(linkPreviewer.url(), testOutputLink);
}
void LinkPreviewerTest::linkPreviewsReject_data()
{
QTest::addColumn<QString>("eventSource");
QTest::newRow("mxc") << QStringLiteral("test-invalidmxclink-event.json");
QTest::newRow("matrixTo") << QStringLiteral("test-invalidmatrixtolink-event.json");
QTest::newRow("noSpace") << QStringLiteral("test-invalidnospacelink-event.json");
}
void LinkPreviewerTest::linkPreviewsReject()
{
QFETCH(QString, eventSource);
auto event = TestUtils::loadEventFromFile<RoomMessageEvent>(eventSource);
auto linkPreviewer = LinkPreviewer(room, event.get());
QCOMPARE(linkPreviewer.empty(), true);
QCOMPARE(linkPreviewer.url(), QUrl());
}
void LinkPreviewerTest::editedLink()
{
room->syncNewEvents(QStringLiteral("test-linkpreviewerintial-sync.json"));
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto linkPreviewer = LinkPreviewer(room, event);
QCOMPARE(linkPreviewer.empty(), false);
QCOMPARE(linkPreviewer.url(), QUrl("https://kde.org"_ls));
room->syncNewEvents(QStringLiteral("test-linkpreviewerreplace-sync.json"));
QCOMPARE(linkPreviewer.empty(), true);
QCOMPARE(linkPreviewer.url(), QUrl());
}
QTEST_MAIN(LinkPreviewerTest)
#include "linkpreviewertest.moc"

View File

@@ -0,0 +1,70 @@
// 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 <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "events/pollevent.h"
#include "pollhandler.h"
#include "testutils.h"
using namespace Quotient;
class PollHandlerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void nullObject();
void poll();
};
void PollHandlerTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), "test-pollhandlerstart-sync.json"_ls);
}
// Basically don't crash.
void PollHandlerTest::nullObject()
{
auto pollHandler = PollHandler();
QCOMPARE(pollHandler.hasEnded(), false);
QCOMPARE(pollHandler.answerCount(), 0);
QCOMPARE(pollHandler.question(), QString());
QCOMPARE(pollHandler.options(), QJsonArray());
QCOMPARE(pollHandler.answers(), QJsonObject());
QCOMPARE(pollHandler.counts(), QJsonObject());
QCOMPARE(pollHandler.kind(), QString());
}
void PollHandlerTest::poll()
{
auto startEvent = eventCast<const PollStartEvent>(room->messageEvents().at(0).get());
auto pollHandler = PollHandler(room, startEvent);
auto options = QJsonArray{QJsonObject{{"id"_ls, "option1"_ls}, {"org.matrix.msc1767.text"_ls, "option1"_ls}},
QJsonObject{{"id"_ls, "option2"_ls}, {"org.matrix.msc1767.text"_ls, "option2"_ls}}};
QCOMPARE(pollHandler.hasEnded(), false);
QCOMPARE(pollHandler.answerCount(), 0);
QCOMPARE(pollHandler.question(), QStringLiteral("test"));
QCOMPARE(pollHandler.options(), options);
QCOMPARE(pollHandler.answers(), QJsonObject());
QCOMPARE(pollHandler.counts(), QJsonObject());
QCOMPARE(pollHandler.kind(), QStringLiteral("org.matrix.msc3381.poll.disclosed"));
}
QTEST_GUILESS_MAIN(PollHandlerTest)
#include "pollhandlertest.moc"

View File

@@ -0,0 +1,83 @@
// 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 <QSignalSpy>
#include <QTest>
#include "models/reactionmodel.h"
#include <Quotient/events/roommessageevent.h>
#include "testutils.h"
using namespace Quotient;
class ReactionModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void nullModel();
void basicReaction();
void newReaction();
};
void ReactionModelTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-reactionmodel-sync.json"));
}
void ReactionModelTest::nullModel()
{
auto model = ReactionModel(nullptr, nullptr);
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QVariant());
}
void ReactionModelTest::basicReaction()
{
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto model = ReactionModel(event, room);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.data(model.index(0), ReactionModel::TextContentRole), QStringLiteral("<span style=\"font-family: 'emoji';\">👍</span>"));
QCOMPARE(model.data(model.index(0), ReactionModel::ReactionRole), QStringLiteral("👍"));
QCOMPARE(model.data(model.index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
auto authorList = QVariantList{room->getUser(room->user(QStringLiteral("@alice:matrix.org")))};
QCOMPARE(model.data(model.index(0), ReactionModel::AuthorsRole), authorList);
QCOMPARE(model.data(model.index(0), ReactionModel::HasLocalUser), false);
}
void ReactionModelTest::newReaction()
{
auto event = eventCast<const RoomMessageEvent>(room->messageEvents().at(0).get());
auto model = new ReactionModel(event, room);
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
QSignalSpy spy(model, SIGNAL(modelReset()));
room->syncNewEvents(QLatin1String("test-reactionmodel-extra-sync.json"));
QCOMPARE(model->rowCount(), 2);
QCOMPARE(spy.count(), 2); // Once for each of the 2 new reactions.
QCOMPARE(model->data(model->index(1), ReactionModel::ReactionRole), QStringLiteral("😆"));
QCOMPARE(model->data(model->index(0), ReactionModel::ToolTipRole),
QStringLiteral("@alice:matrix.org and @bob:example.org reacted with <span style=\"font-family: 'emoji';\">👍</span>"));
delete model;
}
QTEST_MAIN(ReactionModelTest)
#include "reactionmodeltest.moc"

View File

@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com> // 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 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <Quotient/events/event.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include "neochatroom.h" #include "neochatroom.h"
@@ -38,4 +39,17 @@ public:
} }
} }
}; };
template<Quotient::EventClass EventT>
inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
{
if (!eventFileName.isEmpty()) {
QFile testEventFile;
testEventFile.setFileName(QLatin1String(DATA_DIR) + u'/' + eventFileName);
testEventFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson);
}
return nullptr;
}
} }

View File

@@ -64,11 +64,6 @@ private Q_SLOTS:
void receiveRichEdited(); void receiveRichEdited();
void receiveLineSeparator(); void receiveLineSeparator();
void receiveRichCodeUrl(); void receiveRichCodeUrl();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
void linkPreviewsReject_data();
void linkPreviewsReject();
}; };
void TextHandlerTest::initTestCase() void TextHandlerTest::initTestCase()
@@ -523,53 +518,6 @@ void TextHandlerTest::receiveLineSeparator()
QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar")); QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar"));
} }
void TextHandlerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org"_ls)});
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org"_ls)});
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org"_ls)});
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
<< QList<QUrl>({
QUrl("https://kde.org"_ls),
QUrl("www.example.org"_ls),
});
}
void TextHandlerTest::linkPreviewsMatch()
{
QFETCH(QString, testInputString);
QFETCH(QList<QUrl>, testOutputLinks);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
void TextHandlerTest::linkPreviewsReject_data()
{
QTest::addColumn<QString>("testInputString");
QTest::addColumn<QList<QUrl>>("testOutputLinks");
QTest::newRow("mxc") << QStringLiteral("mxc://example.org/SEsfnsuifSDFSSEF") << QList<QUrl>();
QTest::newRow("matrixTo") << QStringLiteral("https://matrix.to/#/@alice:example.org") << QList<QUrl>();
QTest::newRow("noSpace") << QStringLiteral("testhttps://kde.org") << QList<QUrl>();
}
void TextHandlerTest::linkPreviewsReject()
{
QFETCH(QString, testInputString);
QFETCH(QList<QUrl>, testOutputLinks);
TextHandler testTextHandler;
testTextHandler.setData(testInputString);
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
}
void TextHandlerTest::receiveRichCodeUrl() void TextHandlerTest::receiveRichCodeUrl()
{ {
auto input = QStringLiteral("<code>https://kde.org</code>"); auto input = QStringLiteral("<code>https://kde.org</code>");

View File

@@ -0,0 +1,102 @@
// 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 <QQmlApplicationEngine>
#include <QTest>
#include <QWindow>
#include <KConfig>
#include <KSharedConfig>
#include <KWindowConfig>
#include "windowcontroller.h"
class WindowControllerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void nullWindow();
void geometry();
void showAndRaise();
void toggle();
void cleanup();
};
// Basically don't crash when no window is set.
void WindowControllerTest::nullWindow()
{
auto &instance = WindowController::instance();
QCOMPARE(instance.window(), nullptr);
instance.restoreGeometry();
instance.saveGeometry();
instance.showAndRaiseWindow({});
instance.toggleWindow();
}
void WindowControllerTest::geometry()
{
auto &instance = WindowController::instance();
QWindow window;
window.setGeometry(0, 0, 200, 200);
instance.setWindow(&window);
QCOMPARE(instance.window(), &window);
instance.saveGeometry();
const auto stateConfig = KSharedConfig::openStateConfig();
KConfigGroup windowGroup = stateConfig->group(QStringLiteral("Window"));
QCOMPARE(KWindowConfig::hasSavedWindowSize(windowGroup), true);
window.setGeometry(0, 0, 400, 400);
QCOMPARE(window.geometry(), QRect(0, 0, 400, 400));
instance.restoreGeometry();
QCOMPARE(window.geometry(), QRect(0, 0, 200, 200));
}
void WindowControllerTest::showAndRaise()
{
auto &instance = WindowController::instance();
QWindow window;
instance.setWindow(&window);
QCOMPARE(window.isVisible(), false);
instance.showAndRaiseWindow({});
QCOMPARE(window.isVisible(), true);
}
void WindowControllerTest::cleanup()
{
auto &instance = WindowController::instance();
instance.setWindow(nullptr);
QCOMPARE(instance.window(), nullptr);
}
void WindowControllerTest::toggle()
{
auto &instance = WindowController::instance();
QWindow window;
instance.setWindow(&window);
QCOMPARE(window.isVisible(), false);
instance.toggleWindow();
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.isVisible(), false);
// A window is classed as visible by qt when minimized but to the user this is not visible.
// So in this case we expect to show it even though visibility is technically true.
window.setVisibility(QWindow::Minimized);
QCOMPARE(window.windowState(), Qt::WindowMinimized);
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), true);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), false);
}
QTEST_MAIN(WindowControllerTest)
#include "windowcontrollertest.moc"

View File

@@ -52,6 +52,7 @@
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary> <summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary> <summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary> <summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
<summary xml:lang="cs">Mluvte se svými přáteli na Matrixu</summary>
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary> <summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
<summary xml:lang="es">Charle con sus amigos en matrix</summary> <summary xml:lang="es">Charle con sus amigos en matrix</summary>
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary> <summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
@@ -71,6 +72,7 @@
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary> <summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary> <summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary> <summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
<summary xml:lang="zh-CN">在 Matrix 上与朋友聊天</summary>
<summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary> <summary xml:lang="zh-TW">在 Matrix 上與您的朋友聊天</summary>
<description> <description>
<p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami <p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami
@@ -231,49 +233,16 @@ to provide a convergent experience across multiple platforms.</p>
<li xml:lang="zh-TW">位置事件 - MSC3488</li> <li xml:lang="zh-TW">位置事件 - MSC3488</li>
</ul> </ul>
</description> </description>
<url type="homepage">https://apps.kde.org/neochat/</url> <url type="homepage">https://apps.kde.org/neochat</url>
<url type="bugtracker">https://bugs.kde.org/buglist.cgi?component=General&amp;product=NeoChat</url> <url type="bugtracker">https://bugs.kde.org/enter_bug.cgi?product=NeoChat</url>
<categories> <categories>
<category>Network</category> <category>Network</category>
</categories> </categories>
<developer_name>The KDE Community</developer_name> <developer>
<developer_name xml:lang="ar">مجتمع كِيدِي</developer_name> <id>kde.org</id>
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name> <name>The KDE Community</name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name> <url>https://kde.org</url>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name> </developer>
<developer_name xml:lang="cs">Komunita KDE</developer_name>
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
<developer_name xml:lang="el">Η Κοινότητα του KDE</developer_name>
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
<developer_name xml:lang="eo">La KDE-Komunumo</developer_name>
<developer_name xml:lang="es">La comunidad KDE</developer_name>
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
<developer_name xml:lang="fr">La communauté de KDE</developer_name>
<developer_name xml:lang="gl">A comunidade KDE</developer_name>
<developer_name xml:lang="hu">A KDE Közösség</developer_name>
<developer_name xml:lang="ia">Le communitate de KDE</developer_name>
<developer_name xml:lang="id">Komunitas KDE</developer_name>
<developer_name xml:lang="ie">Li comunité de KDE</developer_name>
<developer_name xml:lang="it">La comunità KDE</developer_name>
<developer_name xml:lang="ka">KDE-ის საზოგადოება</developer_name>
<developer_name xml:lang="ko">KDE 커뮤니티</developer_name>
<developer_name xml:lang="nl">De KDE gemeenschap</developer_name>
<developer_name xml:lang="nn">KDE-fellesskapet</developer_name>
<developer_name xml:lang="pa">ਕੇਡੀਈ ਕਮਿਊਨਟੀ</developer_name>
<developer_name xml:lang="pl">Społeczność KDE</developer_name>
<developer_name xml:lang="pt">A Comunidade do KDE</developer_name>
<developer_name xml:lang="pt-BR">A comunidade KDE</developer_name>
<developer_name xml:lang="ru">Сообщество KDE</developer_name>
<developer_name xml:lang="sk">KDE Komunita</developer_name>
<developer_name xml:lang="sl">Skupnost KDE</developer_name>
<developer_name xml:lang="sv">KDE-gemenskapen</developer_name>
<developer_name xml:lang="ta">கே.டீ.யீ. சமூகம்</developer_name>
<developer_name xml:lang="tr">KDE Topluluğu</developer_name>
<developer_name xml:lang="uk">Спільнота KDE</developer_name>
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<developer_name xml:lang="zh-CN">KDE 社区</developer_name>
<developer_name xml:lang="zh-TW">KDE 社群</developer_name>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license> <project_license>GPL-3.0</project_license>
<custom> <custom>
@@ -327,6 +296,7 @@ to provide a convergent experience across multiple platforms.</p>
<caption xml:lang="ar">شاشة الدخول</caption> <caption xml:lang="ar">شاشة الدخول</caption>
<caption xml:lang="ca">Pantalla d'inici de sessió</caption> <caption xml:lang="ca">Pantalla d'inici de sessió</caption>
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption> <caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
<caption xml:lang="cs">Přihlašovací obrazovka</caption>
<caption xml:lang="eo">Ensaluta ekrano</caption> <caption xml:lang="eo">Ensaluta ekrano</caption>
<caption xml:lang="es">Pantalla de inicio de sesión</caption> <caption xml:lang="es">Pantalla de inicio de sesión</caption>
<caption xml:lang="eu">Saio-hasteko pantaila</caption> <caption xml:lang="eu">Saio-hasteko pantaila</caption>

View File

@@ -65,7 +65,7 @@ GenericName[ie]=Cliente de Matrix
GenericName[it]=Client Matrix GenericName[it]=Client Matrix
GenericName[ka]=Matrix -ის კლიენტი GenericName[ka]=Matrix -ის კლიენტი
GenericName[ko]=Matrix 클라이언트 GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programą GenericName[lt]=Matrix kliento programa
GenericName[nl]=Matrix-client GenericName[nl]=Matrix-client
GenericName[nn]=Matrix-klient GenericName[nn]=Matrix-klient
GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ GenericName[pa]=ਮੈਟਰਿਕਸ ਕਲਾਈਂਟ

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

File diff suppressed because it is too large Load Diff

View File

@@ -143,6 +143,11 @@ add_library(neochat STATIC
models/notificationsmodel.h models/notificationsmodel.h
models/timelinemodel.cpp models/timelinemodel.cpp
models/timelinemodel.h models/timelinemodel.h
enums/pushrule.h
models/itinerarymodel.cpp
models/itinerarymodel.h
proxycontroller.cpp
proxycontroller.h
) )
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -160,11 +165,10 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/UserInfoDesktop.qml qml/UserInfoDesktop.qml
qml/RoomPage.qml qml/RoomPage.qml
qml/RoomWindow.qml qml/RoomWindow.qml
qml/JoinRoomPage.qml qml/ExploreRoomsPage.qml
qml/ManualRoomDialog.qml qml/ManualRoomDialog.qml
qml/ExplorerDelegate.qml qml/ExplorerDelegate.qml
qml/InviteUserPage.qml qml/InviteUserPage.qml
qml/StartChatPage.qml
qml/ImageEditorPage.qml qml/ImageEditorPage.qml
qml/WelcomePage.qml qml/WelcomePage.qml
qml/General.qml qml/General.qml
@@ -266,7 +270,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/EmojiTonesPicker.qml qml/EmojiTonesPicker.qml
qml/EmojiDelegate.qml qml/EmojiDelegate.qml
qml/EmojiGrid.qml qml/EmojiGrid.qml
qml/SearchPage.qml qml/RoomSearchPage.qml
qml/LocationDelegate.qml qml/LocationDelegate.qml
qml/LocationChooser.qml qml/LocationChooser.qml
qml/TimelineView.qml qml/TimelineView.qml
@@ -297,11 +301,16 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/NotificationsView.qml qml/NotificationsView.qml
qml/LoadingDelegate.qml qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml qml/TimelineEndDelegate.qml
qml/SearchPage.qml
qml/ServerComboBox.qml
qml/UserSearchPage.qml
RESOURCES RESOURCES
qml/confetti.png qml/confetti.png
qml/glowdot.png qml/glowdot.png
) )
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
if(WIN32) if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib") set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif() endif()
@@ -315,6 +324,15 @@ ecm_qt_declare_logging_category(neochat
EXPORT NEOCHAT EXPORT NEOCHAT
) )
ecm_qt_declare_logging_category(neochat
HEADER "publicroomlist_logging.h"
IDENTIFIER "PublicRoomList"
CATEGORY_NAME "org.kde.neochat.publicroomlistmodel"
DESCRIPTION "Neochat: publicroomlistmodel"
DEFAULT_SEVERITY Info
EXPORT NEOCHAT
)
ecm_qt_declare_logging_category(neochat ecm_qt_declare_logging_category(neochat
HEADER "eventhandler_logging.h" HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling" IDENTIFIER "EventHandling"

8
src/config-neochat.h.in Normal file
View File

@@ -0,0 +1,8 @@
/*
SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"

View File

@@ -7,10 +7,7 @@
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
#include <KLocalizedString> #include <KLocalizedString>
#include <KWindowConfig>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication> #include <QGuiApplication>
#include <QNetworkProxy> #include <QNetworkProxy>
#include <QQuickTextDocument> #include <QQuickTextDocument>
@@ -26,15 +23,13 @@
#include <Quotient/csapi/logout.h> #include <Quotient/csapi/logout.h>
#include <Quotient/csapi/notifications.h> #include <Quotient/csapi/notifications.h>
#include <Quotient/eventstats.h> #include <Quotient/eventstats.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h> #include <Quotient/qt_connection_util.h>
#include <Quotient/user.h>
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "notificationsmanager.h" #include "notificationsmanager.h"
#include "proxycontroller.h"
#include "roommanager.h" #include "roommanager.h"
#include "windowcontroller.h"
#if defined(Q_OS_WIN) || defined(Q_OS_MAC) #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
#include "trayicon.h" #include "trayicon.h"
@@ -51,7 +46,7 @@ Controller::Controller(QObject *parent)
{ {
Connection::setRoomType<NeoChatRoom>(); Connection::setRoomType<NeoChatRoom>();
setApplicationProxy(); ProxyController::instance().setApplicationProxy();
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
setQuitOnLastWindowClosed(); setQuitOnLastWindowClosed();
@@ -68,7 +63,6 @@ Controller::Controller(QObject *parent)
connect(c, &Connection::connected, this, [c, this]() { connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c); m_accountRegistry.add(c);
c->syncLoop(); c->syncLoop();
Q_EMIT initiated();
}); });
} }
@@ -78,8 +72,7 @@ Controller::Controller(QObject *parent)
}); });
#ifndef Q_OS_WINDOWS #ifndef Q_OS_WINDOWS
// Setup Unix signal handlers const auto unixExitHandler = [](int) -> void {
const auto unixExitHandler = [](int /*sig*/) -> void {
QCoreApplication::quit(); QCoreApplication::quit();
}; };
@@ -105,7 +98,7 @@ Controller::Controller(QObject *parent)
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() { connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) { if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]); auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() { connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection); NotificationsManager::instance().handleNotifications(connection);
}); });
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] { connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
@@ -137,23 +130,6 @@ Controller &Controller::instance()
return _instance; return _instance;
} }
void Controller::toggleWindow()
{
auto &instance = WindowController::instance();
auto window = instance.window();
if (window->isVisible()) {
if (window->windowStates() & Qt::WindowMinimized) {
window->showNormal();
window->requestActivate();
} else {
window->close();
}
} else {
instance.showAndRaiseWindow({});
instance.window()->requestActivate();
}
}
void Controller::addConnection(NeoChatConnection *c) void Controller::addConnection(NeoChatConnection *c)
{ {
Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection"); Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection");
@@ -162,9 +138,7 @@ void Controller::addConnection(NeoChatConnection *c)
c->setLazyLoading(true); c->setLazyLoading(true);
connect(c, &NeoChatConnection::syncDone, this, [this, c] { connect(c, &NeoChatConnection::syncDone, this, [c] {
Q_EMIT syncDone();
c->sync(30000); c->sync(30000);
c->saveState(); c->saveState();
}); });
@@ -172,12 +146,6 @@ void Controller::addConnection(NeoChatConnection *c)
dropConnection(c); dropConnection(c);
}); });
connect(c, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
if (job->error() == BaseJob::UserConsentRequired) {
Q_EMIT userConsentRequired(job->errorUrl());
}
});
c->sync(); c->sync();
Q_EMIT connectionAdded(c); Q_EMIT connectionAdded(c);
@@ -194,18 +162,13 @@ void Controller::dropConnection(NeoChatConnection *c)
void Controller::invokeLogin() void Controller::invokeLogin()
{ {
const auto accounts = SettingsGroup("Accounts"_ls).childGroups(); const auto accounts = SettingsGroup("Accounts"_ls).childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) { for (const auto &accountId : accounts) {
AccountSettings account{accountId}; AccountSettings account{accountId};
m_accountsLoading += accountId; m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged(); Q_EMIT accountsLoadingChanged();
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
}
if (!account.homeserver().isEmpty()) { if (!account.homeserver().isEmpty()) {
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account); auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, id, this, accessTokenLoadingJob](QKeychain::Job *) { connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
AccountSettings account{accountId}; AccountSettings account{accountId};
QString accessToken; QString accessToken;
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) { if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
@@ -215,30 +178,11 @@ void Controller::invokeLogin()
} }
auto connection = new NeoChatConnection(account.homeserver()); auto connection = new NeoChatConnection(account.homeserver());
connect(connection, &NeoChatConnection::connected, this, [this, connection, id] { connect(connection, &NeoChatConnection::connected, this, [this, connection] {
connection->loadState(); connection->loadState();
addConnection(connection); addConnection(connection);
m_accountsLoading.removeAll(connection->userId()); m_accountsLoading.removeAll(connection->userId());
Q_EMIT accountsLoadingChanged(); Q_EMIT accountsLoadingChanged();
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &NeoChatConnection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &NeoChatConnection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"), {});
connection->logout(false);
} else if (error == "Connection closed"_ls) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error), {});
// Failed due to network connection issue. This might happen when the homeserver is
// temporary down, or the user trying to re-launch NeoChat in a network that cannot
// connect to the homeserver. In this case, we don't want to do logout().
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error), {});
connection->logout(true);
}
Q_EMIT initiated();
}); });
connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) { connect(connection, &NeoChatConnection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error), {}); Q_EMIT errorOccured(i18n("Network Error: %1", error), {});
@@ -247,9 +191,6 @@ void Controller::invokeLogin()
}); });
} }
} }
if (accounts.isEmpty()) {
Q_EMIT initiated();
}
} }
QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account) QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
@@ -322,10 +263,8 @@ void Controller::setQuitOnLastWindowClosed()
if (NeoChatConfig::self()->systemTray()) { if (NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this); m_trayIcon = new TrayIcon(this);
m_trayIcon->show(); m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
} else { } else {
if (m_trayIcon) { if (m_trayIcon) {
disconnect(m_trayIcon, &TrayIcon::toggleWindow, this, &Controller::toggleWindow);
delete m_trayIcon; delete m_trayIcon;
m_trayIcon = nullptr; m_trayIcon = nullptr;
} }
@@ -346,79 +285,31 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
if (connection == m_connection) { if (connection == m_connection) {
return; return;
} }
if (m_connection != nullptr) {
disconnect(m_connection, &NeoChatConnection::syncError, this, nullptr);
disconnect(m_connection, &NeoChatConnection::accountDataChanged, this, nullptr);
}
m_connection = connection; m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
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."));
}
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
NeoChatConfig::self()->save();
Q_EMIT activeConnectionChanged(); Q_EMIT activeConnectionChanged();
} }
void Controller::saveWindowGeometry()
{
WindowController::instance().saveGeometry();
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
}
void Controller::listenForNotifications() void Controller::listenForNotifications()
{ {
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat")); auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) { auto timer = new QTimer();
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
connect(connector, &KUnifiedPush::Connector::messageReceived, [timer](const QByteArray &data) {
NotificationsManager::instance().postPushNotification(data); NotificationsManager::instance().postPushNotification(data);
timer->stop();
}); });
// Wait five seconds to see if we received any messages or this happened to be an erroneous activation.
// Otherwise, messageReceived is never activated, and this daemon could stick around forever.
timer->start(5000);
connector->registerClient(i18n("Receiving push notifications")); connector->registerClient(i18n("Receiving push notifications"));
#endif #endif
} }
void Controller::setApplicationProxy()
{
NeoChatConfig *cfg = NeoChatConfig::self();
QNetworkProxy proxy;
// type match to ProxyType from neochatconfig.kcfg
switch (cfg->proxyType()) {
case 1: // HTTP
proxy.setType(QNetworkProxy::HttpProxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 2: // SOCKS 5
proxy.setType(QNetworkProxy::Socks5Proxy);
proxy.setHostName(cfg->proxyHost());
proxy.setPort(cfg->proxyPort());
proxy.setUser(cfg->proxyUser());
proxy.setPassword(cfg->proxyPassword());
break;
case 0: // System Default
default:
// do nothing
break;
}
QNetworkProxy::setApplicationProxy(proxy);
}
bool Controller::isFlatpak() const bool Controller::isFlatpak() const
{ {
#ifdef NEOCHAT_FLATPAK #ifdef NEOCHAT_FLATPAK

View File

@@ -9,23 +9,15 @@
#include "neochatconnection.h" #include "neochatconnection.h"
#include <Quotient/accountregistry.h> #include <Quotient/accountregistry.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h> #include <Quotient/settings.h>
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h> #include <kunifiedpush/connector.h>
#endif #endif
class NeoChatRoom;
class TrayIcon; class TrayIcon;
class QQuickTextDocument; class QQuickTextDocument;
namespace Quotient
{
class Room;
class User;
}
namespace QKeychain namespace QKeychain
{ {
class ReadPasswordJob; class ReadPasswordJob;
@@ -92,22 +84,8 @@ public:
[[nodiscard]] bool supportSystemTray() const; [[nodiscard]] bool supportSystemTray() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
* @sa QNetworkProxy::setApplicationProxy
*/
Q_INVOKABLE void setApplicationProxy();
bool isFlatpak() const; bool isFlatpak() const;
/**
* @brief Force a QQuickTextDocument to refresh when images are loaded.
*
* HACK: This is a workaround for QTBUG 93281.
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
/** /**
* @brief Start listening for notifications in dbus-activated mode. * @brief Start listening for notifications in dbus-activated mode.
* These notifications will quit the application when closed. * These notifications will quit the application when closed.
@@ -128,7 +106,6 @@ private:
void loadSettings(); void loadSettings();
void saveSettings() const; void saveSettings() const;
QMap<Quotient::Room *, int> m_notificationCounts;
Quotient::AccountRegistry m_accountRegistry; Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading; QStringList m_accountsLoading;
@@ -136,21 +113,12 @@ private:
private Q_SLOTS: private Q_SLOTS:
void invokeLogin(); void invokeLogin();
void toggleWindow();
void setQuitOnLastWindowClosed(); void setQuitOnLastWindowClosed();
Q_SIGNALS: Q_SIGNALS:
void errorOccured(const QString &error, const QString &detail); void errorOccured(const QString &error, const QString &detail);
void syncDone();
void connectionAdded(NeoChatConnection *connection); void connectionAdded(NeoChatConnection *connection);
void connectionDropped(NeoChatConnection *connection); void connectionDropped(NeoChatConnection *connection);
void initiated();
void quitOnLastWindowClosedChanged();
void unreadCountChanged();
void activeConnectionChanged(); void activeConnectionChanged();
void userConsentRequired(QUrl url);
void accountsLoadingChanged(); void accountsLoadingChanged();
public Q_SLOTS:
void saveWindowGeometry();
}; };

186
src/enums/pushrule.h Normal file
View File

@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
/**
* @class PushRuleKind
*
* A class with the Kind enum for push notifications and helper functions.
*
* The kind relates to the kinds of push rule defined in the matrix spec, see
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
*/
class PushRuleKind : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the different kinds of push rule.
*/
enum Kind {
Override = 0, /**< The highest priority rules. */
Content, /**< These configure behaviour for messages that match certain patterns. */
Room, /**< These rules change the behaviour of all messages for a given room. */
Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */
Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */
};
Q_ENUM(Kind)
/**
* @brief Translate the Kind enum value to a human readable string.
*
* @sa Kind
*/
static QString kindString(Kind kind)
{
switch (kind) {
case Kind::Override:
return QLatin1String("override");
case Kind::Content:
return QLatin1String("content");
case Kind::Room:
return QLatin1String("room");
case Kind::Sender:
return QLatin1String("sender");
case Kind::Underride:
return QLatin1String("underride");
default:
return {};
}
};
};
/**
* @class PushRuleAction
*
* A class with the Action enum for push notifications.
*
* The action relates to the actions of push rule defined in the matrix spec, see
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
*/
class PushRuleAction : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the global push notification actions.
*/
enum Action {
Unknown = 0, /**< The action has not yet been obtained from the server. */
Off, /**< No push notifications are to be sent. */
On, /**< Push notifications are on. */
Noisy, /**< Push notifications are on, also trigger a notification sound. */
Highlight, /**< Push notifications are on, also the event should be highlighted in chat. */
NoisyHighlight, /**< Push notifications are on, also trigger a notification sound and highlight in chat. */
};
Q_ENUM(Action)
};
/**
* @class PushNotificationState
*
* A class with the State enum for room push notification state.
*
* The state define whether the room adheres to the global push rule states for the
* account or is overridden for a room.
*
* @note This is different to the PushRuleAction which defines the type of notification
* for an individual rule.
*
* @sa PushRuleAction
*/
class PushNotificationState : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Describes the push notification state for the room.
*/
enum State {
Unknown, /**< The state has not yet been obtained from the server. */
Default, /**< The room follows the globally configured rules for the local user. */
Mute, /**< No notifications for messages in the room. */
MentionKeyword, /**< Notifications only for local user mentions and keywords. */
All, /**< Notifications for all messages. */
};
Q_ENUM(State)
};
/**
* @class PushRuleSection
*
* A class with the Section enum for push notifications and helper functions.
*
* @note This is different from the PushRuleKind and instead is used for sorting
* in the settings page which is not necessarily by Kind.
*
* @sa PushRuleKind
*/
class PushRuleSection : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the sections to sort push rules into.
*/
enum Section {
Master = 0, /**< The master push rule */
Room, /**< Push rules relating to all rooms. */
Mentions, /**< Push rules relating to user mentions. */
Keywords, /**< Global Keyword push rules. */
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
Invites, /**< Push rules relating to invites. */
Unknown, /**< New default push rules that have not been added to the model yet. */
/**
* @brief Push rules that should never be shown.
*
* There are numerous rules that get set that shouldn't be shown in the general
* list e.g. The array of rules used to override global settings in individual
* rooms.
*
* This is specifically different to unknown which are just new default push
* rule that haven't been added to the model yet.
*/
Undefined,
};
Q_ENUM(Section)
/**
* @brief Translate the Section enum value to a human readable string.
*
* @sa Section
*/
static QString sectionString(Section section)
{
switch (section) {
case Section::Master:
return QLatin1String("Master");
case Section::Room:
return QLatin1String("Room Notifications");
case Section::Mentions:
return QLatin1String("@Mentions");
case Section::Keywords:
return QLatin1String("Keywords");
case Section::Invites:
return QLatin1String("Invites");
default:
return {};
}
};
};

View File

@@ -355,7 +355,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
if (e.repeatsState()) { if (e.repeatsState()) {
auto text = i18n("reinvited %1 to the room", subjectName); auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) { if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped(); text += i18nc("Optional reason for an invitation", ": %1") + (prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
} }
return text; return text;
} }
@@ -379,7 +379,9 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
if (!e.newDisplayName()) { if (!e.newDisplayName()) {
text = i18nc("their refers to a singular user", "cleared their display name"); text = i18nc("their refers to a singular user", "cleared their display name");
} else { } else {
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped()); text = i18nc("their refers to a singular user",
"changed their display name to %1",
prettyPrint ? e.newDisplayName()->toHtmlEscaped() : *e.newDisplayName());
} }
} }
if (e.isAvatarUpdate()) { if (e.isAvatarUpdate()) {
@@ -415,7 +417,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
if (e.reason().isEmpty()) { if (e.reason().isEmpty()) {
return i18n("banned %1 from the room", subjectName); return i18n("banned %1 from the room", subjectName);
} else { } else {
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped()); return i18n("banned %1 from the room: %2", subjectName, prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
} }
} else { } else {
return i18n("self-banned from the room"); return i18n("self-banned from the room");
@@ -431,8 +433,8 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
[](const RoomCanonicalAliasEvent &e) { [](const RoomCanonicalAliasEvent &e) {
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias()); return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
}, },
[](const RoomNameEvent &e) { [prettyPrint](const RoomNameEvent &e) {
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped()); return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", prettyPrint ? e.name().toHtmlEscaped() : e.name());
}, },
[prettyPrint, stripNewlines](const RoomTopicEvent &e) { [prettyPrint, stripNewlines](const RoomTopicEvent &e) {
return (e.topic().isEmpty()) ? i18n("cleared the topic") return (e.topic().isEmpty()) ? i18n("cleared the topic")
@@ -447,14 +449,15 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
[](const EncryptionEvent &) { [](const EncryptionEvent &) {
return i18n("activated End-to-End Encryption"); return i18n("activated End-to-End Encryption");
}, },
[](const RoomCreateEvent &e) { [prettyPrint](const RoomCreateEvent &e) {
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped()) return e.isUpgrade()
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped()); ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()))
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()));
}, },
[](const RoomPowerLevelsEvent &) { [](const RoomPowerLevelsEvent &) {
return i18nc("'power level' means permission level", "changed the power levels for this room"); return i18nc("'power level' means permission level", "changed the power levels for this room");
}, },
[](const StateEvent &e) { [prettyPrint](const StateEvent &e) {
if (e.matrixType() == QLatin1String("m.room.server_acl")) { if (e.matrixType() == QLatin1String("m.room.server_acl")) {
return i18n("changed the server access control lists for this room"); return i18n("changed the server access control lists for this room");
} }
@@ -471,7 +474,7 @@ QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat f
return e.contentJson()["description"_ls].toString(); return e.contentJson()["description"_ls].toString();
} }
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType()) return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped()); : i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
}, },
[](const PollStartEvent &e) { [](const PollStartEvent &e) {
return e.question(); return e.question();
@@ -774,97 +777,6 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
return mediaInfo; return mediaInfo;
} }
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;
}
QString text;
auto event = eventCast<const RoomMessageEvent>(m_event);
if (event->hasTextContent()) {
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
TextHandler textHandler;
textHandler.setData(text);
QList<QUrl> links = textHandler.getLinkPreviews();
if (links.size() > 0) {
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
} else {
return nullptr;
}
}
QSharedPointer<ReactionModel> EventHandler::getReactions() const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
return nullptr;
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
return nullptr;
}
if (!m_event->is<RoomMessageEvent>()) {
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
return nullptr;
}
auto eventId = m_event->id();
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
if (annotations.isEmpty()) {
return nullptr;
};
QMap<QString, QList<User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId()));
}
}
if (reactions.isEmpty()) {
return nullptr;
}
QList<ReactionModel::Reaction> res;
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_room->getUser(author));
}
res.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
if (res.size() > 0) {
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
} else {
return nullptr;
}
}
bool EventHandler::hasReply() const bool EventHandler::hasReply() const
{ {
if (m_event == nullptr) { if (m_event == nullptr) {

View File

@@ -231,25 +231,6 @@ public:
*/ */
QVariantMap getMediaInfo() const; QVariantMap getMediaInfo() const;
/**
* @brief Return a LinkPreviewer object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty LinkPreviewer provided if null.
*
* @sa LinkPreviewer
*/
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
/**
* @brief Return a ReactionModel object for the event.
*
* A nullptr will be returned for any event that doesn't have any links so the
* return should be null checked and an empty QVariantList (or other suitable
* empty mode) provided if null.
*/
QSharedPointer<ReactionModel> getReactions() const;
/** /**
* @brief Whether the event is a reply to another in the timeline. * @brief Whether the event is a reply to another in the timeline.
*/ */

View File

@@ -3,25 +3,37 @@
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "controller.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include <Quotient/csapi/content-repo.h> #include <Quotient/csapi/content-repo.h>
#include <Quotient/events/roommessageevent.h>
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "utils.h"
using namespace Quotient; using namespace Quotient;
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url) LinkPreviewer::LinkPreviewer(const NeoChatRoom *room, const Quotient::RoomMessageEvent *event)
: QObject(parent) : QObject(nullptr)
, m_currentRoom(room) , m_currentRoom(room)
, m_event(event)
, m_loaded(false) , m_loaded(false)
, m_url(url) , m_url(linkPreview(event))
{ {
loadUrlPreview(); connect(this, &LinkPreviewer::urlChanged, this, &LinkPreviewer::emptyChanged);
if (m_currentRoom) {
if (m_event != nullptr && m_currentRoom != nullptr) {
loadUrlPreview();
connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview); connect(m_currentRoom, &NeoChatRoom::urlPreviewEnabledChanged, this, &LinkPreviewer::loadUrlPreview);
// Make sure that we react to edits
connect(m_currentRoom, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_event->id() == newEvent->id()) {
m_event = eventCast<const Quotient::RoomMessageEvent>(newEvent);
m_url = linkPreview(m_event);
Q_EMIT urlChanged();
loadUrlPreview();
}
});
} }
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview); connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, &LinkPreviewer::loadUrlPreview);
} }
@@ -51,15 +63,6 @@ QUrl LinkPreviewer::url() const
return m_url; return m_url;
} }
void LinkPreviewer::setUrl(QUrl url)
{
if (url != m_url) {
m_url = url;
urlChanged();
loadUrlPreview();
}
}
void LinkPreviewer::loadUrlPreview() void LinkPreviewer::loadUrlPreview()
{ {
if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) { if (!m_currentRoom || !NeoChatConfig::showLinkPreview() || !m_currentRoom->urlPreviewEnabled()) {
@@ -98,4 +101,38 @@ bool LinkPreviewer::empty() const
return m_url.isEmpty(); return m_url.isEmpty();
} }
QUrl LinkPreviewer::linkPreview(const Quotient::RoomMessageEvent *event)
{
if (event == nullptr) {
return {};
}
QString text;
if (event->hasTextContent()) {
auto textContent = static_cast<const Quotient::EventContent::TextContent *>(event->content());
if (textContent) {
text = textContent->body;
} else {
text = event->plainBody();
}
} else {
text = event->plainBody();
}
auto data = text.remove(TextRegex::removeRichReply);
auto linksMatch = TextRegex::url.globalMatch(data);
while (linksMatch.hasNext()) {
auto link = linksMatch.next().captured();
if (!link.contains(QStringLiteral("matrix.to"))) {
return QUrl(link);
}
}
return {};
}
bool LinkPreviewer::hasPreviewableLinks(const Quotient::RoomMessageEvent *event)
{
return !linkPreview(event).isEmpty();
}
#include "moc_linkpreviewer.cpp" #include "moc_linkpreviewer.cpp"

View File

@@ -7,6 +7,11 @@
#include <QQmlEngine> #include <QQmlEngine>
#include <QUrl> #include <QUrl>
namespace Quotient
{
class RoomMessageEvent;
}
class NeoChatRoom; class NeoChatRoom;
/** /**
@@ -25,7 +30,7 @@ class LinkPreviewer : public QObject
/** /**
* @brief The URL to get the preview for. * @brief The URL to get the preview for.
*/ */
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
/** /**
* @brief Whether the preview information has been loaded. * @brief Whether the preview information has been loaded.
@@ -55,18 +60,25 @@ class LinkPreviewer : public QObject
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged) Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
public: public:
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {}); explicit LinkPreviewer(const NeoChatRoom *room = nullptr, const Quotient::RoomMessageEvent *event = nullptr);
[[nodiscard]] QUrl url() const; [[nodiscard]] QUrl url() const;
void setUrl(QUrl);
[[nodiscard]] bool loaded() const; [[nodiscard]] bool loaded() const;
[[nodiscard]] QString title() const; [[nodiscard]] QString title() const;
[[nodiscard]] QString description() const; [[nodiscard]] QString description() const;
[[nodiscard]] QUrl imageSource() const; [[nodiscard]] QUrl imageSource() const;
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
/**
* @brief Whether the given event has at least 1 pre-viewable link.
*
* A link is only pre-viewable if it is http, https or something starting with www.
*/
static bool hasPreviewableLinks(const Quotient::RoomMessageEvent *event);
private: private:
const NeoChatRoom *m_currentRoom = nullptr; const NeoChatRoom *m_currentRoom;
const Quotient::RoomMessageEvent *m_event;
bool m_loaded; bool m_loaded;
QString m_title = QString(); QString m_title = QString();
@@ -76,6 +88,14 @@ private:
void loadUrlPreview(); void loadUrlPreview();
/**
* @brief Return the link to be previewed from the given event.
*
* This function is designed to give only links that should be previewed so
* http, https or something starting with www. The first valid link is returned.
*/
static QUrl linkPreview(const Quotient::RoomMessageEvent *event);
Q_SIGNALS: Q_SIGNALS:
void loadedChanged(); void loadedChanged();
void titleChanged(); void titleChanged();

View File

@@ -102,7 +102,6 @@ void LoginHelper::init()
connectSingleShot(m_connection, &Connection::syncDone, this, [this]() { connectSingleShot(m_connection, &Connection::syncDone, this, [this]() {
Q_EMIT loaded(); Q_EMIT loaded();
Q_EMIT Controller::instance().initiated();
}); });
} }

View File

@@ -130,7 +130,7 @@ int main(int argc, char *argv[])
QStringLiteral(NEOCHAT_VERSION_STRING), QStringLiteral(NEOCHAT_VERSION_STRING),
i18n("Matrix client"), i18n("Matrix client"),
KAboutLicense::GPL_V3, KAboutLicense::GPL_V3,
i18n("© 2018-2020 Black Hat, 2020-2023 KDE Community")); i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
about.addAuthor(i18n("Carl Schwan"), about.addAuthor(i18n("Carl Schwan"),
i18n("Maintainer"), i18n("Maintainer"),
QStringLiteral("carl@carlschwan.eu"), QStringLiteral("carl@carlschwan.eu"),

View File

@@ -12,7 +12,6 @@
#include <KLocalizedString> #include <KLocalizedString>
#include "controller.h"
#include "neochatconnection.h" #include "neochatconnection.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>

View File

@@ -4,7 +4,6 @@
#include "actionsmodel.h" #include "actionsmodel.h"
#include "chatbarcache.h" #include "chatbarcache.h"
#include "controller.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "roommanager.h" #include "roommanager.h"
#include <Quotient/events/roommemberevent.h> #include <Quotient/events/roommemberevent.h>

View File

@@ -6,10 +6,8 @@
#include <QImage> #include <QImage>
#include <QMimeDatabase> #include <QMimeDatabase>
#include "controller.h"
#include "emojimodel.h" #include "emojimodel.h"
#include <Quotient/connection.h>
#include <Quotient/csapi/account-data.h> #include <Quotient/csapi/account-data.h>
#include <Quotient/csapi/content-repo.h> #include <Quotient/csapi/content-repo.h>

View File

@@ -6,9 +6,8 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlEngine> #include <QQmlEngine>
#include <QRegularExpression> #include <QRegularExpression>
#include <memory>
class NeoChatConnection; #include "neochatconnection.h"
struct CustomEmoji { struct CustomEmoji {
QString name; // with :semicolons: QString name; // with :semicolons:

View File

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

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "itinerarymodel.h"
#include <QProcess>
#include "config-neochat.h"
#ifndef Q_OS_ANDROID
#include <KIO/ApplicationLauncherJob>
#endif
ItineraryModel::ItineraryModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void ItineraryModel::setConnection(NeoChatConnection *connection)
{
if (m_connection == connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
}
NeoChatConnection *ItineraryModel::connection() const
{
return m_connection;
}
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
auto row = index.row();
auto data = m_data[row];
if (role == NameRole) {
if (data[QStringLiteral("@type")] == QStringLiteral("TrainReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("trainNumber")];
}
if (data[QStringLiteral("@type")] == QStringLiteral("LodgingReservation")) {
return data[QStringLiteral("reservationFor")][QStringLiteral("name")];
}
}
if (role == TypeRole) {
return data[QStringLiteral("@type")];
}
if (role == DepartureStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departureStation")][QStringLiteral("name")];
}
if (role == ArrivalStationRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalStation")][QStringLiteral("name")];
}
if (role == DepartureTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("departureTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == ArrivalTimeRole) {
const auto &time = data[QStringLiteral("reservationFor")][QStringLiteral("arrivalTime")];
auto dateTime = (time.isString() ? time : time[QStringLiteral("@value")]).toVariant().toDateTime();
if (const auto &timeZone = time[QStringLiteral("timezone")].toString(); timeZone.length() > 0) {
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
}
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == AddressRole) {
const auto &addressData = data[QStringLiteral("reservationFor")][QStringLiteral("address")];
return QStringLiteral("%1 - %2 %3 %4")
.arg(addressData[QStringLiteral("streetAddress")].toString(),
addressData[QStringLiteral("postalCode")].toString(),
addressData[QStringLiteral("addressLocality")].toString(),
addressData[QStringLiteral("addressCountry")].toString());
}
if (role == StartTimeRole) {
auto dateTime = data[QStringLiteral("checkinTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == EndTimeRole) {
auto dateTime = data[QStringLiteral("checkoutTime")][QStringLiteral("@value")].toVariant().toDateTime();
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
if (role == DeparturePlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("departurePlatform")];
}
if (role == ArrivalPlatformRole) {
return data[QStringLiteral("reservationFor")][QStringLiteral("arrivalPlatform")];
}
if (role == CoachRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatSection")];
}
if (role == SeatRole) {
return data[QStringLiteral("reservedTicket")][QStringLiteral("ticketedSeat")][QStringLiteral("seatNumber")];
}
return {};
}
int ItineraryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.size();
}
QHash<int, QByteArray> ItineraryModel::roleNames() const
{
return {
{NameRole, "name"},
{TypeRole, "type"},
{DepartureStationRole, "departureStation"},
{ArrivalStationRole, "arrivalStation"},
{DepartureTimeRole, "departureTime"},
{ArrivalTimeRole, "arrivalTime"},
{AddressRole, "address"},
{StartTimeRole, "startTime"},
{EndTimeRole, "endTime"},
{DeparturePlatformRole, "departurePlatform"},
{ArrivalPlatformRole, "arrivalPlatform"},
{CoachRole, "coach"},
{SeatRole, "seat"},
};
}
QString ItineraryModel::path() const
{
return m_path;
}
void ItineraryModel::setPath(const QString &path)
{
if (path == m_path) {
return;
}
m_path = path;
Q_EMIT pathChanged();
loadData();
}
void ItineraryModel::loadData()
{
auto process = new QProcess(this);
process->start(QLatin1String(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + QLatin1String("/kitinerary-extractor"), {m_path.mid(7)});
connect(process, &QProcess::finished, this, [this, process]() {
auto data = process->readAllStandardOutput();
beginResetModel();
m_data = QJsonDocument::fromJson(data).array();
endResetModel();
});
}
void ItineraryModel::sendToItinerary()
{
#ifndef Q_OS_ANDROID
auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.itinerary")));
job->setUrls({QUrl::fromLocalFile(m_path.mid(7))});
job->start();
#endif
}

View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QString>
#include "neochatconnection.h"
class ItineraryModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
public:
enum Roles {
NameRole = Qt::DisplayRole,
TypeRole,
DepartureStationRole,
ArrivalStationRole,
DepartureTimeRole,
ArrivalTimeRole,
AddressRole,
StartTimeRole,
EndTimeRole,
DeparturePlatformRole,
ArrivalPlatformRole,
CoachRole,
SeatRole,
};
Q_ENUM(Roles)
explicit ItineraryModel(QObject *parent = nullptr);
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
QString path() const;
void setPath(const QString &path);
Q_INVOKABLE void sendToItinerary();
Q_SIGNALS:
void connectionChanged();
void pathChanged();
private:
QPointer<NeoChatConnection> m_connection;
QJsonArray m_data;
QString m_path;
void loadData();
};

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
#include "messageeventmodel.h" #include "messageeventmodel.h"
#include "linkpreviewer.h"
#include "messageeventmodel_logging.h" #include "messageeventmodel_logging.h"
#include "neochatconfig.h" #include "neochatconfig.h"
@@ -20,7 +21,9 @@
#include "enums/delegatetype.h" #include "enums/delegatetype.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "events/pollevent.h"
#include "models/reactionmodel.h" #include "models/reactionmodel.h"
#include "texthandler.h"
using namespace Quotient; using namespace Quotient;
@@ -34,7 +37,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[TimeStringRole] = "timeString"; roles[TimeStringRole] = "timeString";
roles[SectionRole] = "section"; roles[SectionRole] = "section";
roles[AuthorRole] = "author"; roles[AuthorRole] = "author";
roles[ContentRole] = "content";
roles[HighlightRole] = "isHighlighted"; roles[HighlightRole] = "isHighlighted";
roles[SpecialMarksRole] = "marks"; roles[SpecialMarksRole] = "marks";
roles[ProgressInfoRole] = "progressInfo"; roles[ProgressInfoRole] = "progressInfo";
@@ -65,12 +67,19 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[LatitudeRole] = "latitude"; roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude"; roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset"; roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
return roles; return roles;
} }
MessageEventModel::MessageEventModel(QObject *parent) MessageEventModel::MessageEventModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
connect(this, &MessageEventModel::modelAboutToBeReset, this, [this]() {
resetting = true;
});
connect(this, &MessageEventModel::modelReset, this, [this]() {
resetting = false;
});
} }
NeoChatRoom *MessageEventModel::room() const NeoChatRoom *MessageEventModel::room() const
@@ -101,6 +110,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) { if (const auto &roomMessageEvent = &*event->viewAs<RoomMessageEvent>()) {
createEventObjects(roomMessageEvent); createEventObjects(roomMessageEvent);
} }
if (event->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event->event()));
}
} }
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) { if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
@@ -151,6 +163,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
} }
} }
} }
if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
}
} }
m_initialized = true; m_initialized = true;
beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1); beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1);
@@ -160,6 +175,9 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) { if (const auto &roomMessageEvent = dynamic_cast<RoomMessageEvent *>(event.get())) {
createEventObjects(roomMessageEvent); createEventObjects(roomMessageEvent);
} }
if (event->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(event.get()));
}
} }
if (rowCount() > 0) { if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312 rowBelowInserted = rowCount() - 1; // See #312
@@ -221,6 +239,10 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}); });
connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) {
refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex()); refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex());
const RoomMessageEvent *message = eventCast<const RoomMessageEvent>(newEvent);
if (message != nullptr) {
createEventObjects(message);
}
}); });
connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) { connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) {
if (eventId.isEmpty()) { // How did we get here? if (eventId.isEmpty()) { // How did we get here?
@@ -231,8 +253,11 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) { if (const auto &event = dynamic_cast<const RoomMessageEvent *>(&**eventIt)) {
createEventObjects(event); createEventObjects(event);
} }
if (eventIt->event()->is<PollStartEvent>()) {
m_currentRoom->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
}
} }
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole}); refreshEventRoles(eventId, {Qt::DisplayRole});
}); });
connect(m_currentRoom, &Room::changed, this, [this]() { connect(m_currentRoom, &Room::changed, this, [this]() {
for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) { for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) {
@@ -253,6 +278,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
lastReadEventId.clear(); lastReadEventId.clear();
} }
endResetModel(); endResetModel();
// After reset put a read marker in if required.
// This is needed when changing back to a room that has already loaded messages.
if (room) {
moveReadMarker(m_currentRoom->lastFullyReadEventId());
}
} }
int MessageEventModel::refreshEvent(const QString &eventId) int MessageEventModel::refreshEvent(const QString &eventId)
@@ -479,28 +510,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return eventHandler.getAuthor(isPending); return eventHandler.getAuthor(isPending);
} }
if (role == ContentRole) {
if (evt.isRedacted()) {
auto reason = evt.redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("[REDACTED]") : i18n("[REDACTED: %1]").arg(evt.redactedBecause()->reason());
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base
return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
};
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(e->image().originalJson);
}
return evt.contentJson();
}
if (role == HighlightRole) { if (role == HighlightRole) {
return eventHandler.isHighlighted(); return eventHandler.isHighlighted();
} }
@@ -696,6 +705,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return row < static_cast<int>(m_currentRoom->pendingEvents().size()); return row < static_cast<int>(m_currentRoom->pendingEvents().size());
} }
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_currentRoom->poll(evt.id()));
}
return {}; return {};
} }
@@ -713,19 +726,37 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
{ {
auto eventId = event->id(); auto eventId = event->id();
EventHandler eventHandler; if (m_linkPreviewers.contains(eventId)) {
eventHandler.setRoom(m_currentRoom); if (!LinkPreviewer::hasPreviewableLinks(event)) {
eventHandler.setEvent(event); m_linkPreviewers.remove(eventId);
}
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
m_linkPreviewers[eventId] = linkPreviewer;
} else { } else {
m_linkPreviewers.remove(eventId); if (LinkPreviewer::hasPreviewableLinks(event)) {
m_linkPreviewers[eventId] = QSharedPointer<LinkPreviewer>(new LinkPreviewer(m_currentRoom, event));
}
} }
if (auto reactionModel = eventHandler.getReactions()) {
m_reactionModels[eventId] = reactionModel; // ReactionModel handles updates to add and remove reactions, we only need to
// handle adding and removing whole models here.
if (m_reactionModels.contains(eventId)) {
// If a model already exists but now has no reactions remove it
if (m_reactionModels[eventId]->rowCount() <= 0) {
m_reactionModels.remove(eventId);
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
}
}
} else { } else {
m_reactionModels.remove(eventId); if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) {
// If a model doesn't exist and there are reactions add it.
auto reactionModel = QSharedPointer<ReactionModel>(new ReactionModel(event, m_currentRoom));
if (reactionModel->rowCount() > 0) {
m_reactionModels[eventId] = reactionModel;
if (!resetting) {
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole});
}
}
}
} }
} }

View File

@@ -9,6 +9,7 @@
#include "linkpreviewer.h" #include "linkpreviewer.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "pollhandler.h"
class ReactionModel; class ReactionModel;
@@ -45,7 +46,6 @@ public:
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */ TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
SectionRole, /**< The date of the event as a string. */ SectionRole, /**< The date of the event as a string. */
AuthorRole, /**< The author of the event. */ AuthorRole, /**< The author of the event. */
ContentRole, /**< The full message content. */
HighlightRole, /**< Whether the event should be highlighted. */ HighlightRole, /**< Whether the event should be highlighted. */
SpecialMarksRole, /**< Whether the event is hidden or not. */ SpecialMarksRole, /**< Whether the event is hidden or not. */
ProgressInfoRole, /**< Progress info when downloading files. */ ProgressInfoRole, /**< Progress info when downloading files. */
@@ -83,6 +83,7 @@ public:
LatitudeRole, /**< Latitude for a location event. */ LatitudeRole, /**< Latitude for a location event. */
LongitudeRole, /**< Longitude for a location event. */ LongitudeRole, /**< Longitude for a location event. */
AssetRole, /**< Type of location event, e.g. self pin of the user location. */ AssetRole, /**< Type of location event, e.g. self pin of the user location. */
PollHandlerRole, /**< The PollHandler for the event, if any. */
LastRole, // Keep this last LastRole, // Keep this last
}; };
Q_ENUM(EventRoles) Q_ENUM(EventRoles)
@@ -130,6 +131,7 @@ private:
QString lastReadEventId; QString lastReadEventId;
QPersistentModelIndex m_lastReadEventIndex; QPersistentModelIndex m_lastReadEventIndex;
int rowBelowInserted = -1; int rowBelowInserted = -1;
bool resetting = false;
bool movingEvent = false; bool movingEvent = false;
KFormat m_format; KFormat m_format;

View File

@@ -5,6 +5,8 @@
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include "publicroomlist_logging.h"
using namespace Quotient; using namespace Quotient;
PublicRoomListModel::PublicRoomListModel(QObject *parent) PublicRoomListModel::PublicRoomListModel(QObject *parent)
@@ -41,7 +43,7 @@ void PublicRoomListModel::setConnection(Connection *conn)
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) { if (m_connection) {
@@ -50,7 +52,6 @@ void PublicRoomListModel::setConnection(Connection *conn)
Q_EMIT connectionChanged(); Q_EMIT connectionChanged();
Q_EMIT serverChanged(); Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
} }
QString PublicRoomListModel::server() const QString PublicRoomListModel::server() const
@@ -71,14 +72,13 @@ void PublicRoomListModel::setServer(const QString &value)
nextBatch = QString(); nextBatch = QString();
attempted = false; attempted = false;
rooms.clear(); rooms.clear();
Q_EMIT loadingChanged();
endResetModel(); endResetModel();
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) { if (m_connection) {
@@ -86,42 +86,30 @@ void PublicRoomListModel::setServer(const QString &value)
} }
Q_EMIT serverChanged(); Q_EMIT serverChanged();
Q_EMIT hasMoreChanged();
} }
QString PublicRoomListModel::keyword() const QString PublicRoomListModel::searchText() const
{ {
return m_keyword; return m_searchText;
} }
void PublicRoomListModel::setKeyword(const QString &value) void PublicRoomListModel::setSearchText(const QString &value)
{ {
if (m_keyword == value) { if (m_searchText == value) {
return; return;
} }
m_keyword = value; m_searchText = value;
Q_EMIT searchTextChanged();
beginResetModel();
nextBatch = QString(); nextBatch = QString();
attempted = false; attempted = false;
rooms.clear();
endResetModel();
if (job) { if (job) {
job->abandon(); job->abandon();
job = nullptr; job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
} }
if (m_connection) {
next();
}
Q_EMIT keywordChanged();
Q_EMIT hasMoreChanged();
} }
bool PublicRoomListModel::showOnlySpaces() const bool PublicRoomListModel::showOnlySpaces() const
@@ -138,15 +126,28 @@ void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
Q_EMIT showOnlySpacesChanged(); Q_EMIT showOnlySpacesChanged();
} }
void PublicRoomListModel::next(int count) void PublicRoomListModel::search(int limit)
{ {
if (count < 1) { if (limit < 1 || attempted) {
return; return;
} }
if (job) { if (job) {
qDebug() << "PublicRoomListModel: Other jobs running, ignore"; qCDebug(PublicRoomList) << "Other job running, ignore";
return;
}
next(limit);
}
void PublicRoomListModel::next(int limit)
{
if (m_connection == nullptr || limit < 1) {
return;
}
if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore";
return; return;
} }
@@ -154,11 +155,17 @@ void PublicRoomListModel::next(int count)
if (m_showOnlySpaces) { if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space"); roomTypes += QLatin1String("m.space");
} }
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword, roomTypes}); job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] { connect(job, &BaseJob::finished, this, [this] {
attempted = true; if (!attempted) {
beginResetModel();
rooms.clear();
endResetModel();
attempted = true;
}
if (job->status() == BaseJob::Success) { if (job->status() == BaseJob::Success) {
nextBatch = job->nextBatch(); nextBatch = job->nextBatch();
@@ -166,14 +173,10 @@ void PublicRoomListModel::next(int count)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1); this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk()); rooms.append(job->chunk());
this->endInsertRows(); this->endInsertRows();
if (job->nextBatch().isEmpty()) {
Q_EMIT hasMoreChanged();
}
} }
this->job = nullptr; this->job = nullptr;
Q_EMIT loadingChanged(); Q_EMIT searchingChanged();
}); });
} }
@@ -184,8 +187,7 @@ QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
} }
if (index.row() >= rooms.count()) { if (index.row() >= rooms.count()) {
qDebug() << "PublicRoomListModel, something's wrong: index.row() >= " qCDebug(PublicRoomList) << "something's wrong: index.row() >= rooms.count()";
"rooms.count()";
return {}; return {};
} }
auto room = rooms.at(index.row()); auto room = rooms.at(index.row());
@@ -271,12 +273,19 @@ int PublicRoomListModel::rowCount(const QModelIndex &parent) const
return rooms.count(); return rooms.count();
} }
bool PublicRoomListModel::hasMore() const bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
{ {
return !(attempted && nextBatch.isEmpty()); Q_UNUSED(parent)
return !nextBatch.isEmpty();
} }
bool PublicRoomListModel::loading() const void PublicRoomListModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent)
next();
}
bool PublicRoomListModel::searching() const
{ {
return job != nullptr; return job != nullptr;
} }

View File

@@ -41,9 +41,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged) Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged)
/** /**
* @brief The filter keyword for the list of public rooms. * @brief The text to search the public room list for.
*/ */
Q_PROPERTY(QString keyword READ keyword WRITE setKeyword NOTIFY keywordChanged) Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/** /**
* @brief Whether only space rooms should be shown. * @brief Whether only space rooms should be shown.
@@ -51,14 +51,9 @@ class PublicRoomListModel : public QAbstractListModel
Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged) Q_PROPERTY(bool showOnlySpaces READ showOnlySpaces WRITE setShowOnlySpaces NOTIFY showOnlySpacesChanged)
/** /**
* @brief Whether the model has more items to load. * @brief Whether the model is searching.
*/ */
Q_PROPERTY(bool hasMore READ hasMore NOTIFY hasMoreChanged) Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
/**
* @biref Whether the model is still loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public: public:
/** /**
@@ -105,31 +100,38 @@ public:
[[nodiscard]] QString server() const; [[nodiscard]] QString server() const;
void setServer(const QString &value); void setServer(const QString &value);
[[nodiscard]] QString keyword() const; [[nodiscard]] QString searchText() const;
void setKeyword(const QString &value); void setSearchText(const QString &searchText);
[[nodiscard]] bool showOnlySpaces() const; [[nodiscard]] bool showOnlySpaces() const;
void setShowOnlySpaces(bool showOnlySpaces); void setShowOnlySpaces(bool showOnlySpaces);
[[nodiscard]] bool hasMore() const; [[nodiscard]] bool searching() const;
[[nodiscard]] bool loading() const; /**
* @brief Search the room directory.
*
* @param limit the maximum number of rooms to load.
*/
Q_INVOKABLE void search(int limit = 50);
private:
QPointer<Quotient::Connection> m_connection = nullptr;
QString m_server;
QString m_searchText;
bool m_showOnlySpaces = false;
/** /**
* @brief Load the next set of rooms. * @brief Load the next set of rooms.
* *
* @param count the maximum number of rooms to load. * @param limit the maximum number of rooms to load.
*/ */
Q_INVOKABLE void next(int count = 50); void next(int limit = 50);
bool canFetchMore(const QModelIndex &parent) const override;
private: void fetchMore(const QModelIndex &parent) override;
Quotient::Connection *m_connection = nullptr;
QString m_server;
QString m_keyword;
bool m_showOnlySpaces = false;
bool attempted = false; bool attempted = false;
bool m_loading = false; bool m_searching = false;
QString nextBatch; QString nextBatch;
QList<Quotient::PublicRoomsChunk> rooms; QList<Quotient::PublicRoomsChunk> rooms;
@@ -139,8 +141,7 @@ private:
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void serverChanged(); void serverChanged();
void keywordChanged(); void searchTextChanged();
void showOnlySpacesChanged(); void showOnlySpacesChanged();
void hasMoreChanged(); void searchingChanged();
void loadingChanged();
}; };

View File

@@ -11,7 +11,6 @@
#include <Quotient/csapi/pushrules.h> #include <Quotient/csapi/pushrules.h>
#include <Quotient/jobs/basejob.h> #include <Quotient/jobs/basejob.h>
#include "controller.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include <KLazyLocalizedString> #include <KLazyLocalizedString>
@@ -34,25 +33,25 @@ static const QHash<QString, KLazyLocalizedString> defaultRuleNames = {
}; };
// Sections for default rules. // Sections for default rules.
static const QHash<QString, PushNotificationSection::Section> defaultSections = { static const QHash<QString, PushRuleSection::Section> defaultSections = {
{QStringLiteral(".m.rule.master"), PushNotificationSection::Master}, {QStringLiteral(".m.rule.master"), PushRuleSection::Master},
{QStringLiteral(".m.rule.room_one_to_one"), PushNotificationSection::Room}, {QStringLiteral(".m.rule.room_one_to_one"), PushRuleSection::Room},
{QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushNotificationSection::Room}, {QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushRuleSection::Room},
{QStringLiteral(".m.rule.message"), PushNotificationSection::Room}, {QStringLiteral(".m.rule.message"), PushRuleSection::Room},
{QStringLiteral(".m.rule.encrypted"), PushNotificationSection::Room}, {QStringLiteral(".m.rule.encrypted"), PushRuleSection::Room},
{QStringLiteral(".m.rule.tombstone"), PushNotificationSection::Room}, {QStringLiteral(".m.rule.tombstone"), PushRuleSection::Room},
{QStringLiteral(".m.rule.contains_display_name"), PushNotificationSection::Mentions}, {QStringLiteral(".m.rule.contains_display_name"), PushRuleSection::Mentions},
{QStringLiteral(".m.rule.is_user_mention"), PushNotificationSection::Mentions}, {QStringLiteral(".m.rule.is_user_mention"), PushRuleSection::Mentions},
{QStringLiteral(".m.rule.is_room_mention"), PushNotificationSection::Mentions}, {QStringLiteral(".m.rule.is_room_mention"), PushRuleSection::Mentions},
{QStringLiteral(".m.rule.contains_user_name"), PushNotificationSection::Mentions}, {QStringLiteral(".m.rule.contains_user_name"), PushRuleSection::Mentions},
{QStringLiteral(".m.rule.roomnotif"), PushNotificationSection::Mentions}, {QStringLiteral(".m.rule.roomnotif"), PushRuleSection::Mentions},
{QStringLiteral(".m.rule.invite_for_me"), PushNotificationSection::Invites}, {QStringLiteral(".m.rule.invite_for_me"), PushRuleSection::Invites},
{QStringLiteral(".m.rule.call"), PushNotificationSection::Undefined}, // TODO: make invites when VOIP added. {QStringLiteral(".m.rule.call"), PushRuleSection::Undefined}, // TODO: make invites when VOIP added.
{QStringLiteral(".m.rule.suppress_notices"), PushNotificationSection::Undefined}, {QStringLiteral(".m.rule.suppress_notices"), PushRuleSection::Undefined},
{QStringLiteral(".m.rule.member_event"), PushNotificationSection::Undefined}, {QStringLiteral(".m.rule.member_event"), PushRuleSection::Undefined},
{QStringLiteral(".m.rule.reaction"), PushNotificationSection::Undefined}, {QStringLiteral(".m.rule.reaction"), PushRuleSection::Undefined},
{QStringLiteral(".m.rule.room.server_acl"), PushNotificationSection::Undefined}, {QStringLiteral(".m.rule.room.server_acl"), PushRuleSection::Undefined},
{QStringLiteral(".im.vector.jitsi"), PushNotificationSection::Undefined}, {QStringLiteral(".im.vector.jitsi"), PushRuleSection::Undefined},
}; };
// Default rules that don't have a highlight option as it would lead to all messages // Default rules that don't have a highlight option as it would lead to all messages
@@ -67,7 +66,7 @@ static const QStringList noHighlight = {
PushRuleModel::PushRuleModel(QObject *parent) PushRuleModel::PushRuleModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
m_defaultKeywordAction = static_cast<PushNotificationAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault()); m_defaultKeywordAction = static_cast<PushRuleAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
} }
void PushRuleModel::updateNotificationRules(const QString &type) void PushRuleModel::updateNotificationRules(const QString &type)
@@ -83,11 +82,11 @@ void PushRuleModel::updateNotificationRules(const QString &type)
m_rules.clear(); m_rules.clear();
// Doing this 5 times because PushRuleset is a struct. // Doing this 5 times because PushRuleset is a struct.
setRules(ruleData.override, PushNotificationKind::Override); setRules(ruleData.override, PushRuleKind::Override);
setRules(ruleData.content, PushNotificationKind::Content); setRules(ruleData.content, PushRuleKind::Content);
setRules(ruleData.room, PushNotificationKind::Room); setRules(ruleData.room, PushRuleKind::Room);
setRules(ruleData.sender, PushNotificationKind::Sender); setRules(ruleData.sender, PushRuleKind::Sender);
setRules(ruleData.underride, PushNotificationKind::Underride); setRules(ruleData.underride, PushRuleKind::Underride);
Q_EMIT globalNotificationsEnabledChanged(); Q_EMIT globalNotificationsEnabledChanged();
Q_EMIT globalNotificationsSetChanged(); Q_EMIT globalNotificationsSetChanged();
@@ -95,7 +94,7 @@ void PushRuleModel::updateNotificationRules(const QString &type)
endResetModel(); endResetModel();
} }
void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind) void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind)
{ {
for (const auto &rule : rules) { for (const auto &rule : rules) {
QString roomId; QString roomId;
@@ -128,7 +127,7 @@ int PushRuleModel::getRuleIndex(const QString &ruleId) const
return -1; return -1;
} }
PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule rule) PushRuleSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
{ {
auto ruleId = rule.ruleId; auto ruleId = rule.ruleId;
@@ -136,7 +135,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
return defaultSections.value(ruleId); return defaultSections.value(ruleId);
} else { } else {
if (rule.ruleId.startsWith(u'.')) { if (rule.ruleId.startsWith(u'.')) {
return PushNotificationSection::Unknown; return PushRuleSection::Unknown;
} }
/** /**
* If the rule name resolves to a matrix id for a room that the user is part * If the rule name resolves to a matrix id for a room that the user is part
@@ -146,7 +145,7 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
* Rooms that the user hasn't joined shouldn't have a rule. * Rooms that the user hasn't joined shouldn't have a rule.
*/ */
if (m_connection->room(ruleId) != nullptr) { if (m_connection->room(ruleId) != nullptr) {
return PushNotificationSection::Undefined; return PushRuleSection::Undefined;
} }
/** /**
* If the rule name resolves to a matrix id for a user it shouldn't appear * If the rule name resolves to a matrix id for a user it shouldn't appear
@@ -160,26 +159,26 @@ PushNotificationSection::Section PushRuleModel::getSection(Quotient::PushRule ru
testUserId.prepend(u'@'); testUserId.prepend(u'@');
} }
if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) { if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
return PushNotificationSection::Undefined; return PushRuleSection::Undefined;
} }
// If the rule has push conditions and one is a room ID it is a room only keyword. // If the rule has push conditions and one is a room ID it is a room only keyword.
if (!rule.conditions.isEmpty()) { if (!rule.conditions.isEmpty()) {
for (auto condition : rule.conditions) { for (auto condition : rule.conditions) {
if (condition.key == QStringLiteral("room_id")) { if (condition.key == QStringLiteral("room_id")) {
return PushNotificationSection::RoomKeywords; return PushRuleSection::RoomKeywords;
} }
} }
} }
return PushNotificationSection::Keywords; return PushRuleSection::Keywords;
} }
} }
PushNotificationAction::Action PushRuleModel::defaultState() const PushRuleAction::Action PushRuleModel::defaultState() const
{ {
return m_defaultKeywordAction; return m_defaultKeywordAction;
} }
void PushRuleModel::setDefaultState(PushNotificationAction::Action defaultState) void PushRuleModel::setDefaultState(PushRuleAction::Action defaultState)
{ {
if (defaultState == m_defaultKeywordAction) { if (defaultState == m_defaultKeywordAction) {
return; return;
@@ -273,7 +272,7 @@ QHash<int, QByteArray> PushRuleModel::roleNames() const
return roles; return roles;
} }
void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction::Action action) void PushRuleModel::setPushRuleAction(const QString &id, PushRuleAction::Action action)
{ {
int index = getRuleIndex(id); int index = getRuleIndex(id);
if (index == -1) { if (index == -1) {
@@ -283,22 +282,22 @@ void PushRuleModel::setPushRuleAction(const QString &id, PushNotificationAction:
auto rule = m_rules[index]; auto rule = m_rules[index];
// Override rules need to be disabled when off so that other rules can match the message if they apply. // Override rules need to be disabled when off so that other rules can match the message if they apply.
if (action == PushNotificationAction::Off && rule.kind == PushNotificationKind::Override) { if (action == PushRuleAction::Off && rule.kind == PushRuleKind::Override) {
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, false); setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, false);
} else if (rule.kind == PushNotificationKind::Override) { } else if (rule.kind == PushRuleKind::Override) {
setNotificationRuleEnabled(PushNotificationKind::kindString(rule.kind), rule.id, true); setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, true);
} }
setNotificationRuleActions(PushNotificationKind::kindString(rule.kind), rule.id, action); setNotificationRuleActions(PushRuleKind::kindString(rule.kind), rule.id, action);
} }
void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId) void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
{ {
PushNotificationKind::Kind kind = PushNotificationKind::Content; PushRuleKind::Kind kind = PushRuleKind::Content;
const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction); const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction);
QList<Quotient::PushCondition> pushConditions; QList<Quotient::PushCondition> pushConditions;
if (!roomId.isEmpty()) { if (!roomId.isEmpty()) {
kind = PushNotificationKind::Override; kind = PushRuleKind::Override;
Quotient::PushCondition roomCondition; Quotient::PushCondition roomCondition;
roomCondition.kind = QStringLiteral("event_match"); roomCondition.kind = QStringLiteral("event_match");
@@ -314,7 +313,7 @@ void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
} }
auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"), auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
PushNotificationKind::kindString(kind), PushRuleKind::kindString(kind),
keyword, keyword,
actions, actions,
QString(), QString(),
@@ -338,7 +337,7 @@ void PushRuleModel::removeKeyword(const QString &keyword)
return; return;
} }
auto kind = PushNotificationKind::kindString(m_rules[index].kind); auto kind = PushRuleKind::kindString(m_rules[index].kind);
auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id); auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() { connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString(); qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
@@ -355,7 +354,7 @@ void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QStrin
}); });
} }
void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action) void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action)
{ {
QList<QVariant> actions; QList<QVariant> actions;
if (ruleId == QStringLiteral(".m.rule.call")) { if (ruleId == QStringLiteral(".m.rule.call")) {
@@ -367,7 +366,7 @@ void PushRuleModel::setNotificationRuleActions(const QString &kind, const QStrin
m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions); m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
} }
PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled) PushRuleAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
{ {
bool notify = false; bool notify = false;
bool isNoisy = false; bool isNoisy = false;
@@ -392,47 +391,47 @@ PushNotificationAction::Action PushRuleModel::variantToAction(const QList<QVaria
} }
if (!enabled) { if (!enabled) {
return PushNotificationAction::Off; return PushRuleAction::Off;
} }
if (notify) { if (notify) {
if (isNoisy && highlightEnabled) { if (isNoisy && highlightEnabled) {
return PushNotificationAction::NoisyHighlight; return PushRuleAction::NoisyHighlight;
} else if (isNoisy) { } else if (isNoisy) {
return PushNotificationAction::Noisy; return PushRuleAction::Noisy;
} else if (highlightEnabled) { } else if (highlightEnabled) {
return PushNotificationAction::Highlight; return PushRuleAction::Highlight;
} else { } else {
return PushNotificationAction::On; return PushRuleAction::On;
} }
} else { } else {
return PushNotificationAction::Off; return PushRuleAction::Off;
} }
} }
QList<QVariant> PushRuleModel::actionToVariant(PushNotificationAction::Action action, const QString &sound) QList<QVariant> PushRuleModel::actionToVariant(PushRuleAction::Action action, const QString &sound)
{ {
// The caller should never try to set the state to unknown. // The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server. // It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) { if (action == PushRuleAction::Unknown) {
Q_ASSERT(false); Q_ASSERT(false);
return QList<QVariant>(); return QList<QVariant>();
} }
QList<QVariant> actions; QList<QVariant> actions;
if (action != PushNotificationAction::Off) { if (action != PushRuleAction::Off) {
actions.append(QStringLiteral("notify")); actions.append(QStringLiteral("notify"));
} else { } else {
actions.append(QStringLiteral("dont_notify")); actions.append(QStringLiteral("dont_notify"));
} }
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) { if (action == PushRuleAction::Noisy || action == PushRuleAction::NoisyHighlight) {
QJsonObject soundTweak; QJsonObject soundTweak;
soundTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("sound")); soundTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("sound"));
soundTweak.insert(QStringLiteral("value"), sound); soundTweak.insert(QStringLiteral("value"), sound);
actions.append(soundTweak); actions.append(soundTweak);
} }
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) { if (action == PushRuleAction::Highlight || action == PushRuleAction::NoisyHighlight) {
QJsonObject highlightTweak; QJsonObject highlightTweak;
highlightTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("highlight")); highlightTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("highlight"));
actions.append(highlightTweak); actions.append(highlightTweak);

View File

@@ -8,124 +8,8 @@
#include <Quotient/csapi/definitions/push_rule.h> #include <Quotient/csapi/definitions/push_rule.h>
#include "notificationsmanager.h" #include "enums/pushrule.h"
#include "neochatconnection.h"
/**
* @class PushNotificationKind
*
* A class with the Kind enum for push notifications and helper functions.
*
* The kind relates to the kinds of push rule definied in the matrix spec, see
* https://spec.matrix.org/v1.7/client-server-api/#push-rules for full details.
*/
class PushNotificationKind : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the different kinds of push rule.
*/
enum Kind {
Override = 0, /**< The highest priority rules. */
Content, /**< These configure behaviour for messages that match certain patterns. */
Room, /**< These rules change the behaviour of all messages for a given room. */
Sender, /**< These rules configure notification behaviour for messages from a specific Matrix user ID. */
Underride, /**< These are identical to override rules, but have a lower priority than content, room and sender rules. */
};
Q_ENUM(Kind)
/**
* @brief Translate the Kind enum value to a human readable string.
*
* @sa Kind
*/
static QString kindString(Kind kind)
{
switch (kind) {
case Kind::Override:
return QLatin1String("override");
case Kind::Content:
return QLatin1String("content");
case Kind::Room:
return QLatin1String("room");
case Kind::Sender:
return QLatin1String("sender");
case Kind::Underride:
return QLatin1String("underride");
default:
return {};
}
};
};
/**
* @class PushNotificationSection
*
* A class with the Section enum for push notifications and helper functions.
*
* @note This is different from the PushNotificationKind and instead is used for sorting
* in the settings page which is not necessarily by Kind.
*
* @sa PushNotificationKind
*/
class PushNotificationSection : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Defines the sections to sort push rules into.
*/
enum Section {
Master = 0, /**< The master push rule */
Room, /**< Push rules relating to all rooms. */
Mentions, /**< Push rules relating to user mentions. */
Keywords, /**< Global Keyword push rules. */
RoomKeywords, /**< Keyword push rules that only apply to a specific room. */
Invites, /**< Push rules relating to invites. */
Unknown, /**< New default push rules that have not been added to the model yet. */
/**
* @brief Push rules that should never be shown.
*
* There are numerous rules that get set that shouldn't be shown in the general
* list e.g. The array of rules used to override global settings in individual
* rooms.
*
* This is specifically different to unknown which are just new default push
* rule that haven't been added to the model yet.
*/
Undefined,
};
Q_ENUM(Section)
/**
* @brief Translate the Section enum value to a human readable string.
*
* @sa Section
*/
static QString sectionString(Section section)
{
switch (section) {
case Section::Master:
return QLatin1String("Master");
case Section::Room:
return QLatin1String("Room Notifications");
case Section::Mentions:
return QLatin1String("@Mentions");
case Section::Keywords:
return QLatin1String("Keywords");
case Section::Invites:
return QLatin1String("Invites");
default:
return {};
}
};
};
/** /**
* @class PushRuleModel * @class PushRuleModel
@@ -140,7 +24,7 @@ class PushRuleModel : public QAbstractListModel
/** /**
* @brief The default state for any newly created keyword rule. * @brief The default state for any newly created keyword rule.
*/ */
Q_PROPERTY(PushNotificationAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged) Q_PROPERTY(PushRuleAction::Action defaultState READ defaultState WRITE setDefaultState NOTIFY defaultStateChanged)
/** /**
* @brief The global notification state. * @brief The global notification state.
@@ -153,7 +37,7 @@ class PushRuleModel : public QAbstractListModel
/** /**
* @brief Whether the global notification state has been retrieved from the server. * @brief Whether the global notification state has been retrieved from the server.
* *
* @sa globalNotificationsEnabled, PushNotificationAction::Action * @sa globalNotificationsEnabled, PushRuleAction::Action
*/ */
Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged) Q_PROPERTY(bool globalNotificationsSet READ globalNotificationsSet NOTIFY globalNotificationsSetChanged)
@@ -162,9 +46,9 @@ class PushRuleModel : public QAbstractListModel
public: public:
struct Rule { struct Rule {
QString id; QString id;
PushNotificationKind::Kind kind; PushRuleKind::Kind kind;
PushNotificationAction::Action action; PushRuleAction::Action action;
PushNotificationSection::Section section; PushRuleSection::Section section;
bool enabled; bool enabled;
QString roomId; QString roomId;
}; };
@@ -176,7 +60,7 @@ public:
NameRole = Qt::DisplayRole, /**< The push rule name. */ NameRole = Qt::DisplayRole, /**< The push rule name. */
IdRole, /**< The push rule ID. */ IdRole, /**< The push rule ID. */
KindRole, /**< The kind of notification rule; override, content, etc. */ KindRole, /**< The kind of notification rule; override, content, etc. */
ActionRole, /**< The PushNotificationAction for the rule. */ ActionRole, /**< The PushRuleAction for the rule. */
HighlightableRole, /**< Whether the rule can have a highlight action. */ HighlightableRole, /**< Whether the rule can have a highlight action. */
DeletableRole, /**< Whether the rule can be deleted the rule. */ DeletableRole, /**< Whether the rule can be deleted the rule. */
SectionRole, /**< The section to sort into in the settings page. */ SectionRole, /**< The section to sort into in the settings page. */
@@ -186,8 +70,8 @@ public:
explicit PushRuleModel(QObject *parent = nullptr); explicit PushRuleModel(QObject *parent = nullptr);
[[nodiscard]] PushNotificationAction::Action defaultState() const; [[nodiscard]] PushRuleAction::Action defaultState() const;
void setDefaultState(PushNotificationAction::Action defaultState); void setDefaultState(PushRuleAction::Action defaultState);
[[nodiscard]] bool globalNotificationsEnabled() const; [[nodiscard]] bool globalNotificationsEnabled() const;
void setGlobalNotificationsEnabled(bool enabled); void setGlobalNotificationsEnabled(bool enabled);
@@ -215,7 +99,7 @@ public:
*/ */
[[nodiscard]] QHash<int, QByteArray> roleNames() const override; [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void setPushRuleAction(const QString &id, PushNotificationAction::Action action); Q_INVOKABLE void setPushRuleAction(const QString &id, PushRuleAction::Action action);
/** /**
* @brief Add a new keyword to the model. * @brief Add a new keyword to the model.
@@ -240,18 +124,18 @@ private Q_SLOTS:
void updateNotificationRules(const QString &type); void updateNotificationRules(const QString &type);
private: private:
PushNotificationAction::Action m_defaultKeywordAction; PushRuleAction::Action m_defaultKeywordAction;
QList<Rule> m_rules; QList<Rule> m_rules;
NeoChatConnection *m_connection; NeoChatConnection *m_connection;
void setRules(QList<Quotient::PushRule> rules, PushNotificationKind::Kind kind); void setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind);
int getRuleIndex(const QString &ruleId) const; int getRuleIndex(const QString &ruleId) const;
PushNotificationSection::Section getSection(Quotient::PushRule rule); PushRuleSection::Section getSection(Quotient::PushRule rule);
void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled); void setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled);
void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action); void setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action);
PushNotificationAction::Action variantToAction(const QList<QVariant> &actions, bool enabled); PushRuleAction::Action variantToAction(const QList<QVariant> &actions, bool enabled);
QList<QVariant> actionToVariant(PushNotificationAction::Action action, const QString &sound = QStringLiteral("default")); QList<QVariant> actionToVariant(PushRuleAction::Action action, const QString &sound = QStringLiteral("default"));
}; };
Q_DECLARE_METATYPE(PushRuleModel *) Q_DECLARE_METATYPE(PushRuleModel *)

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "reactionmodel.h" #include "reactionmodel.h"
#include "neochatroom.h"
#include <QDebug> #include <QDebug>
#ifdef HAVE_ICU #ifdef HAVE_ICU
@@ -15,11 +16,20 @@
#include <Quotient/user.h> #include <Quotient/user.h>
ReactionModel::ReactionModel(QObject *parent, QList<Reaction> reactions, Quotient::User *localUser) ReactionModel::ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room)
: QAbstractListModel(parent) : QAbstractListModel(nullptr)
, m_localUser(localUser) , m_room(room)
, m_event(event)
{ {
setReactions(reactions); if (m_event != nullptr && m_room != nullptr) {
connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) {
if (m_event->id() == eventId) {
updateReactions();
}
});
updateReactions();
}
} }
QVariant ReactionModel::data(const QModelIndex &index, int role) const QVariant ReactionModel::data(const QModelIndex &index, int role) const
@@ -35,38 +45,11 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
const auto &reaction = m_reactions.at(index.row()); const auto &reaction = m_reactions.at(index.row());
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
const auto reactionText = isEmoji(reaction.reaction)
? QStringLiteral("<span style=\"font-family: 'emoji';\">") + reaction.reaction + QStringLiteral("</span>")
: reaction.reaction;
if (role == TextContentRole) { if (role == TextContentRole) {
if (reaction.authors.count() > 1) { if (reaction.authors.count() > 1) {
return QStringLiteral("%1 %2").arg(reactionText, QString::number(reaction.authors.count())); return QStringLiteral("%1 %2").arg(reactionText(reaction.reaction), QString::number(reaction.authors.count()));
} else { } else {
return reactionText; return reactionText(reaction.reaction);
} }
} }
@@ -97,7 +80,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
"%2 reacted with %3", "%2 reacted with %3",
reaction.authors.count(), reaction.authors.count(),
text, text,
reactionText); reactionText(reaction.reaction));
return text; return text;
} }
@@ -107,7 +90,7 @@ QVariant ReactionModel::data(const QModelIndex &index, int role) const
if (role == HasLocalUser) { if (role == HasLocalUser) {
for (auto author : reaction.authors) { for (auto author : reaction.authors) {
if (author.toMap()[QStringLiteral("id")] == m_localUser->id()) { if (author.toMap()[QStringLiteral("id")] == m_room->localUser()->id()) {
return true; return true;
} }
} }
@@ -123,11 +106,44 @@ int ReactionModel::rowCount(const QModelIndex &parent) const
return m_reactions.count(); return m_reactions.count();
} }
void ReactionModel::setReactions(QList<Reaction> reactions) void ReactionModel::updateReactions()
{ {
beginResetModel(); beginResetModel();
m_reactions.clear(); m_reactions.clear();
m_reactions = reactions;
const auto &annotations = m_room->relatedEvents(*m_event, Quotient::EventRelation::AnnotationType);
if (annotations.isEmpty()) {
endResetModel();
return;
};
QMap<QString, QList<Quotient::User *>> reactions = {};
for (const auto &a : annotations) {
if (a->isRedacted()) { // Just in case?
continue;
}
if (const auto &e = eventCast<const Quotient::ReactionEvent>(a)) {
reactions[e->key()].append(m_room->user(e->senderId()));
}
}
if (reactions.isEmpty()) {
endResetModel();
return;
}
auto i = reactions.constBegin();
while (i != reactions.constEnd()) {
QVariantList authors;
for (const auto &author : i.value()) {
authors.append(m_room->getUser(author));
}
m_reactions.append(ReactionModel::Reaction{i.key(), authors});
++i;
}
endResetModel(); endResetModel();
} }
@@ -142,4 +158,32 @@ QHash<int, QByteArray> ReactionModel::roleNames() const
}; };
} }
QString ReactionModel::reactionText(const QString &text)
{
const auto isEmoji = [](const QString &text) {
#ifdef HAVE_ICU
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
int from = 0;
while (finder.toNextBoundary() != -1) {
auto to = finder.position();
if (text[from].isSpace()) {
from = to;
continue;
}
auto first = text.mid(from, to - from).toUcs4()[0];
if (!u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION)) {
return false;
}
from = to;
}
return true;
#else
return false;
#endif
};
return isEmoji(text) ? QStringLiteral("<span style=\"font-family: 'emoji';\">") + text + QStringLiteral("</span>") : text;
}
#include "moc_reactionmodel.cpp" #include "moc_reactionmodel.cpp"

View File

@@ -3,8 +3,10 @@
#pragma once #pragma once
#include "neochatroom.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlEngine> #include <QQmlEngine>
#include <Quotient/events/reactionevent.h>
namespace Quotient namespace Quotient
{ {
@@ -20,6 +22,7 @@ class ReactionModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("")
public: public:
/** /**
@@ -41,7 +44,7 @@ public:
HasLocalUser, /**< Whether the local user is in the list of authors. */ HasLocalUser, /**< Whether the local user is in the list of authors. */
}; };
explicit ReactionModel(QObject *parent = nullptr, QList<Reaction> reactions = {}, Quotient::User *localUser = nullptr); explicit ReactionModel(const Quotient::RoomMessageEvent *event, const NeoChatRoom *room);
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
@@ -64,14 +67,12 @@ public:
*/ */
[[nodiscard]] QHash<int, QByteArray> roleNames() const override; [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Set the reactions data in the model.
*/
void setReactions(QList<Reaction> reactions);
private: private:
const NeoChatRoom *m_room;
const Quotient::RoomMessageEvent *m_event;
QList<Reaction> m_reactions; QList<Reaction> m_reactions;
Quotient::User *m_localUser; void updateReactions();
static QString reactionText(const QString &text);
}; };
Q_DECLARE_METATYPE(ReactionModel *) Q_DECLARE_METATYPE(ReactionModel *)

View File

@@ -3,15 +3,12 @@
#include "roomlistmodel.h" #include "roomlistmodel.h"
#include "controller.h"
#include "eventhandler.h" #include "eventhandler.h"
#include "neochatconfig.h" #include "neochatconfig.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "roommanager.h" #include "roommanager.h"
#include "spacehierarchycache.h" #include "spacehierarchycache.h"
#include <Quotient/user.h>
#include <QDebug> #include <QDebug>
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
@@ -23,7 +20,6 @@
#include <KLocalizedString> #include <KLocalizedString>
#include <QGuiApplication> #include <QGuiApplication>
#include <utility>
using namespace Quotient; using namespace Quotient;
@@ -349,7 +345,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true); return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true);
} }
if (role == SubtitleTextRole) { if (role == SubtitleTextRole) {
if (room->lastEventIsSpoiler()) { if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) {
return QString(); return QString();
} }
EventHandler eventHandler; EventHandler eventHandler;
@@ -372,6 +368,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == ReplacementIdRole) { if (role == ReplacementIdRole) {
return room->successorId(); return room->successorId();
} }
if (role == IsDirectChat) {
return room->isDirectChat();
}
return QVariant(); return QVariant();
} }
@@ -405,6 +404,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[IsSpaceRole] = "isSpace"; roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId"; roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace"; roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
return roles; return roles;
} }
@@ -416,7 +416,7 @@ QString RoomListModel::categoryName(int category)
case NeoChatRoomType::Favorite: case NeoChatRoomType::Favorite:
return i18n("Favorite"); return i18n("Favorite");
case NeoChatRoomType::Direct: case NeoChatRoomType::Direct:
return i18n("Direct Messages"); return i18n("Friends");
case NeoChatRoomType::Normal: case NeoChatRoomType::Normal:
return i18n("Normal"); return i18n("Normal");
case NeoChatRoomType::Deprioritized: case NeoChatRoomType::Deprioritized:

View File

@@ -3,8 +3,6 @@
#pragma once #pragma once
#include <Quotient/events/roomevent.h>
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlEngine> #include <QQmlEngine>
@@ -79,6 +77,7 @@ public:
IsSpaceRole, /**< Whether the room is a space. */ IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */ IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */ ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
}; };
Q_ENUM(EventRoles) Q_ENUM(EventRoles)

View File

@@ -36,7 +36,7 @@ void SearchModel::setSearchText(const QString &searchText)
void SearchModel::search() void SearchModel::search()
{ {
Q_ASSERT(m_connection); Q_ASSERT(m_room);
setSearching(true); setSearching(true);
if (m_job) { if (m_job) {
m_job->abandon(); m_job->abandon();
@@ -62,7 +62,7 @@ void SearchModel::search()
}; };
auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria}); auto job = m_room->connection()->callApi<SearchJob>(SearchJob::Categories{criteria});
m_job = job; m_job = job;
connect(job, &BaseJob::finished, this, [this, job] { connect(job, &BaseJob::finished, this, [this, job] {
beginResetModel(); beginResetModel();
@@ -74,17 +74,6 @@ void SearchModel::search()
}); });
} }
Connection *SearchModel::connection() const
{
return m_connection;
}
void SearchModel::setConnection(Connection *connection)
{
m_connection = connection;
Q_EMIT connectionChanged();
}
QVariant SearchModel::data(const QModelIndex &index, int role) const QVariant SearchModel::data(const QModelIndex &index, int role) const
{ {
auto row = index.row(); auto row = index.row();
@@ -136,6 +125,10 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return eventHandler.isHighlighted(); return eventHandler.isHighlighted();
case EventIdRole: case EventIdRole:
return eventHandler.getId(); return eventHandler.getId();
case IsThreadedRole:
return eventHandler.isThreaded();
case ThreadRootRole:
return eventHandler.threadRoot();
} }
return DelegateType::Message; return DelegateType::Message;
} }
@@ -181,6 +174,8 @@ QHash<int, QByteArray> SearchModel::roleNames() const
{MimeTypeRole, "mimeType"}, {MimeTypeRole, "mimeType"},
{ShowLinkPreviewRole, "showLinkPreview"}, {ShowLinkPreviewRole, "showLinkPreview"},
{LinkPreviewRole, "linkPreview"}, {LinkPreviewRole, "linkPreview"},
{IsThreadedRole, "isThreaded"},
{ThreadRootRole, "threadRoot"},
}; };
} }

View File

@@ -31,11 +31,6 @@ class SearchModel : public QAbstractListModel
*/ */
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged) Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
/**
* @brief The current connection that the model is using to search for messages.
*/
Q_PROPERTY(Quotient::Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/** /**
* @brief The current room that the search is being done from. * @brief The current room that the search is being done from.
*/ */
@@ -85,6 +80,8 @@ public:
MimeTypeRole, MimeTypeRole,
ShowLinkPreviewRole, ShowLinkPreviewRole,
LinkPreviewRole, LinkPreviewRole,
IsThreadedRole,
ThreadRootRole,
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
explicit SearchModel(QObject *parent = nullptr); explicit SearchModel(QObject *parent = nullptr);
@@ -92,9 +89,6 @@ public:
QString searchText() const; QString searchText() const;
void setSearchText(const QString &searchText); void setSearchText(const QString &searchText);
Quotient::Connection *connection() const;
void setConnection(Quotient::Connection *connection);
NeoChatRoom *room() const; NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
@@ -128,7 +122,6 @@ public:
Q_SIGNALS: Q_SIGNALS:
void searchTextChanged(); void searchTextChanged();
void connectionChanged();
void roomChanged(); void roomChanged();
void searchingChanged(); void searchingChanged();
@@ -139,7 +132,6 @@ private:
void setSearching(bool searching); void setSearching(bool searching);
QString m_searchText; QString m_searchText;
Quotient::Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr; NeoChatRoom *m_room = nullptr;
Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none; Quotient::Omittable<Quotient::SearchJob::ResultRoomEvents> m_result = Quotient::none;
Quotient::SearchJob *m_job = nullptr; Quotient::SearchJob *m_job = nullptr;

View File

@@ -3,8 +3,6 @@
#include "serverlistmodel.h" #include "serverlistmodel.h"
#include "controller.h"
#include <Quotient/connection.h> #include <Quotient/connection.h>
#include <QDebug> #include <QDebug>
@@ -13,6 +11,8 @@
#include <KConfigGroup> #include <KConfigGroup>
#include <KSharedConfig> #include <KSharedConfig>
#include "neochatconnection.h"
ServerListModel::ServerListModel(QObject *parent) ServerListModel::ServerListModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {

View File

@@ -83,6 +83,21 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
{ {
Q_UNUSED(source_parent); Q_UNUSED(source_parent);
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
return false;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded") if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel()) && dynamic_cast<RoomListModel *>(sourceModel())
->connection() ->connection()
@@ -90,10 +105,6 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
return false; return false;
} }
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) { if (m_activeSpaceId.isEmpty()) {
return acceptRoom; return acceptRoom;
} else { } else {
@@ -116,4 +127,20 @@ void SortFilterRoomListModel::setActiveSpaceId(const QString &spaceId)
invalidate(); invalidate();
} }
SortFilterRoomListModel::Mode SortFilterRoomListModel::mode() const
{
return m_mode;
}
void SortFilterRoomListModel::setMode(SortFilterRoomListModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
}
#include "moc_sortfilterroomlistmodel.cpp" #include "moc_sortfilterroomlistmodel.cpp"

View File

@@ -47,6 +47,11 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
*/ */
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged) Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public: public:
enum RoomSortOrder { enum RoomSortOrder {
Alphabetical, Alphabetical,
@@ -55,6 +60,13 @@ public:
}; };
Q_ENUM(RoomSortOrder) Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomListModel(QObject *parent = nullptr); explicit SortFilterRoomListModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder); void setRoomSortOrder(RoomSortOrder sortOrder);
@@ -66,6 +78,9 @@ public:
QString activeSpaceId() const; QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId); void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected: protected:
/** /**
* @brief Returns true if the value of source_left is less than source_right. * @brief Returns true if the value of source_left is less than source_right.
@@ -85,9 +100,11 @@ Q_SIGNALS:
void roomSortOrderChanged(); void roomSortOrderChanged();
void filterTextChanged(); void filterTextChanged();
void activeSpaceIdChanged(); void activeSpaceIdChanged();
void modeChanged();
private: private:
RoomSortOrder m_sortOrder = Categories; RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText; QString m_filterText;
QString m_activeSpaceId; QString m_activeSpaceId;
}; };

View File

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

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