Compare commits

...

94 Commits

Author SHA1 Message Date
Joshua Goins
a94579e0cf Pin kirigami-addons in the Flatpak build to 1.7.0 2025-02-03 15:48:28 -05:00
James Graham
66343ba11e Fix new MessageModel
Make sure that we initialise the MessageContentModel for nwe and historical events after they have been added to the timeline
2025-02-03 17:16:40 +00:00
l10n daemon script
684cd85a7a GIT_SILENT Sync po/docbooks with svn 2025-02-02 01:38:47 +00:00
Heiko Becker
ef9a80e76f GIT_SILENT Update Appstream for new release
(cherry picked from commit 96b03082e3)
2025-01-31 01:44:04 +01:00
l10n daemon script
fbb5f02379 GIT_SILENT Sync po/docbooks with svn 2025-01-29 01:35:45 +00:00
James Graham
5f4bde96e9 Max Width Threads
Since threads are a conversation where both the local user and others take part always make them span the full available width
2025-01-28 18:13:56 +00:00
l10n daemon script
f8c8a68840 GIT_SILENT Sync po/docbooks with svn 2025-01-28 01:36:42 +00:00
Tobias Fella
bf6f4a951e Mark MessageModel as uncreatable 2025-01-27 16:07:30 +01:00
l10n daemon script
58c9366548 GIT_SILENT Sync po/docbooks with svn 2025-01-27 01:37:35 +00:00
l10n daemon script
f410ecac2b GIT_SILENT Sync po/docbooks with svn 2025-01-26 01:35:04 +00:00
Max Buchholz
1d1a43ade2 Appdata: add display size 2025-01-25 17:57:21 +00:00
James Graham
37adb56233 Thread fetch more button
Changes threads so there is a button to fetch more events. Also adds a separator between the thread root and the rest of the events.
2025-01-25 16:50:29 +00:00
Justin Zobel
aca0669bf6 CI: Add JSON, XML and YML linting 2025-01-25 09:33:05 -05:00
Justin Zobel
b33ab76ff8 YAML formatting 2025-01-25 09:33:04 -05:00
Gary Wang
38a391b7fa CI: add frameworks/kiconthemes to .kde-ci.yaml 2025-01-25 14:14:19 +00:00
Gary Wang
82434fe87c fix: no icon under Windows
See also:

- https://invent.kde.org/frameworks/kiconthemes/-/issues/3
- https://planet.kde.org/christoph-cullmann-2024-05-11-kde-applications-icons/
2025-01-25 14:14:19 +00:00
Tobias Fella
8bf7c36249 Improve verification method choosing 2025-01-25 11:56:10 +01:00
l10n daemon script
cff3557a24 GIT_SILENT Sync po/docbooks with svn 2025-01-25 01:36:39 +00:00
Carl Schwan
2c476c4351 Fix double separator in RoomDrawer 2025-01-24 12:36:50 +00:00
l10n daemon script
82c8ab511d GIT_SILENT Sync po/docbooks with svn 2025-01-24 01:36:28 +00:00
l10n daemon script
486ed6edd2 GIT_SILENT Sync po/docbooks with svn 2025-01-23 01:33:32 +00:00
l10n daemon script
a4b0a9ed36 GIT_SILENT Sync po/docbooks with svn 2025-01-22 01:34:28 +00:00
l10n daemon script
bba9c37ba5 GIT_SILENT Sync po/docbooks with svn 2025-01-21 01:35:12 +00:00
Joshua Goins
febc7d1630 Add UI to set a custom display name for specific rooms
This is the same functionality that /myroomnick does, but it's now
exposed in a much more accessible place in the UI. A new page to the
room settings is added to configure your profile in the room. It's
currently limited to a display name.
2025-01-19 21:33:07 -05:00
l10n daemon script
1ed071949b GIT_SILENT Sync po/docbooks with svn 2025-01-20 01:39:21 +00:00
l10n daemon script
3878c264ef 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"
2025-01-20 01:28:38 +00:00
Joshua Goins
21daf0b664 Fix "Configure Web Shortcuts" not doing anything, again 2025-01-19 19:52:37 +00:00
l10n daemon script
5efaa72cea GIT_SILENT Sync po/docbooks with svn 2025-01-19 01:35:00 +00:00
Mark Penner
191cd7cbba add missing icons on Android 2025-01-18 13:00:31 -06:00
Joshua Goins
c583e31b16 Add how many rooms you have in common to the user detail dialog
Eventually this will be expanded into an actual list you can look
through, but this can at least give you an idea the
number of rooms this user shares with you. If the user doesn't share any
rooms with you (e.g. they left) then the label is hidden.
2025-01-18 17:40:26 +00:00
Joshua Goins
590fba7deb Always open the user details dialog in the focused window
This fixes some odd UX where you tap on someone's user in a search or
pinned messages window, but it opens in the NeoChat main window instead.

Fixes #681
2025-01-18 14:41:34 +00:00
Joshua Goins
a8f22003cb Adapt to new libQuotient API changes
postPlainText is gone
2025-01-18 13:28:39 +00:00
l10n daemon script
54596e3fe6 GIT_SILENT Sync po/docbooks with svn 2025-01-18 12:28:26 +00:00
l10n daemon script
2ee4d110a0 GIT_SILENT Sync po/docbooks with svn 2025-01-18 01:41:35 +00:00
James Graham
7a949dccbb Refactor threads
The focus here is to make threads use the standard message content system rather than having a special implementation.

To achieve this the threadroot content model will now get a thread body component which will visualise the thread model with all the other messages. The latest message in the thread will then just ask for the thread root content model and show that.

Note: in order to stop a cyclical dependency with MessageComponentChooser and new base version has been added which is just missing ThreadBodyComponent and and the main version is now inherited from that with ThreadBodyComponent added.
2025-01-17 19:18:44 +00:00
Tobias Fella
111a45ab38 Use ellipsis character instead of ... 2025-01-17 16:50:56 +01:00
l10n daemon script
2bcf59c225 GIT_SILENT Sync po/docbooks with svn 2025-01-17 01:34:40 +00:00
Joshua Goins
d542033125 Don't spam pending invites every time NeoChat is started
Currently the way we show invite notifications is sub-optimal. We did it
during the initial room state load, which meant it shows an invite
notification *every time* you opened NeoChat. This gets annoying very
quickly if you have any pending invitations you don't want to take
action on just yet.

Instead, let's handle this in NotificationsManager directly, and also
remove some scaffolding now that it isn't plumbed through
NeoChatRoom/NeoChatConnection.
2025-01-16 20:35:42 +00:00
Tobias Fella
44c72828e1 Fixes for password changing 2025-01-16 18:21:35 +01:00
l10n daemon script
99d3ee32fa GIT_SILENT Sync po/docbooks with svn 2025-01-16 01:35:29 +00:00
l10n daemon script
7df0ff309e GIT_SILENT Sync po/docbooks with svn 2025-01-15 01:35:33 +00:00
James Graham
856a751fcb Fix Getting Member Objects
It seems that there are no guarantees that we will have a room member event available when a message has arrived especially early on after room load so we should create member object unconditionally and make it the responsibility of the caller to only ask for real senders.

BUG: 498649
2025-01-14 18:54:34 +00:00
l10n daemon script
da99bcae5d GIT_SILENT Sync po/docbooks with svn 2025-01-14 01:36:26 +00:00
Joshua Goins
1b0c6c2847 Add button to view pinned messages in a room
BUG: 497427
2025-01-13 20:48:35 +00:00
Joshua Goins
c315e817b2 Add an option to disable encryption in new chats
Right now NeoChat (or more technically, libQuotient) decides to use
encryption by default in new chats. Some users may not prefer or need
this, so a new option is added under Security to change this behavior.

BUG: 498375
2025-01-13 20:34:00 +00:00
l10n daemon script
6fde07a20d GIT_SILENT Sync po/docbooks with svn 2025-01-13 01:38:22 +00:00
James Graham
8a86159fd7 Fix getting content models for old events in a search model
Fix getting content models for old events in a search model by allowing for calling using the event. This gets past the intial checks and the content model itself can load the event from the server.

Requires network/neochat!2110 to fix the showauthor issue
2025-01-12 15:14:48 +00:00
Joshua Goins
1d532a1fc1 Move showAuthor role to MessageModel, so it's available for all models
This fixes features like the search model, where no message delegates
could be created because it's missing showAuthor.
2025-01-12 13:48:31 +00:00
Tobias Fella
a67ce75924 Improve ssss result reporting 2025-01-12 14:33:42 +01:00
l10n daemon script
b3d845ea32 GIT_SILENT Sync po/docbooks with svn 2025-01-12 01:40:23 +00:00
Carl Schwan
008e12cb42 Port AccountMenu to ConvergentContextMenu 2025-01-11 17:07:17 +00:00
Carl Schwan
ceaed8be51 Simplify ExploreComponent
Remove mobile mode support as this is handled by ExploreComponentMobile
2025-01-11 17:07:17 +00:00
Carl Schwan
f0e0979366 Port DelegateContextMenu to ConvergentContextMenu 2025-01-11 17:07:17 +00:00
Carl Schwan
c43563a804 Port space menu to ConvergentContextMenu 2025-01-11 17:07:17 +00:00
Carl Schwan
8ec3b2d05d Port to ConvergentContextMenu 2025-01-11 17:07:17 +00:00
Tobias Fella
39a95c727f Remove unused functions for getting crypto keys 2025-01-11 17:47:30 +01:00
James Graham
a2f5a585e3 Use the libQuotient function to get the user power level as it is now equivalent 2025-01-11 16:38:20 +00:00
Tobias Fella
aa95bc62bd Use 6.8 flatpak runtime 2025-01-11 15:47:45 +01:00
James Graham
ae7bfa5bcb Move the storage of thread models to the room 2025-01-11 13:22:51 +00:00
James Graham
37de1ec583 Move the storage of MessageContentModels to the room
Move the storage of MessageContentModels to the room in the same manner as memeber objects to prevent duplication but mainly to make the system easier to maintain going forward with things like threads for example. This requires the creation of a MessageContentFilterModel as the same model may be used in multiple places, sometimes with the author showning sometimes not.
2025-01-11 13:16:14 +00:00
l10n daemon script
bb8f0eae1b GIT_SILENT Sync po/docbooks with svn 2025-01-11 01:33:52 +00:00
l10n daemon script
571d3c14c8 GIT_SILENT Sync po/docbooks with svn 2025-01-10 10:24:58 +00:00
Joshua Goins
d7202ae0a7 Implement request for user data erasure
This adds UI for MSC4025 to the account deactivation dialog, if the
server supports it. We also switch away from our
customDeactivateAccountJob to libQuotient's.

Fixes #670.
2025-01-09 16:37:09 -05:00
Carl Schwan
2a9c75e24f Improve handling of DonwloadAction
Set the progress the download action only when the currentProgressInfo
changed, otherwise we sometimes end up in a data race.
2025-01-09 16:41:21 +00:00
Carl Schwan
7231662f94 Fix right clicking on NeoChatMaximizedComponent
The api of RoomManager.viewEventMenu changed and now require also
passing the author.
2025-01-09 16:41:21 +00:00
Carl Schwan
df83927ed7 Expose ProgressInfoRole also for other type of attachments 2025-01-09 16:41:21 +00:00
Carl Schwan
f14dfc5de8 Set explicitely parent in MaximizeComponent
Currently it uses applicationWindow().overlay which works but is
not ideal for multiple reasons:

- This as a tendency to breaks unexpectedly
- It can't be optimized by the qml compiler

So we are trying to move away from these construct everywhere.
2025-01-09 16:41:21 +00:00
l10n daemon script
188d0c9d5c GIT_SILENT Sync po/docbooks with svn 2025-01-09 01:36:14 +00:00
l10n daemon script
8d68c64fdf GIT_SILENT Sync po/docbooks with svn 2025-01-08 01:36:51 +00:00
Carl Schwan
d796ab350e Use Component.onCompleted
as we don't yet depends on KF 6.10
2025-01-07 16:33:31 +01:00
Carl Schwan
334a1b5bef Revert "Require Kirigami master"
This reverts commit 3304ee0985.
2025-01-07 16:32:31 +01:00
Joshua Goins
3304ee0985 Require Kirigami master
This is needed for emptyHelpfulAction, which is only in the currently unreleased KF 6.10.
This should fix the KDE Android Nightly.
2025-01-07 08:17:44 +00:00
l10n daemon script
d7b3523544 GIT_SILENT Sync po/docbooks with svn 2025-01-07 01:35:44 +00:00
l10n daemon script
1a8d346064 GIT_SILENT Sync po/docbooks with svn 2025-01-06 01:37:20 +00:00
James Graham
42f9b36667 Get rid of the m_memberObjects store in MessageModel
Get rid of the m_memberObjects store in MessageModel which was missed in the last mr. Also clean up the code for NeoChatMembers
2025-01-05 18:52:18 +00:00
James Graham
c21e9f2114 Store NeochatRoomMember objects in the room
Store NeochatRoomMember objects in the room so we don't duplicate them unnecessarily. This also adds a visible property for a room which is set true when shown by MessageEventModel and false when not, triggering the deletion of member objects. This mechanism will be used for other object types in the future.
2025-01-05 11:06:42 +00:00
l10n daemon script
3dbe605de8 GIT_SILENT Sync po/docbooks with svn 2025-01-05 01:35:48 +00:00
James Graham
41a6dd6175 Timeline Memory Test App
Create a simple app that puts a number of events into a simple timeline which can be used to verify future memory optimisations

When Neochat is built with tests on you should find the app at ~/kde/usr/bin/timeline-memtest assuming ~/kde is your kdesrc-build directory
2025-01-04 15:29:40 +00:00
l10n daemon script
09af6fe0a7 GIT_SILENT Sync po/docbooks with svn 2025-01-04 01:34:01 +00:00
Kai Uwe Broulik
3b8c3afa3e Support canceling file transfer from pseudo job
Allows to click cancel on the job popup rather than only from
within NeoChat.
2025-01-03 13:56:33 +01:00
Kai Uwe Broulik
7b7f4d264c Handle transfer job being canceled
Set KilledJobError to indicate it was canceled by the user
to avoid a bogus "finished" notification.

Sadly, fileTransferCanceled has been removed from libQuotient
so this lambda botch checking transfer status needs to be done.
2025-01-03 13:55:56 +01:00
Kai Uwe Broulik
c454a4942e MessageDelegateContextMenu: Fix "Edit" action not showing
There's no such thing as Emote or Message anymore, it's all "Text".
Matches IsEditableRole of MessageEventModel.

While at it, also clear threadId, which is what the Edit quick
button also does.
2025-01-03 10:21:36 +00:00
l10n daemon script
44cd52af6c GIT_SILENT Sync po/docbooks with svn 2025-01-03 01:34:59 +00:00
Heiko Becker
0d01339b02 GIT_SILENT Update Appstream for new release
(cherry picked from commit a5b37a78a0)
2025-01-02 15:06:37 +01:00
Carl Schwan
e5b4ca53f8 Use nightly kirigami addons in craft builds 2025-01-02 15:05:36 +01:00
l10n daemon script
703b03b33c GIT_SILENT Sync po/docbooks with svn 2025-01-02 01:33:44 +00:00
l10n daemon script
73cdad66ac GIT_SILENT Sync po/docbooks with svn 2025-01-01 01:34:36 +00:00
James Graham
4b1afdbe2d Fix Thread Chatbars
Since the new thread API was released in 0.9.2 update the if defs to include it. This will solve the double chat bar in thread for anyone on 0.9.2 or above
2024-12-31 16:13:27 +00:00
James Graham
64bfc0f29a Add more detail to the MessageModel documentation
Because when I was making a new one these are the things I forgot
2024-12-31 14:52:49 +00:00
Carl Schwan
28c4c0b48c Use symbolic icon for purpose plugin 2024-12-31 14:05:05 +00:00
l10n daemon script
40ccde9f06 GIT_SILENT Sync po/docbooks with svn 2024-12-31 01:36:00 +00:00
l10n daemon script
baa7d02ba5 GIT_SILENT Sync po/docbooks with svn 2024-12-30 01:41:01 +00:00
Kai Uwe Broulik
a391df1e67 QuickSwitcher: Suggest to explore rooms when there are no search results
My go to place for doing anything with rooms, is the search button.
However, I cannot join/find new rooms from there. Therefore this adds
an "Expore rooms" button as helpful action when there are no matching
rooms.
2024-12-29 18:56:50 +00:00
Kai Uwe Broulik
1f26485208 ChatBar: Remove explicit Keys.onDeletePressed handler
Key-specific handlers, such as Key.onDeletePressed implicitly accept
the event. This means that the entire logic for the delete key must
be reimplemented, and e.g. Ctrl+Delete to delete the previous *word*
was missed.

Since all it has to do is handle the typing notification and format bar,
just use the already existing Keys.onPressed handler (which does *not*
accept the event) and add a case for Delete alongside Backspace.
2024-12-29 11:02:36 +01:00
134 changed files with 25493 additions and 19326 deletions

View File

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

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.7",
"runtime-version": "6.8",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -27,7 +27,7 @@
"name": "kirigamiaddons",
"config-opts": [ "-DBUILD_TESTING=OFF" ],
"buildsystem": "cmake-ninja",
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "commit": "34d311219e8b7209746a98b3a29b91ded05ff936" } ]
"sources": [ { "type": "git", "url": "https://invent.kde.org/libraries/kirigami-addons.git", "tag": "v1.7.0" } ]
},
{
"name": "kquickimageeditor",

View File

@@ -5,10 +5,13 @@ include:
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/json-validation.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml

View File

@@ -2,42 +2,43 @@
# SPDX-License-Identifier: BSD-2-Clause
Dependencies:
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6'
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
'require':
'frameworks/extra-cmake-modules': '@latest-kf6'
'frameworks/kcoreaddons': '@latest-kf6'
'frameworks/kirigami': '@latest-kf6'
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
'libraries/kirigami-addons': '@latest-kf6'
'third-party/libquotient': '@latest'
'third-party/qtkeychain': '@latest'
'third-party/cmark': '@latest'
'third-party/qcoro': '@latest'
- 'on': ['Windows', 'Linux', 'FreeBSD']
'require':
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'
'frameworks/purpose': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
- 'on': ['Linux']
'require':
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
Options:
per-test-timeout: 90
require-passing-tests-on: [ 'Linux', 'Android', 'FreeBSD', 'Windows' ]
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']

View File

@@ -66,7 +66,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -174,6 +174,9 @@ if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
add_subdirectory(autotests)
add_subdirectory(appiumtests)
if (NOT ANDROID)
add_subdirectory(memorytests)
endif()
endif()
if(KF6DocTools_FOUND)

View File

@@ -82,3 +82,9 @@ path = "src/purpose/purposeplugin.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = "memorytests/memtest-sync.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
SPDX-License-Identifier = "BSD-2-Clause"

View File

@@ -137,7 +137,11 @@ void TimelineMessageModelTest::pendingEvent()
model->setRoom(room);
QCOMPARE(model->rowCount(), 0);
#if Quotient_VERSION_MINOR > 9
auto txnId = room->postText("New plain message"_L1);
#else
auto txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 1);
@@ -145,7 +149,11 @@ void TimelineMessageModelTest::pendingEvent()
QCOMPARE(model->rowCount(), 0);
QCOMPARE(spyRemove.count(), 1);
#if Quotient_VERSION_MINOR > 9
txnId = room->postText("New plain message"_L1);
#else
txnId = room->postPlainText("New plain message"_L1);
#endif
QCOMPARE(model->rowCount(), 1);
QCOMPARE(spyInsert.count(), 2);

View File

@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
# SPDX-License-Identifier: BSD-2-Clause
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline-memtest
main.cpp
)
target_link_libraries(timeline-memtest PRIVATE neochatplugin timelineplugin)
target_link_libraries(timeline-memtest PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
Qt::Gui
Qt::QuickControls2
KF6::Kirigami
QuotientQt6
neochat
)
ecm_add_qml_module(timeline-memtest URI org.kde.neochat.timeline-memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline-memtest
QML_FILES
Main.qml
SOURCES
memtesttimelinemodel.cpp
memtesttimelinemodel.h
DEPENDENCIES
QtCore
QtQuick
IMPORTS
org.kde.neochat
)

33
memorytests/Main.qml Normal file
View File

@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
QQC2.ApplicationWindow {
id: root
title: "Timeline Memory Test"
minimumWidth: Kirigami.Units.gridUnit * 30
minimumHeight: Kirigami.Units.gridUnit * 30
visible: true
QQC2.ScrollView {
width: root.width
height: root.height
contentItem: ListView {
model: messageFilterModel
delegate: EventDelegate {
room: memTestTimelineModel.room
}
}
}
}

30
memorytests/main.cpp Normal file
View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <KLocalizedString>
#include "memtesttimelinemodel.h"
#include "models/messagefiltermodel.h"
using namespace Qt::StringLiterals;
int main(int argc, char **argv)
{
QApplication app(argc, argv);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
QQmlApplicationEngine engine;
engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel;
MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
return app.exec();
}

View File

@@ -0,0 +1,655 @@
{
"ephemeral": {
"events": [
{
"content": {
"$1000000000000:example.org": {
"m.read": {
"@alice:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000000:example.org": {
"m.read": {
"@bob:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@tim:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@example:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@jeff:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@tina:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@sally:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
},
{
"content": {
"$1000000000003:example.org": {
"m.read": {
"@fred:example.org": {
"ts": 1000000000000
}
}
}
},
"type": "m.receipt"
}
]
},
"state": {
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Example",
"membership": "join"
},
"event_id": "$143273582555PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "alice:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "bob:example.org",
"state_key": "@bob:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Tim",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "tim:example.org",
"state_key": "@tim:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Jeff",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "jeff:example.org",
"state_key": "@jeff:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Tina",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "tina:example.org",
"state_key": "@tina:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Sally",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "sally:example.org",
"state_key": "@sally:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Fred",
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "fred:example.org",
"state_key": "@fred:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": "$1000000000000:example.org",
"origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": "$1000000000001:example.org",
"origin_server_ts": 1000000000001,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$1000000000001:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000002,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$1000000000002:example.org",
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$1000000000000:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000003,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$1000000000003:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1000000000004:example.org",
"origin_server_ts": 1000000000004,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": "$1000000000005:example.org",
"origin_server_ts": 1000000000005,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": "$1000000000006:example.org",
"origin_server_ts": 1000000000006,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": "$1000000000007:example.org",
"origin_server_ts": 1000000000007,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": "$1000000000008:example.org",
"origin_server_ts": 1000000000008,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": "$1000000000009:example.org",
"origin_server_ts": 1000000000009,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$1000000000009:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000010,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$1000000000010:example.org",
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$1000000000008:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000011,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$1000000000011:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1000000000012:example.org",
"origin_server_ts": 1000000000012,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": "$1000000000013:example.org",
"origin_server_ts": 1000000000013,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": "$1000000000014:example.org",
"origin_server_ts": 1000000000014,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": "$1000000000015:example.org",
"origin_server_ts": 1000000000015,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": "$1000000000016:example.org",
"origin_server_ts": 1000000000016,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": "$1000000000017:example.org",
"origin_server_ts": 1000000000017,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$1000000000017:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000018,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$1000000000018:example.org",
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$1000000000016:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000019,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$1000000000019:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1000000000020:example.org",
"origin_server_ts": 1000000000020,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": "$1000000000021:example.org",
"origin_server_ts": 1000000000021,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": "$1000000000022:example.org",
"origin_server_ts": 1000000000022,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": "$1000000000023:example.org",
"origin_server_ts": 1000000000023,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
}
],
"limited": true,
"prev_batch": "t34-23535_0_0"
}
}

View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "memtesttimelinemodel.h"
#include <Quotient/events/eventcontent.h>
#include <Quotient/events/roommessageevent.h>
using namespace Quotient;
MemTestTimelineModel::MemTestTimelineModel(QObject *parent)
: MessageModel(parent)
{
beginResetModel();
m_connection = Connection::makeMockConnection(u"@bob:example.org"_s);
m_room = new MemTestRoom(m_connection, u"#memtestroom:example.org"_s, u"memtest-sync.json"_s);
for (const auto &eventIt : m_room->messageEvents()) {
Q_EMIT newEventAdded(eventIt.event());
}
endResetModel();
}
std::optional<std::reference_wrapper<const RoomEvent>> MemTestTimelineModel::getEventForIndex(QModelIndex index) const
{
return *m_room->messageEvents().at(index.row()).event();
}
int MemTestTimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_room->messageEvents().size();
}
#include "moc_memtesttimelinemodel.cpp"

View File

@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <Quotient/events/roomevent.h>
#include <Quotient/syncdata.h>
#include "models/messagemodel.h"
namespace Quotient
{
class Connection;
}
class NeoChatRoom;
class MemTestRoom : public NeoChatRoom
{
public:
MemTestRoom(Quotient::Connection *connection, const QString &roomName, const QString &syncFileName = {})
: NeoChatRoom(connection, roomName, Quotient::JoinState::Join)
{
syncNewEvents(syncFileName);
}
void update(Quotient::SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
void syncNewEvents(const QString &syncFileName)
{
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson);
update(std::move(roomData));
}
}
};
/**
* @class MemTestTimelineModel
*
* This is a special version of the MessageModel design to load an unchanging set
* of events from a json file so that timeline memory optimisations can be measured.
*/
class MemTestTimelineModel : public MessageModel
{
Q_OBJECT
QML_ELEMENT
public:
explicit MemTestTimelineModel(QObject *parent = nullptr);
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
private:
QPointer<Quotient::Connection> m_connection;
std::vector<Quotient::RoomEventPtr> m_events;
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
};

View File

@@ -473,6 +473,8 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="24.12.2" date="2025-02-06"/>
<release version="24.12.1" date="2025-01-09"/>
<release version="24.12.0" date="2024-12-12"/>
<release version="24.08.3" date="2024-11-07"/>
<release version="24.08.2" date="2024-10-10"/>
@@ -648,6 +650,9 @@
<url>https://carlschwan.eu/2020/12/23/announcing-neochat-1.0-the-kde-matrix-client/</url>
</release>
</releases>
<requires>
<display_length compare="ge">360</display_length>
</requires>
<branding>
<color type="primary" scheme_preference="light">#a6e4f3</color>
<color type="primary" scheme_preference="dark">#235670</color>

View File

@@ -119,6 +119,7 @@ Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவு
Comment[tr]=Matrix üzerinde sohbet edin
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment[zh_CN]=在 Matrix 上聊天
Comment[zh_TW]=在 Matrix 上聊天
MimeType=x-scheme-handler/matrix;
Exec=neochat %u

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

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

@@ -28,8 +28,8 @@ apps:
compression: lzo
package-repositories:
- type: apt
ppa: ubuntu-toolchain-r/test
- type: apt
ppa: ubuntu-toolchain-r/test
slots:
session-dbus-interface:
@@ -66,7 +66,7 @@ parts:
- -Dcrypto=disabled
- -Dgtk_doc=false
build-packages:
- meson
- meson
- libglib2.0-dev
- libgcrypt20-dev
prime:
@@ -81,7 +81,8 @@ parts:
plugin: cmake
build-environment:
- PATH: /snap/bin:${PATH}
- PKG_CONFIG_PATH: $CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:$PKG_CONFIG_PATH
- PKG_CONFIG_PATH: "$CRAFT_STAGE/usr/lib/$CRAFT_ARCH_TRIPLET\
/pkgconfig:$PKG_CONFIG_PATH"
cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release
@@ -115,7 +116,9 @@ parts:
- -DQuotient_ENABLE_E2EE=ON
- -DBUILD_WITH_QT6=ON
override-build: |
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13
"update-alternatives --install /usr/bin/gcc gcc\
/usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ \
/usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13"
craftctl default
prime:
- -usr/include
@@ -172,4 +175,3 @@ parts:
- libcmark0.30.2
prime:
- usr/lib/*/libcmark.so*

View File

@@ -126,8 +126,6 @@ add_library(neochat STATIC
registration.cpp
neochatconnection.cpp
neochatconnection.h
jobs/neochatdeactivateaccountjob.cpp
jobs/neochatdeactivateaccountjob.h
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatgetcommonroomsjob.h
mediasizehelper.cpp
@@ -194,6 +192,12 @@ add_library(neochat STATIC
models/roomsortparametermodel.h
models/messagemodel.cpp
models/messagemodel.h
models/messagecontentfiltermodel.cpp
models/messagecontentfiltermodel.h
models/pinnedmessagemodel.cpp
models/pinnedmessagemodel.h
models/commonroomsmodel.cpp
models/commonroomsmodel.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -213,7 +217,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/AccountMenu.qml
qml/ExploreComponent.qml
qml/ExploreComponentMobile.qml
qml/ContextMenu.qml
qml/RoomContextMenu.qml
qml/CollapsedRoomDelegate.qml
qml/RoomDelegate.qml
qml/RoomListPage.qml
@@ -249,6 +253,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/MessageSourceSheet.qml
qml/ConfirmEncryptionDialog.qml
qml/RoomSearchPage.qml
qml/RoomPinnedMessagesPage.qml
qml/LocationChooser.qml
qml/TimelineView.qml
qml/InvitationView.qml
@@ -418,6 +423,7 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::IconThemes
KF6::ColorScheme
KF6::ItemModels
QuotientQt6
@@ -491,6 +497,7 @@ if(ANDROID)
"network-connect"
"list-remove-user"
"org.kde.neochat"
"org.kde.neochat.tray"
"preferences-system-users"
"preferences-desktop-theme-global"
"notifications"
@@ -528,12 +535,16 @@ if(ANDROID)
"object-rotate-left"
"object-rotate-right"
"add-subtitle"
"security-high"
"security-low"
"security-low-symbolic"
"kde"
"list-remove-symbolic"
"edit-delete"
"user-home-symbolic"
"pin-symbolic"
"kt-restore-defaults-symbolic"
"user-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()

View File

@@ -237,20 +237,6 @@ QQC2.Control {
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd);
} else {
remove(cursorPosition, cursorPosition + 1);
}
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false);
repeatTimer.stop();
}
if (quickFormatBar.visible) {
quickFormatBar.close();
}
}
Keys.onEnterPressed: event => {
const controlIsPressed = event.modifiers & Qt.ControlModifier;
if (completionMenu.visible) {
@@ -289,7 +275,7 @@ QQC2.Control {
completionMenu.decrementIndex();
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex();
} else if (event.key === Qt.Key_Backspace) {
} else if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) {
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false);
repeatTimer.stop();

View File

@@ -168,7 +168,6 @@ void Controller::addConnection(NeoChatConnection *c)
connect(c, &NeoChatConnection::syncDone, this, [this, c]() {
m_notificationsManager.handleNotifications(c);
});
connect(c, &NeoChatConnection::showInviteNotification, &m_notificationsManager, &NotificationsManager::postInviteNotification);
c->sync();

View File

@@ -53,9 +53,13 @@ public:
LinkPreview, /**< A preview of a URL in the message. */
LinkPreviewLoad, /**< A loading dialog for a link preview. */
ChatBar, /**< A text edit for editing a message. */
ThreadRoot, /**< The root message of the thread. */
ThreadBody, /**< The other messages in the thread. */
ReplyButton, /**< A button to reply in the current thread. */
FetchButton, /**< A button to fetch more messages in the current thread. */
Verification, /**< A user verification session start message. */
Loading, /**< The component is loading. */
Separator, /**< A horizontal separator. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -12,6 +12,7 @@ FileTransferPseudoJob::FileTransferPseudoJob(Operation operation, const QString
, m_eventId(eventId)
, m_operation(operation)
{
setCapabilities(KJob::Killable);
}
void FileTransferPseudoJob::fileTransferProgress(const QString &id, qint64 progress, qint64 total)
@@ -41,6 +42,15 @@ void FileTransferPseudoJob::fileTransferFailed(const QString &id, const QString
emitResult();
}
void FileTransferPseudoJob::fileTransferCanceled(const QString &id)
{
if (id != m_eventId) {
return;
}
setError(KJob::KilledJobError);
emitResult();
}
void FileTransferPseudoJob::start()
{
setTotalAmount(Unit::Files, 1);
@@ -49,3 +59,9 @@ void FileTransferPseudoJob::start()
{i18nc("The URL being downloaded/uploaded", "Source"), m_path},
{i18nc("The location being downloaded to", "Destination"), m_path});
}
bool FileTransferPseudoJob::doKill()
{
Q_EMIT cancelRequested(m_eventId);
return true;
}

View File

@@ -15,6 +15,7 @@
*/
class FileTransferPseudoJob : public KJob
{
Q_OBJECT
public:
enum Operation {
Download,
@@ -38,11 +39,22 @@ public:
*/
void fileTransferFailed(const QString &id, const QString &errorMessage = {});
/**
* @brief Set the file transfer as canceled.
*/
void fileTransferCanceled(const QString &id);
/**
* @brief Start the file transfer.
*/
void start() override;
protected:
bool doKill() override;
Q_SIGNALS:
void cancelRequested(const QString &id);
private:
QString m_path;
QString m_eventId;

View File

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

View File

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

View File

@@ -37,6 +37,7 @@
#include <KCrash>
#endif
#include <KIconTheme>
#include <KLocalizedContext>
#include <KLocalizedString>
@@ -101,6 +102,7 @@ Q_DECL_EXPORT
#endif
int main(int argc, char *argv[])
{
KIconTheme::initTheme();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW

View File

@@ -136,7 +136,11 @@ QList<ActionsModel::Action> actions{
Action{
u"plain"_s,
[](const QString &text, NeoChatRoom *room, ChatBarCache *) {
#if Quotient_VERSION_MINOR > 9
room->postText(text.toHtmlEscaped());
#else
room->postPlainText(text.toHtmlEscaped());
#endif
return QString();
},
std::nullopt,

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "commonroomsmodel.h"
#include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication>
using namespace Quotient;
CommonRoomsModel::CommonRoomsModel(QObject *parent)
: QAbstractListModel(parent)
{
}
NeoChatConnection *CommonRoomsModel::connection() const
{
return m_connection;
}
void CommonRoomsModel::setConnection(NeoChatConnection *connection)
{
m_connection = connection;
Q_EMIT connectionChanged();
reload();
}
QString CommonRoomsModel::userId() const
{
return m_userId;
}
void CommonRoomsModel::setUserId(const QString &userId)
{
m_userId = userId;
Q_EMIT userIdChanged();
reload();
}
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{
Q_UNUSED(index)
Q_UNUSED(roleName)
return {};
}
int CommonRoomsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_commonRooms.size();
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {
return;
}
if (!m_connection->canCheckMutualRooms()) {
return;
}
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
});
}
#include "moc_commonroomsmodel.cpp"

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include "neochatconnection.h"
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h>
/**
* @brief Model to show the common or mutual rooms between you and another user.
*/
class CommonRoomsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
};
Q_ENUM(Roles)
explicit CommonRoomsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
[[nodiscard]] QString userId() const;
void setUserId(const QString &userId);
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();
void countChanged();
private:
void reload();
QPointer<NeoChatConnection> m_connection;
QString m_userId;
QList<QString> m_commonRooms;
};

View File

@@ -39,14 +39,6 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date();
return day != previousEventDay;
}
// Catch and force the author to be shown for all rows
if (role == TimelineMessageModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
if (model != nullptr) {
model->setShowAuthor(true);
}
return QVariant::fromValue<MessageContentModel *>(model);
}
QVariantMap mediaInfo = mapToSource(index).data(TimelineMessageModel::MediaInfoRole).toMap();

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 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 "messagecontentfiltermodel.h"
#include "enums/messagecomponenttype.h"
#include "models/messagecontentmodel.h"
MessageContentFilterModel::MessageContentFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
}
bool MessageContentFilterModel::showAuthor() const
{
return m_showAuthor;
}
void MessageContentFilterModel::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
Q_EMIT showAuthorChanged();
}
bool MessageContentFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (m_showAuthor) {
return true;
}
const auto index = sourceModel()->index(source_row, 0, source_parent);
auto contentType = static_cast<MessageComponentType::Type>(index.data(MessageContentModel::ComponentTypeRole).toInt());
return contentType != MessageComponentType::Author;
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 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 <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class MessageContentFilterModel
*
* This model filters a message's contents.
*/
class MessageContentFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether the author component should be shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public:
explicit MessageContentFilterModel(QObject *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
Q_SIGNALS:
void showAuthorChanged();
protected:
/**
* @brief Whether a row should be shown out or not.
*
* @sa QSortFilterProxyModel::filterAcceptsRow
*/
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
bool m_showAuthor = true;
};

View File

@@ -12,7 +12,7 @@
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#include <Quotient/qt_connection_util.h>
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
#include <Quotient/thread.h>
#endif
@@ -135,14 +135,14 @@ void MessageContentModel::initializeModel()
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
if (senderId().isEmpty() || senderId() == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
if (senderId().isEmpty() || senderId() == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
@@ -178,16 +178,6 @@ void MessageContentModel::initializeEvent()
} else {
m_currentState = Available;
}
if (m_eventSenderObject == nullptr) {
auto senderId = eventResult.first->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
}
@@ -218,30 +208,29 @@ void MessageContentModel::getEvent()
m_room->downloadEventFromServer(m_eventId);
}
bool MessageContentModel::showAuthor() const
QString MessageContentModel::senderId() const
{
return m_showAuthor;
const auto eventResult = m_room->getEvent(m_eventId);
if (eventResult.first == nullptr) {
return {};
}
auto senderId = eventResult.first->senderId();
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
return senderId;
}
void MessageContentModel::setShowAuthor(bool showAuthor)
NeochatRoomMember *MessageContentModel::senderObject() const
{
if (showAuthor == m_showAuthor) {
return;
const auto eventResult = m_room->getEvent(m_eventId);
if (eventResult.first == nullptr) {
return nullptr;
}
m_showAuthor = showAuthor;
if (m_room->connection()->isIgnored(m_eventSenderId)) {
if (showAuthor) {
beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
endInsertRows();
} else {
beginRemoveRows({}, 0, 0);
m_components.remove(0, 1);
endRemoveRows();
}
if (eventResult.first->senderId().isEmpty()) {
return m_room->qmlSafeMember(m_room->localMember().id());
}
Q_EMIT showAuthorChanged();
return m_room->qmlSafeMember(eventResult.first->senderId());
}
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
@@ -275,7 +264,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
}
if (role == DisplayRole) {
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(m_eventSenderId)) {
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(senderId())) {
Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
@@ -317,7 +306,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending);
}
if (role == AuthorRole) {
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
return QVariant::fromValue<NeochatRoomMember *>(senderObject());
}
if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_room, event.first);
@@ -351,6 +340,17 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
}
if (role == ThreadRootRole) {
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first);
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
#else
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
#endif
return roomMessageEvent->threadRootEventId();
}
return {};
}
if (role == LinkPreviewerRole) {
if (component.type == MessageComponentType::LinkPreview) {
return QVariant::fromValue<LinkPreviewer *>(
@@ -377,27 +377,32 @@ int MessageContentModel::rowCount(const QModelIndex &parent) const
QHash<int, QByteArray> MessageContentModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[DisplayRole] = "display";
roles[ComponentTypeRole] = "componentType";
roles[ComponentAttributesRole] = "componentAttributes";
roles[EventIdRole] = "eventId";
roles[TimeRole] = "time";
roles[TimeStringRole] = "timeString";
roles[AuthorRole] = "author";
roles[MediaInfoRole] = "mediaInfo";
roles[FileTransferInfoRole] = "fileTransferInfo";
roles[ItineraryModelRole] = "itineraryModel";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[AssetRole] = "asset";
roles[PollHandlerRole] = "pollHandler";
roles[ReplyEventIdRole] = "replyEventId";
roles[ReplyAuthorRole] = "replyAuthor";
roles[ReplyContentModelRole] = "replyContentModel";
roles[ThreadRootRole] = "threadRoot";
roles[LinkPreviewerRole] = "linkPreviewer";
roles[ChatBarCacheRole] = "chatBarCache";
return roleNamesStatic();
}
QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
{
QHash<int, QByteArray> roles;
roles[MessageContentModel::DisplayRole] = "display";
roles[MessageContentModel::ComponentTypeRole] = "componentType";
roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes";
roles[MessageContentModel::EventIdRole] = "eventId";
roles[MessageContentModel::TimeRole] = "time";
roles[MessageContentModel::TimeStringRole] = "timeString";
roles[MessageContentModel::AuthorRole] = "author";
roles[MessageContentModel::MediaInfoRole] = "mediaInfo";
roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo";
roles[MessageContentModel::ItineraryModelRole] = "itineraryModel";
roles[MessageContentModel::LatitudeRole] = "latitude";
roles[MessageContentModel::LongitudeRole] = "longitude";
roles[MessageContentModel::AssetRole] = "asset";
roles[MessageContentModel::PollHandlerRole] = "pollHandler";
roles[MessageContentModel::ReplyEventIdRole] = "replyEventId";
roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor";
roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel";
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
return roles;
}
@@ -406,7 +411,7 @@ void MessageContentModel::resetModel()
beginResetModel();
m_components.clear();
if (m_room->connection()->isIgnored(m_eventSenderId) || m_currentState == UnAvailable) {
if (m_room->connection()->isIgnored(senderId()) || m_currentState == UnAvailable) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel();
return;
@@ -419,9 +424,7 @@ void MessageContentModel::resetModel()
return;
}
if (m_showAuthor) {
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
}
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
m_components += messageContentComponents();
endResetModel();
@@ -477,8 +480,18 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
newComponents = addLinkPreviews(newComponents);
}
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#else
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
#endif
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
}
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
#else
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {

View File

@@ -25,11 +25,6 @@ class MessageContentModel : public QAbstractListModel
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief Whether the author component is being shown.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
public:
enum MessageState {
Unknown, /**< The message state is unknown. */
@@ -75,9 +70,6 @@ public:
bool isPending = false,
MessageContentModel *parent = nullptr);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
/**
* @brief Get the given role value at the given index.
*
@@ -99,6 +91,8 @@ public:
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
static QHash<int, QByteArray> roleNamesStatic();
/**
* @brief Close the link preview at the given index.
*
@@ -113,11 +107,10 @@ Q_SIGNALS:
private:
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QString m_eventSenderId;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
QString senderId() const;
NeochatRoomMember *senderObject() const;
MessageState m_currentState = Unknown;
bool m_showAuthor = true;
bool m_isReply;
void initializeModel();

View File

@@ -9,12 +9,11 @@
#include "enums/delegatetype.h"
#include "messagecontentmodel.h"
#include "neochatconfig.h"
#include "neochatroommember.h"
#include "timelinemessagemodel.h"
using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
MessageFilterModel::MessageFilterModel(QObject *parent, QAbstractItemModel *sourceModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceModel);
@@ -93,12 +92,8 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const
return authorList(mapToSource(index).row());
} else if (role == ExcessAuthorsRole) {
return excessAuthors(mapToSource(index).row());
} else if (role == TimelineMessageModel::ContentModelRole) {
const auto model = qvariant_cast<MessageContentModel *>(mapToSource(index).data(TimelineMessageModel::ContentModelRole));
if (model != nullptr && !showAuthor(index)) {
model->setShowAuthor(false);
}
return QVariant::fromValue<MessageContentModel *>(model);
} else if (role == MessageModel::ShowAuthorRole) {
return showAuthor(index);
}
return QSortFilterProxyModel::data(index, role);
}

View File

@@ -37,7 +37,7 @@ public:
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr, TimelineModel *sourceModel = nullptr);
explicit MessageFilterModel(QObject *parent = nullptr, QAbstractItemModel *sourceModel = nullptr);
/**
* @brief Custom filter function to remove hidden messages.

View File

@@ -7,7 +7,7 @@
#include <Quotient/events/roommessageevent.h>
#include <Quotient/events/stickerevent.h>
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
#include <Quotient/thread.h>
#endif
@@ -18,6 +18,7 @@
#include "eventhandler.h"
#include "events/pollevent.h"
#include "models/reactionmodel.h"
#include "neochatroommember.h"
using namespace Quotient;
@@ -54,6 +55,9 @@ void MessageModel::setRoom(NeoChatRoom *room)
beginResetModel();
m_room = room;
if (m_room != nullptr) {
m_room->setVisible(true);
}
Q_EMIT roomChanged();
endResetModel();
}
@@ -116,16 +120,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
}
if (role == ContentModelRole) {
QString modelId;
if (!event->get().id().isEmpty() && m_contentModels.contains(event->get().id())) {
modelId = event.value().get().id();
} else if (!event.value().get().transactionId().isEmpty() && m_contentModels.contains(event.value().get().transactionId())) {
modelId = event.value().get().transactionId();
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(roomMessageEvent->threadRootEventId()));
}
if (!modelId.isEmpty()) {
return QVariant::fromValue<MessageContentModel *>(m_contentModels.at(modelId).get());
}
return {};
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(&event->get()));
}
if (role == GenericDisplayRole) {
@@ -144,11 +143,11 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
mId = event.value().get().senderId();
}
if (!m_memberObjects.contains(mId)) {
if (!m_room->isMember(mId)) {
return QVariant::fromValue<NeochatRoomMember *>(emptyNeochatRoomMember);
}
return QVariant::fromValue<NeochatRoomMember *>(m_memberObjects.at(mId).get());
return QVariant::fromValue<NeochatRoomMember *>(m_room->qmlSafeMember(mId));
}
if (role == HighlightRole) {
@@ -174,7 +173,7 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
}
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event.value().get());
#if Quotient_VERSION_MINOR > 9
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(event.value().get().id()))) {
const auto &thread = m_room->threads().value(roomMessageEvent->isThreaded() ? roomMessageEvent->threadRootEventId() : event.value().get().id());
if (thread.latestEventId != event.value().get().id()) {
@@ -197,13 +196,15 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
if (role == ProgressInfoRole) {
if (auto e = eventCast<const RoomMessageEvent>(&event.value().get())) {
if (e->has<EventContent::FileContent>()) {
if (e->has<EventContent::FileContent>() || e->has<EventContent::ImageContent>() || e->has<EventContent::VideoContent>()
|| e->has<EventContent::AudioContent>()) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get()));
}
}
if (eventCast<const StickerEvent>(&event.value().get())) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get()));
}
return {};
}
if (role == TimeRole) {
@@ -299,6 +300,10 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
&& event.value().get().senderId() == m_room->localMember().id();
}
if (role == ShowAuthorRole) {
return true;
}
return {};
}
@@ -328,6 +333,7 @@ QHash<int, QByteArray> MessageModel::roleNames() const
roles[ContentModelRole] = "contentModel";
roles[MediaInfoRole] = "mediaInfo";
roles[IsEditableRole] = "isEditable";
roles[ShowAuthorRole] = "showAuthor";
return roles;
}
@@ -424,21 +430,6 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
senderId = m_room->localMember().id();
}
if (!m_memberObjects.contains(senderId)) {
m_memberObjects[senderId] = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
if (!event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_room, eventId, false, isPending));
}
}
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) {
m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer<ThreadModel>(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room));
}
// ReadMarkerModel handles updates to add and remove markers, we only need to
// handle adding and removing whole models here.
if (m_readMarkerModels.contains(eventId)) {
@@ -493,12 +484,17 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isP
void MessageModel::clearModel()
{
if (m_room) {
const auto oldRoom = m_room;
// HACK: Reset the model to a null room first to make sure QML dismantles
// last room's objects before the room is actually changed
beginResetModel();
m_room->disconnect(this);
m_room = nullptr;
endResetModel();
// Because we don't want any of the object deleted before the model is cleared.
oldRoom->setVisible(false);
}
// Don't clear the member objects until the model has been fully reset and all
@@ -508,8 +504,6 @@ void MessageModel::clearModel()
void MessageModel::clearEventObjects()
{
m_memberObjects.clear();
m_contentModels.clear();
m_reactionModels.clear();
m_readMarkerModels.clear();
}
@@ -524,7 +518,7 @@ bool MessageModel::event(QEvent *event)
ThreadModel *MessageModel::threadModelForRootId(const QString &threadRootId) const
{
return m_threadModels[threadRootId].data();
return m_room->modelForThread(threadRootId);
}
#include "moc_messagemodel.cpp"

View File

@@ -7,9 +7,7 @@
#include <QQmlEngine>
#include <functional>
#include "messagecontentmodel.h"
#include "neochatroom.h"
#include "neochatroommember.h"
#include "pollhandler.h"
#include "readmarkermodel.h"
#include "threadmodel.h"
@@ -19,11 +17,27 @@ class ReactionModel;
/**
* @class MessageModel
*
* This class defines a model for visualising the room events.
* This class defines a base model for visualising the room events.
*
* This model covers all event types in the room with many of the roles being
* specific to a subset of events. This means the user needs to understand which
* roles will return useful information for a given event type.
* On its own MessageModel will result in an empty timeline as there is no mechanism
* to retrieve events. This is by design as it allows the model to be inherited from
* and the user can specify their own source of events, e.g. a room timeline or a
* search result.
*
* The inherited model MUST do the following:
* - Define methods for retrieving events
* - Call newEventAdded() for each new event in the model so that all the required
* event objects are created.
* - Override getEventForIndex() so that the data() function can get an event for a
* given index
* - Override rowCount()
*
* Optionally the new model can:
* - override timelineServerIndex() if dealing with pending events otherwise the default
* function returns 0 which is correct for other use cases.
* - m_lastReadEventIndex is available to track a read marker location (so that the
* data function can output the appropriate values). The new class must implement
* the functionality to add, move, remove, etc though.
*
* @sa NeoChatRoom
*/
@@ -31,6 +45,7 @@ class MessageModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
/**
* @brief The current room that the model is getting its messages from.
@@ -70,6 +85,7 @@ public:
IsRedactedRole, /**< Whether an event has been deleted. */
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
IsEditableRole, /**< Whether the event can be edited by the user. */
ShowAuthorRole, /**< Whether the author of a message should be shown. */
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
@@ -98,10 +114,23 @@ public:
*/
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
/**
* @brief Get a ThreadModel for the give thread root Matrix ID.
*/
Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const;
Q_SIGNALS:
/**
* @brief Emitted when the room is changed.
*/
void roomChanged();
/**
* @brief A signal to tell the MessageModel that a new event has been added.
*
* Any model inheriting from MessageModel needs to emit this signal for every
* new event it adds.
*/
void newEventAdded(const Quotient::RoomEvent *event, bool isPending = false);
protected:
@@ -125,10 +154,7 @@ private:
bool resetting = false;
bool movingEvent = false;
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false);

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "pinnedmessagemodel.h"
#include "enums/delegatetype.h"
#include "eventhandler.h"
#include "models/messagecontentmodel.h"
#include "neochatroom.h"
#include <QGuiApplication>
#include <KLocalizedString>
using namespace Quotient;
PinnedMessageModel::PinnedMessageModel(QObject *parent)
: MessageModel(parent)
{
connect(this, &MessageModel::roomChanged, this, &PinnedMessageModel::fill);
}
bool PinnedMessageModel::loading() const
{
return m_loading;
}
int PinnedMessageModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_pinnedEvents.size();
}
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> PinnedMessageModel::getEventForIndex(const QModelIndex index) const
{
if (static_cast<size_t>(index.row()) >= m_pinnedEvents.size() || index.row() < 0) {
return std::nullopt;
}
return std::reference_wrapper{*m_pinnedEvents[index.row()].get()};
}
void PinnedMessageModel::setLoading(bool loading)
{
m_loading = loading;
Q_EMIT loadingChanged();
}
void PinnedMessageModel::fill()
{
if (!m_room) {
return;
}
const auto events = m_room->pinnedEventIds();
for (const auto &event : std::as_const(events)) {
auto job = m_room->connection()->callApi<GetOneRoomEventJob>(m_room->id(), event);
connect(job, &BaseJob::success, this, [this, job] {
beginInsertRows({}, m_pinnedEvents.size(), m_pinnedEvents.size());
m_pinnedEvents.push_back(std::move(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData())));
Q_EMIT newEventAdded(m_pinnedEvents.back().get(), false);
endInsertRows();
});
}
}
#include "moc_pinnedmessagemodel.cpp"

View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QString>
#include <Quotient/csapi/rooms.h>
#include "messagemodel.h"
#include "neochatroommember.h"
namespace Quotient
{
class Connection;
}
class NeoChatRoom;
/**
* @class PinnedMessageModel
*
* This class defines the model for visualising a room's pinned messages.
*/
class PinnedMessageModel : public MessageModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief Whether the model is currently loading.
*/
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
explicit PinnedMessageModel(QObject *parent = nullptr);
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool loading() const;
Q_SIGNALS:
void loadingChanged();
protected:
std::optional<std::reference_wrapper<const Quotient::RoomEvent>> getEventForIndex(QModelIndex index) const override;
private:
void setLoading(bool loading);
void fill();
bool m_loading = false;
std::vector<Quotient::event_ptr_tt<Quotient::RoomEvent>> m_pinnedEvents;
};

View File

@@ -11,18 +11,18 @@
#include "chatbarcache.h"
#include "eventhandler.h"
#include "messagecomponenttype.h"
#include "messagecontentmodel.h"
#include "neochatroom.h"
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
: QConcatenateTablesProxyModel(room)
, m_threadRootId(threadRootId)
, m_threadFetchModel(new ThreadFetchModel(this))
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
{
Q_ASSERT(!m_threadRootId.isEmpty());
Q_ASSERT(room);
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0)
connect(room, &Quotient::Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
#else
@@ -49,7 +49,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
// If the thread was created by the local user fetchMore() won't find the current
// pending event.
checkPending();
fetchMore({});
fetchMoreEvents(3);
addModels();
}
@@ -73,34 +73,28 @@ QString ThreadModel::threadRootId() const
return m_threadRootId;
}
MessageContentModel *ThreadModel::threadRootContentModel() const
{
return m_threadRootContentModel.get();
}
QHash<int, QByteArray> ThreadModel::roleNames() const
{
return m_threadRootContentModel->roleNames();
return MessageContentModel::roleNamesStatic();
}
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
bool ThreadModel::moreEventsAvailable(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return !m_currentJob && m_nextBatch.has_value();
}
void ThreadModel::fetchMore(const QModelIndex &parent)
void ThreadModel::fetchMoreEvents(int max)
{
Q_UNUSED(parent);
if (!m_currentJob && m_nextBatch.has_value()) {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
const auto connection = room->connection();
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5);
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), max);
Q_EMIT moreEventsAvailableChanged();
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) {
m_contentModels.push_back(new MessageContentModel(room, event->id()));
m_events.push_back(event->id());
}
addModels();
@@ -116,18 +110,18 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
}
m_currentJob.clear();
Q_EMIT moreEventsAvailableChanged();
});
}
}
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
{
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto eventId = event->id();
if (eventId.isEmpty()) {
eventId = event->transactionId();
}
m_contentModels.push_front(new MessageContentModel(room, eventId));
m_events.push_front(eventId);
}
void ThreadModel::addModels()
@@ -136,9 +130,16 @@ void ThreadModel::addModels()
clearModels();
}
addSourceModel(m_threadRootContentModel.get());
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
addSourceModel(*it);
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
if (room == nullptr) {
return;
}
addSourceModel(m_threadFetchModel);
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
const auto contentModel = room->contentModelForEvent(*it);
if (contentModel != nullptr) {
addSourceModel(room->contentModelForEvent(*it));
}
}
addSourceModel(m_threadChatBarModel);
@@ -148,15 +149,90 @@ void ThreadModel::addModels()
void ThreadModel::clearModels()
{
removeSourceModel(m_threadRootContentModel.get());
for (const auto &model : m_contentModels) {
if (sourceModels().contains(model)) {
removeSourceModel(model);
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
if (room == nullptr) {
return;
}
removeSourceModel(m_threadFetchModel);
for (const auto &model : m_events) {
const auto contentModel = room->contentModelForEvent(model);
if (sourceModels().contains(contentModel)) {
removeSourceModel(contentModel);
}
}
removeSourceModel(m_threadChatBarModel);
}
void ThreadModel::closeLinkPreview(int row)
{
if (row < 0 || row >= rowCount()) {
return;
}
const auto index = this->index(row, 0);
if (!index.isValid()) {
return;
}
const auto sourceIndex = mapToSource(index);
const auto sourceModel = sourceIndex.model();
if (sourceModel == nullptr) {
return;
}
// This is a bit silly but we can only get a const reference to the model from the
// index so we need to search the source models.
for (const auto &model : sourceModels()) {
if (model == sourceModel) {
const auto sourceContentModel = dynamic_cast<MessageContentModel *>(model);
if (sourceContentModel == nullptr) {
return;
}
sourceContentModel->closeLinkPreview(sourceIndex.row());
}
}
}
ThreadFetchModel::ThreadFetchModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto threadModel = dynamic_cast<ThreadModel *>(parent);
Q_ASSERT(threadModel != nullptr);
connect(threadModel, &ThreadModel::moreEventsAvailableChanged, this, [this]() {
beginResetModel();
endResetModel();
});
}
QVariant ThreadFetchModel::data(const QModelIndex &idx, int role) const
{
if (idx.row() < 0 || idx.row() > 1) {
return {};
}
if (role == ComponentTypeRole) {
return MessageComponentType::FetchButton;
}
return {};
}
int ThreadFetchModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
if (threadModel == nullptr) {
qWarning() << "ThreadFetchModel created with incorrect parent, a ThreadModel must be set as the parent on creation.";
return {};
}
return threadModel->moreEventsAvailable({}) ? 1 : 0;
}
QHash<int, QByteArray> ThreadFetchModel::roleNames() const
{
return {
{ComponentTypeRole, "componentType"},
};
}
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
: QAbstractListModel(parent)
, m_room(room)

View File

@@ -21,6 +21,52 @@
class NeoChatRoom;
class ReactionModel;
/**
* @class ThreadFetchModel
*
* A model to provide a fetch more historical messages button in a thread.
*/
class ThreadFetchModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Defines the model roles.
*
* The role values need to match MessageContentModel not to blow up.
*
* @sa MessageContentModel
*/
enum Roles {
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
};
Q_ENUM(Roles)
explicit ThreadFetchModel(QObject *parent);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1 or 0, depending on whether there are more messages to download.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with ComponentTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
};
/**
* @class ThreadChatBarModel
*
@@ -91,11 +137,6 @@ public:
QString threadRootId() const;
/**
* @brief The content model for the thread root event.
*/
MessageContentModel *threadRootContentModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
@@ -104,32 +145,33 @@ public:
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
/**
* @brief Whether there is more data available for the model to fetch.
*
* @sa QAbstractItemModel::canFetchMore()
* @brief Whether there are more events for the model to fetch.
*/
bool canFetchMore(const QModelIndex &parent) const override;
bool moreEventsAvailable(const QModelIndex &parent) const;
/**
* @brief Fetches the next batch of model data if any is available.
*
* @sa QAbstractItemModel::fetchMore()
* @brief Fetches the next batch of events if any is available.
*/
void fetchMore(const QModelIndex &parent) override;
Q_INVOKABLE void fetchMoreEvents(int max = 5);
/**
* @brief Close the link preview at the given index.
*
* If the given index is not a link preview component, nothing happens.
*/
Q_INVOKABLE void closeLinkPreview(int row);
Q_SIGNALS:
void moreEventsAvailableChanged();
private:
QString m_threadRootId;
QPointer<MessageContentModel> m_threadRootContentModel;
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
std::deque<MessageContentModel *> m_contentModels;
std::deque<QString> m_events;
ThreadFetchModel *m_threadFetchModel;
ThreadChatBarModel *m_threadChatBarModel;
QList<QString> m_events;
QList<QString> m_pendingEvents;
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;

View File

@@ -27,24 +27,20 @@ void TimelineMessageModel::connectNewRoom()
}
connect(m_room, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
for (auto &&event : events) {
Q_EMIT newEventAdded(event.get());
}
m_initialized = true;
beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1);
});
connect(m_room, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
for (auto &event : events) {
Q_EMIT newEventAdded(event.get());
}
if (rowCount() > 0) {
rowBelowInserted = rowCount() - 1; // See #312
}
m_initialized = true;
beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1);
});
connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) {
if (m_initialized) {
for (int i = lowest; i == biggest; ++i) {
const auto event = m_room->findInTimeline(i)->event();
Q_EMIT newEventAdded(event);
}
endInsertRows();
}
if (!m_lastReadEventIndex.isValid()) {

View File

@@ -174,8 +174,8 @@ void UserListModel::refreshAllMembers()
m_members = m_currentRoom->joinedMemberIds();
MemberSorter sorter;
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
const auto leftPl = m_currentRoom->getUserPowerLevel(left);
const auto rightPl = m_currentRoom->getUserPowerLevel(right);
const auto leftPl = m_currentRoom->memberEffectivePowerLevel(left);
const auto rightPl = m_currentRoom->memberEffectivePowerLevel(right);
if (leftPl > rightPl) {
return true;
} else if (rightPl > leftPl) {
@@ -184,7 +184,6 @@ void UserListModel::refreshAllMembers()
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
});
}
endResetModel();
Q_EMIT usersRefreshed();

View File

@@ -39,7 +39,7 @@ struct WebShortcutModelPrivate;
* }
* QQC2.MenuSeparator {}
* QQC2.MenuItem {
* text: i18n("Configure Web Shortcuts...")
* text: i18n("Configure Web Shortcuts")
* icon.name: "configure"
* onTriggered: webshortcutmodel.configureWebShortcuts()
* }

View File

@@ -223,6 +223,10 @@
<label>Reject unknown invites</label>
<default>false</default>
</entry>
<entry name="PreferUsingEncryption" type="bool">
<label>Prefer encrypting chats</label>
<default>true</default>
</entry>
</group>
</kcfg>

View File

@@ -6,7 +6,6 @@
#include <QImageReader>
#include <QJsonDocument>
#include "jobs/neochatdeactivateaccountjob.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "spacehierarchycache.h"
@@ -21,7 +20,6 @@
#include <Quotient/csapi/profile.h>
#include <Quotient/csapi/registration.h>
#include <Quotient/csapi/versions.h>
#include <Quotient/database.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/room.h>
@@ -109,10 +107,6 @@ void NeoChatConnection::connectSignals()
Q_EMIT homeHaveHighlightNotificationsChanged();
});
});
connect(this, &NeoChatConnection::invitedRoom, this, [this](Quotient::Room *room) {
auto r = dynamic_cast<NeoChatRoom *>(room);
connect(r, &NeoChatRoom::showInviteNotification, this, &NeoChatConnection::showInviteNotification);
});
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
Q_UNUSED(room)
if (prev && prev->isDirectChat()) {
@@ -142,9 +136,15 @@ void NeoChatConnection::connectSignals()
connect(job, &GetVersionsJob::success, this, [this, job] {
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_L1);
Q_EMIT canCheckMutualRoomsChanged();
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
Q_EMIT canEraseDataChanged();
});
},
Qt::SingleShotConnection);
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] {
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
});
}
int NeoChatConnection::badgeNotificationCount() const
@@ -255,20 +255,20 @@ QString NeoChatConnection::label() const
return accountDataJson("org.kde.neochat.account_label"_L1)["account_label"_L1].toString();
}
void NeoChatConnection::deactivateAccount(const QString &password)
void NeoChatConnection::deactivateAccount(const QString &password, const bool erase)
{
auto job = callApi<NeoChatDeactivateAccountJob>();
connect(job, &BaseJob::result, this, [this, job, password] {
auto job = callApi<DeactivateAccountJob>();
connect(job, &BaseJob::result, this, [this, job, password, erase] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"_L1] = replyData["session"_L1];
authData["password"_L1] = password;
authData["type"_L1] = "m.login.password"_L1;
authData["user"_L1] = user()->id();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.authInfo["password"_L1] = password;
authData.type = "m.login.password"_L1;
authData.authInfo["user"_L1] = user()->id();
QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
authData["identifier"_L1] = identifier;
auto innerJob = callApi<NeoChatDeactivateAccountJob>(authData);
authData.authInfo["identifier"_L1] = identifier;
auto innerJob = callApi<DeactivateAccountJob>(authData, QString{}, erase);
connect(innerJob, &BaseJob::success, this, [this]() {
logout(false);
});
@@ -481,23 +481,6 @@ QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
#endif
}
QString NeoChatConnection::deviceKey() const
{
return edKeyForUserDevice(userId(), deviceId());
}
QString NeoChatConnection::encryptionKey() const
{
auto query = database()->prepareQuery(u"SELECT curveKey FROM tracked_devices WHERE matrixId=:matrixId AND deviceid=:deviceId LIMIT 1;"_s);
query.bindValue(u":matrixId"_s, userId());
query.bindValue(u":deviceId"_s, deviceId());
database()->execute(query);
if (!query.next()) {
return {};
}
return query.value(0).toString();
}
bool NeoChatConnection::isOnline() const
{
return m_isOnline;
@@ -548,4 +531,9 @@ KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphra
return KeyImport::Success;
}
bool NeoChatConnection::canEraseData() const
{
return m_canEraseData;
}
#include "moc_neochatconnection.cpp"

View File

@@ -31,8 +31,6 @@ class NeoChatConnection : public Quotient::Connection
* Set to an empty string to remove the label.
*/
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
/**
* @brief The model with the account's 3PIDs.
@@ -87,6 +85,11 @@ class NeoChatConnection : public Quotient::Connection
*/
Q_PROPERTY(bool canCheckMutualRooms READ canCheckMutualRooms NOTIFY canCheckMutualRoomsChanged)
/**
* @brief Whether the server supports erasing user data when deactivating the account. This checks for MSC4025.
*/
Q_PROPERTY(bool canEraseData READ canEraseData NOTIFY canEraseDataChanged)
public:
/**
* @brief Defines the status after an attempt to change the password on an account.
@@ -104,6 +107,7 @@ public:
Q_INVOKABLE void logout(bool serverSideLogout);
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
bool canCheckMutualRooms() const;
bool canEraseData() const;
/**
* @brief Change the password for an account.
@@ -123,7 +127,7 @@ public:
[[nodiscard]] QString label() const;
void setLabel(const QString &label);
Q_INVOKABLE void deactivateAccount(const QString &password);
Q_INVOKABLE void deactivateAccount(const QString &password, bool erase);
ThreePIdModel *threePIdModel() const;
@@ -174,9 +178,6 @@ public:
// the reference could be destroyed before the task is finished
QCoro::Task<void> setupPushNotifications(QString endpoint);
QString deviceKey() const;
QString encryptionKey() const;
bool isOnline() const;
LinkPreviewer *previewerForLink(const QUrl &link);
@@ -194,6 +195,7 @@ Q_SIGNALS:
void userConsentRequired(QUrl url);
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
void canCheckMutualRoomsChanged();
void canEraseDataChanged();
/**
* @brief Request a message be shown to the user of the given type.
@@ -205,11 +207,6 @@ Q_SIGNALS:
*/
void errorOccured(const QString &error);
/**
* @brief Request a notification be shown for an invite to this room.
*/
void showInviteNotification(NeoChatRoom *room);
private:
bool m_isOnline = true;
void setIsOnline(bool isOnline);
@@ -223,4 +220,5 @@ private:
QCache<QUrl, LinkPreviewer> m_linkPreviewers;
bool m_canCheckMutualRooms = false;
bool m_canEraseData = false;
};

View File

@@ -44,6 +44,7 @@
#include "events/pollevent.h"
#include "filetransferpseudojob.h"
#include "neochatconfig.h"
#include "neochatroommember.h"
#include "roomlastmessageprovider.h"
#include "spacehierarchycache.h"
#include "texthandler.h"
@@ -126,9 +127,6 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
updatePushNotificationState(u"m.push_rules"_s);
Q_EMIT canEncryptRoomChanged();
if (this->joinState() == JoinState::Invite) {
Q_EMIT showInviteNotification(this);
}
},
Qt::SingleShotConnection);
connect(this, &Room::changed, this, [this] {
@@ -161,6 +159,22 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
});
}
bool NeoChatRoom::visible() const
{
return m_visible;
}
void NeoChatRoom::setVisible(bool visible)
{
m_visible = visible;
if (!visible) {
m_memberObjects.clear();
m_eventContentModels.clear();
m_threadModels.clear();
}
}
int NeoChatRoom::contextAwareNotificationCount() const
{
// DOn't include spaces, rooms that the user hasn't joined and rooms where the user has joined the successor.
@@ -256,7 +270,15 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Upload, url.toLocalFile(), txnId);
connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
connect(this, &Room::fileTransferFailed, job, [this, job, txnId] {
auto info = fileTransferInfo(txnId);
if (info.status == FileTransferInfo::Cancelled) {
job->fileTransferCanceled(txnId);
} else {
job->fileTransferFailed(txnId);
}
});
connect(job, &FileTransferPseudoJob::cancelRequested, this, &Room::cancelFileTransfer);
KIO::getJobTracker()->registerJob(job);
job->start();
#endif
@@ -759,22 +781,6 @@ void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel
}
}
int NeoChatRoom::getUserPowerLevel(const QString &userId) const
{
if (!successorId().isEmpty()) {
return 0; // No one can upgrade a room that's already upgraded
}
const auto &mId = userId.isEmpty() ? connection()->userId() : userId;
if (const auto *plEvent = currentState().get<RoomPowerLevelsEvent>()) {
return plEvent->powerLevelForUser(mId);
}
if (const auto *createEvent = creation()) {
return createEvent->senderId() == mId ? 100 : 0;
}
return 0; // That's rather weird but may happen, according to rvdh
}
QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
{
QStringList events;
@@ -1511,7 +1517,15 @@ void NeoChatRoom::download(const QString &eventId, const QUrl &localFilename)
auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Download, localFilename.toLocalFile(), eventId);
connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
connect(this, &Room::fileTransferFailed, job, [this, job, eventId] {
auto info = fileTransferInfo(eventId);
if (info.status == FileTransferInfo::Cancelled) {
job->fileTransferCanceled(eventId);
} else {
job->fileTransferFailed(eventId);
}
});
connect(job, &FileTransferPseudoJob::cancelRequested, this, &Room::cancelFileTransfer);
KIO::getJobTracker()->registerJob(job);
job->start();
#endif
@@ -1710,4 +1724,82 @@ void NeoChatRoom::setRoomState(const QString &type, const QString &stateKey, con
setState(type, stateKey, QJsonDocument::fromJson(content).object());
}
NeochatRoomMember *NeoChatRoom::qmlSafeMember(const QString &memberId)
{
if (!m_memberObjects.contains(memberId)) {
return m_memberObjects.emplace(memberId, std::make_unique<NeochatRoomMember>(this, memberId)).first->second.get();
}
return m_memberObjects[memberId].get();
}
MessageContentModel *NeoChatRoom::contentModelForEvent(const QString &eventId)
{
if (eventId.isEmpty()) {
return nullptr;
}
if (!m_eventContentModels.contains(eventId)) {
return m_eventContentModels.emplace(eventId, std::make_unique<MessageContentModel>(this, eventId)).first->second.get();
}
return m_eventContentModels[eventId].get();
}
MessageContentModel *NeoChatRoom::contentModelForEvent(const Quotient::RoomEvent *event)
{
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (roomMessageEvent == nullptr) {
// If for some reason a model is there remove.
if (m_eventContentModels.contains(event->id())) {
m_eventContentModels.erase(event->id());
}
if (m_eventContentModels.contains(event->transactionId())) {
m_eventContentModels.erase(event->transactionId());
}
return nullptr;
}
if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
return nullptr;
}
auto eventId = event->id();
const auto txnId = event->transactionId();
if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) {
return m_eventContentModels.emplace(eventId, std::make_unique<MessageContentModel>(this, eventId.isEmpty() ? txnId : eventId, false, eventId.isEmpty()))
.first->second.get();
}
if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) {
return m_eventContentModels[eventId].get();
}
if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) {
if (eventId.isEmpty()) {
return m_eventContentModels[txnId].get();
}
// If we now have an event ID use that as the map key instead of transaction ID.
auto txnModel = std::move(m_eventContentModels[txnId]);
m_eventContentModels.erase(txnId);
return m_eventContentModels.emplace(eventId, std::move(txnModel)).first->second.get();
}
return nullptr;
}
ThreadModel *NeoChatRoom::modelForThread(const QString &threadRootId)
{
if (threadRootId.isEmpty()) {
return nullptr;
}
if (!m_threadModels.contains(threadRootId)) {
return m_threadModels.emplace(threadRootId, std::make_unique<ThreadModel>(threadRootId, this)).first->second.get();
}
return m_threadModels[threadRootId].get();
}
#include "moc_neochatroom.cpp"

View File

@@ -3,6 +3,7 @@
#pragma once
#include <Quotient/events/roomevent.h>
#include <Quotient/room.h>
#include <QCache>
@@ -14,6 +15,8 @@
#include "enums/messagetype.h"
#include "enums/pushrule.h"
#include "models/messagecontentmodel.h"
#include "models/threadmodel.h"
#include "neochatroommember.h"
#include "pollhandler.h"
@@ -211,6 +214,9 @@ class NeoChatRoom : public Quotient::Room
public:
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
bool visible() const;
void setVisible(bool visible);
[[nodiscard]] QDateTime lastActiveTime();
/**
@@ -498,14 +504,6 @@ public:
bool canEncryptRoom() const;
/**
* @brief Get the power level for the given user ID in the room.
*
* Returns the default value for a user in the room if they have no escalated
* privileges or if they are not a member so membership should be known before using.
*/
Q_INVOKABLE [[nodiscard]] int getUserPowerLevel(const QString &userId) const;
Q_INVOKABLE void setUserPowerLevel(const QString &userID, const int &powerLevel);
ChatBarCache *mainCache() const;
@@ -590,7 +588,57 @@ public:
*/
Quotient::FileTransferInfo cachedFileTransferInfo(const Quotient::RoomEvent *event) const;
/**
* @brief Return a NeochatRoomMember object for the given user ID.
*
* @warning Because we can't guarantee that a member state event is downloaded
* before a message they sent arrives this will create the object unconditionally
* assuming that the state event will turn up later. It is therefor the
* responsibility of the caller to ensure that they only ask for objects
* for real senders.
*/
NeochatRoomMember *qmlSafeMember(const QString &memberId);
/**
* @brief Returns the content model for the given event ID.
*
* A model is created is one doesn't exist. Will return nullptr if evtOrTxnId
* is empty.
*
* @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent
* event ID. The caller must ensure that the ID is a real event. A model will be
* returned unconditionally.
*
* @warning Do NOT use for pending events as this function has no way to differentiate.
*/
MessageContentModel *contentModelForEvent(const QString &evtOrTxnId);
/**
* @brief Returns the content model for the given event.
*
* A model is created is one doesn't exist. Will return nullptr if event is:
* - nullptr
* - not a Quotient::RoomMessageEvent (e.g a state event)
*
* @note This method is preferred to the version using just an event ID as it
* can perform some basic checks. If a copy of the event is not available,
* you may have to use the version that takes an event ID.
*
* @note This version must be used for pending events as it can differentiate.
*/
MessageContentModel *contentModelForEvent(const Quotient::RoomEvent *event);
/**
* @brief Returns the thread model for the given thread root event ID.
*
* A model is created is one doesn't exist. Will return nullptr if threadRootId
* is empty.
*/
Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId);
private:
bool m_visible = false;
QSet<const Quotient::RoomEvent *> highlights;
bool m_hasFileUploading = false;
@@ -619,6 +667,10 @@ private:
void cleanupExtraEventRange(Quotient::RoomEventsRange events);
void cleanupExtraEvent(const QString &eventId);
std::unordered_map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
std::unordered_map<QString, std::unique_ptr<MessageContentModel>> m_eventContentModels;
std::unordered_map<QString, std::unique_ptr<ThreadModel>> m_threadModels;
private Q_SLOTS:
void updatePushNotificationState(QString type);
@@ -654,14 +706,6 @@ Q_SIGNALS:
*/
void showMessage(MessageType::Type messageType, const QString &message);
/**
* @brief Request a notification be shown for an invite to this room.
*
* @note This may later be blocked if there are any rules on where invites can
* come from, but this is not NeoChatRoom's responsibility.
*/
void showInviteNotification(NeoChatRoom *room);
public Q_SLOTS:
/**
* @brief Upload a file to the matrix server and post the file to the room.

View File

@@ -127,9 +127,8 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
}
auto sender = room->member(notification["event"_L1]["sender"_L1].toString());
// Don't display notifications for events in invited rooms
// This should prevent empty notifications from appearing when they shouldn't
if (room->joinState() == JoinState::Invite) {
postInviteNotification(qobject_cast<NeoChatRoom *>(room));
continue;
}
@@ -244,7 +243,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (canReply) {
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
replyAction->setPlaceholderText(i18n("Reply..."));
replyAction->setPlaceholderText(i18n("Reply"));
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
TextHandler textHandler;
textHandler.setData(text);

View File

@@ -71,7 +71,7 @@
"Description[uk]": "Оприлюднити за допомогою NeoChat",
"Description[x-test]": "xxShare via NeoChatxx",
"Description[zh_TW]": "透過 NeoChat 分享",
"Icon": "org.kde.neochat",
"Icon": "org.kde.neochat.tray",
"License": "GPL",
"Name": "NeoChat",
"Name[ar]": "نيوتشات",

View File

@@ -6,20 +6,19 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.settings
import org.kde.neochat.devtools
QQC2.Menu {
KirigamiComponents.ConvergentContextMenu {
id: root
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window
margins: Kirigami.Units.smallSpacing
QQC2.MenuItem {
QQC2.Action {
text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
@@ -36,7 +35,8 @@ QQC2.Menu {
qrMax.open();
}
}
QQC2.MenuItem {
QQC2.Action {
text: i18n("Edit This Account")
icon.name: "document-edit"
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.settings', 'AccountEditorPage'), {
@@ -45,21 +45,24 @@ QQC2.Menu {
title: i18n("Account editor")
})
}
QQC2.MenuItem {
QQC2.Action {
text: i18n("Notification Settings")
icon.name: "notifications"
onTriggered: {
NeoChatSettingsView.open('notifications');
}
}
QQC2.MenuItem {
QQC2.Action {
text: i18n("Devices")
icon.name: "computer-symbolic"
onTriggered: {
NeoChatSettingsView.open('devices');
}
}
QQC2.MenuItem {
Kirigami.Action {
text: i18n("Open Developer Tools")
icon.name: "tools"
visible: NeoChatConfig.developerTools
@@ -71,7 +74,8 @@ QQC2.Menu {
height: Kirigami.Units.gridUnit * 42
})
}
QQC2.MenuItem {
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock"
visible: NeoChatConfig.secretBackup
@@ -79,22 +83,21 @@ QQC2.Menu {
title: i18nc("@title:window", "Open Key Backup")
})
}
QQC2.MenuItem {
QQC2.Action {
text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low"
onTriggered: root.connection.startSelfVerification()
enabled: Controller.csSupported
}
QQC2.MenuItem {
QQC2.Action {
text: i18n("Logout")
icon.name: "im-kick-user"
onTriggered: confirmLogoutDialogComponent.createObject(QQC2.ApplicationWindow.window.overlay).open()
}
Component {
id: confirmLogoutDialogComponent
ConfirmLogoutDialog {
connection: root.connection
}
readonly property Component confirmLogoutDialogComponent: ConfirmLogoutDialog {
connection: root.connection
}
}

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigamiaddons.formcard as FormCard
@@ -18,12 +19,21 @@ Kirigami.PromptDialog {
dialogType: Kirigami.PromptDialog.Warning
mainItem: FormCard.FormTextFieldDelegate {
id: passwordField
label: i18nc("@label:textbox", "Password")
echoMode: TextInput.Password
horizontalPadding: 0
bottomPadding: 0
mainItem: ColumnLayout {
FormCard.FormTextFieldDelegate {
id: passwordField
label: i18nc("@label:textbox", "Password")
echoMode: TextInput.Password
horizontalPadding: 0
bottomPadding: 0
}
FormCard.FormCheckDelegate {
id: eraseDelegate
text: i18nc("@label:checkbox", "Erase Data")
description: i18nc("@info", "Request your server to delete as much user data as possible.")
visible: connection.canEraseData
}
}
footer: QQC2.DialogButtonBox {
@@ -34,7 +44,7 @@ Kirigami.PromptDialog {
icon.name: "emblem-warning"
enabled: passwordField.text.length > 0
onClicked: {
root.connection.deactivateAccount(passwordField.text);
root.connection.deactivateAccount(passwordField.text, eraseDelegate.checked);
root.closeDialog();
}
}

View File

@@ -1,246 +0,0 @@
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
import org.kde.neochat.settings
/**
* Context menu when clicking on a room in the room list
*/
Loader {
id: root
property NeoChatRoom room
required property NeoChatConnection connection
signal closed
Component {
id: regularMenu
QQC2.Menu {
QQC2.MenuItem {
text: i18n("Mark as Read")
icon.name: "checkmark"
enabled: room.notificationCount > 0
onTriggered: room.markAllMessagesAsRead()
}
QQC2.MenuSeparator {}
QQC2.Menu {
title: i18nc("@action:inmenu", "Notifications")
icon.name: "notifications"
QQC2.MenuItem {
text: i18n("Follow Global Setting")
icon.name: "globe"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Default
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Default;
}
}
QQC2.MenuItem {
text: i18nc("As in 'notify for all messages'", "All")
icon.name: "notifications"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.All
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.All;
}
}
QQC2.MenuItem {
text: i18nc("As in 'notify when the user is mentioned or the message contains a set keyword'", "@Mentions and Keywords")
icon.name: "im-user"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.MentionKeyword
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.MentionKeyword;
}
}
QQC2.MenuItem {
text: i18nc("As in 'do not notify for any messages'", "Off")
icon.name: "notifications-disabled"
checkable: true
autoExclusive: true
checked: room.pushNotificationState === PushNotificationState.Mute
enabled: room.pushNotificationState != PushNotificationState.Unknown
onTriggered: {
room.pushNotificationState = PushNotificationState.Mute;
}
}
}
QQC2.MenuItem {
text: room.isFavourite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
icon.name: room.isFavourite ? "rating" : "rating-unrated"
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
QQC2.MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
QQC2.MenuSeparator {}
QQC2.MenuItem {
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
icon.name: "edit-copy"
onTriggered: if (room.isDirectChat()) {
Clipboard.saveText(room.directChatRemoteMember.id);
} else if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id);
} else {
Clipboard.saveText(room.canonicalAlias);
}
}
QQC2.MenuItem {
text: i18nc("@action:inmenu", "Room Settings")
icon.name: 'settings-configure-symbolic'
onTriggered: {
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
}
}
QQC2.MenuSeparator {}
QQC2.MenuItem {
text: i18n("Leave Room")
icon.name: "go-previous"
onTriggered: {
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
room: root.room
}).open();
}
}
onClosed: {
root.closed();
}
}
}
Component {
id: mobileMenu
Kirigami.OverlayDrawer {
id: drawer
parent: applicationWindow().overlay
edge: Qt.BottomEdge
height: popupContent.implicitHeight
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
KirigamiComponents.Avatar {
id: avatar
source: room.avatarMediaUrl
name: room.displayName
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
Kirigami.Heading {
level: 5
Layout.fillWidth: true
text: room.displayName
elide: Text.ElideRight
}
QQC2.ToolButton {
checked: room.isFavourite
checkable: true
icon.name: 'favorite'
Accessible.name: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
QQC2.ToolButton {
text: i18nc("@action:button", "Room Settings")
icon.name: 'settings-configure-symbolic'
onClicked: {
RoomSettingsView.openRoomSettings(root.room, RoomSettingsView.Room);
drawer.close();
}
}
}
Delegates.RoundedItemDelegate {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
onClicked: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
text: i18n("Mark as Read")
icon.name: "checkmark"
onClicked: room.markAllMessagesAsRead()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
text: i18n("Leave Room")
icon.name: "go-previous"
onClicked: {
Qt.createComponent('org.kde.neochat', 'ConfirmLeaveDialog').createObject(root.QQC2.ApplicationWindow.window, {
room: root.room
}).open();
drawer.close();
}
Layout.fillWidth: true
}
}
onClosed: root.closed()
}
}
asynchronous: true
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
function open() {
active = true;
}
onStatusChanged: if (status == Loader.Ready) {
if (Kirigami.Settings.isMobile) {
item.open();
} else {
item.popup();
}
}
}

View File

@@ -9,6 +9,7 @@ import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
@@ -25,7 +26,7 @@ import org.kde.neochat
* For event types that need alternate actions this class can be used as a base and
* the actions and nested actions can be overwritten to show the alternate items.
*/
Loader {
KirigamiComponents.ConvergentContextMenu {
id: root
/**
@@ -62,25 +63,6 @@ Loader {
*/
property string hoveredLink: ""
/**
* @brief The list of menu item actions that have sub-actions.
*
* Each action will be instantiated as a single line that open a sub menu.
*/
property list<Kirigami.Action> nestedActions
/**
* @brief The main list of menu item actions.
*
* Each action will be instantiated as a single line in the menu.
*/
property list<Kirigami.Action> actions
/**
* @brief Whether the web search menu should be shown or not.
*/
property bool enableWebSearch: true
/**
* Some common actions shared between menus
*/
@@ -112,7 +94,7 @@ Loader {
}
}
component ReplyMessageAction: Kirigami.Action {
component ReplyMessageAction: QQC2.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
@@ -150,90 +132,70 @@ Loader {
}
}
Component {
id: regularMenu
headerContentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
QQC2.Menu {
id: menu
Instantiator {
model: root.nestedActions
delegate: QQC2.Menu {
id: menuItem
visible: modelData.visible
title: modelData.text
icon: modelData.icon
KirigamiComponents.Avatar {
source: root.author.avatarUrl
Instantiator {
model: modelData.children
delegate: QQC2.MenuItem {
text: modelData.text
icon.name: modelData.icon.name
onTriggered: modelData.trigger()
}
onObjectAdded: (index, object) => {
menuItem.insertItem(0, object);
}
}
}
onObjectAdded: (index, object) => {
object.visible = false;
menu.addMenu(object);
}
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Kirigami.Heading {
level: 4
text: root.author.htmlSafeDisplayName
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
QQC2.Label {
text: root.plainText
textFormat: Text.PlainText
elide: Text.ElideRight
onLinkActivated: RoomManager.resolveResource(link, "join")
Layout.fillWidth: true
}
}
}
Kirigami.Action {
visible: Kirigami.Settings.isMobile
displayComponent: RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: root.actions
DelegateChooser {
role: "separator"
DelegateChoice {
roleValue: true
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: Delegates.RoundedItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
QQC2.MenuSeparator {
visible: modelData.visible
}
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
DelegateChoice {
roleValue: false
QQC2.MenuItem {
visible: modelData.visible
action: modelData
onClicked: root.item.close()
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
root.item.close();
}
}
}
QQC2.Menu {
id: webshortcutmenu
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
icon.name: "search-symbolic"
property bool isVisible: webshortcutmodel.enabled && root.enableWebSearch
Component.onCompleted: {
webshortcutmenu.parent.visible = isVisible;
}
onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible
Instantiator {
model: WebShortcutModel {
id: webshortcutmodel
selectedText: root.selectedText.length > 0 ? root.selectedText : root.plainText
onOpenUrl: url => RoomManager.resolveResource(url.toString())
}
delegate: QQC2.MenuItem {
text: model.display
icon.name: model.decoration
onTriggered: webshortcutmodel.trigger(model.edit)
}
onObjectAdded: (index, object) => webshortcutmenu.insertItem(0, object)
}
QQC2.MenuSeparator {}
QQC2.MenuItem {
text: i18n("Configure Web Shortcuts...")
icon.name: "configure"
visible: !Controller.isFlatpak
onTriggered: webshortcutmodel.configureWebShortcuts()
}
}
}
}
/*
}
}
Component {
@@ -330,31 +292,6 @@ Loader {
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
root.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
@@ -422,5 +359,5 @@ Loader {
} else {
item.popup();
}
}
}*/
}

View File

@@ -19,65 +19,6 @@ RowLayout {
signal search
property Kirigami.Action exploreAction: Kirigami.Action {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
}
}
property Kirigami.Action chatAction: Kirigami.Action {
text: i18n("Find your friends")
icon.name: "list-add-user"
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
property Kirigami.Action roomAction: Kirigami.Action {
text: i18n("Create a Room")
icon.name: "system-users-symbolic"
onTriggered: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Create a Room")
});
}
shortcut: StandardKey.New
}
property Kirigami.Action spaceAction: Kirigami.Action {
text: i18n("Create a Space")
icon.name: "list-add"
onTriggered: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection,
isSpace: true,
title: i18nc("@title", "Create a Space")
}, {
title: i18nc("@title", "Create a Space")
});
}
}
property Kirigami.Action scanAction: Kirigami.Action {
text: i18n("Scan a QR Code")
icon.name: "view-barcode-qr"
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
connection: root.connection
}, {
title: i18nc("@title", "Scan a QR Code")
})
}
/**
* @brief Emitted when the text is changed in the search field.
*/
@@ -119,18 +60,13 @@ RowLayout {
Accessible.onPressAction: menuButton.action.trigger()
display: QQC2.AbstractButton.IconOnly
checkable: true
action: Kirigami.Action {
action: QQC2.Action {
text: i18nc("@action:button", "Show Menu")
icon.name: "application-menu-symbolic"
onTriggered: {
if (Kirigami.isMobile) {
const menu = mobileMenu.createObject();
menu.open();
} else {
const menu = desktopMenu.createObject(menuButton);
menu.closed.connect(menuButton.toggle);
menu.open();
}
const item = menu.createObject(menuButton);
item.closed.connect(menuButton.toggle);
item.open();
}
}
@@ -140,86 +76,70 @@ RowLayout {
}
Component {
id: desktopMenu
id: menu
QQC2.Menu {
x: mirrored ? parent.width - width : 0
y: parent ? parent.height : 0
modal: true
dim: false
QQC2.MenuItem {
text: i18n("Explore rooms")
icon.name: "compass"
onTriggered: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});
}
}
QQC2.MenuItem {
Accessible.onPressAction: action.triggered()
action: exploreAction
text: i18n("Find your friends")
icon.name: "list-add-user"
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Find your friends")
})
}
QQC2.MenuItem {
Accessible.onPressAction: action.triggered()
action: chatAction
text: i18n("Create a Room")
icon.name: "system-users-symbolic"
action: QQC2.Action {
shortcut: StandardKey.New
onTriggered: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection
}, {
title: i18nc("@title", "Create a Room")
});
}
}
}
QQC2.MenuItem {
Accessible.onPressAction: action.triggered()
action: roomAction
text: i18n("Create a Space")
icon.name: "list-add"
onTriggered: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
connection: root.connection,
isSpace: true,
title: i18nc("@title", "Create a Space")
}, {
title: i18nc("@title", "Create a Space")
});
}
}
QQC2.MenuItem {
Accessible.onPressAction: action.triggered()
action: spaceAction
}
QQC2.MenuItem {
Accessible.onPressAction: action.triggered()
action: scanAction
}
}
}
Component {
id: mobileMenu
Kirigami.OverlayDrawer {
id: menuRoot
edge: Qt.BottomEdge
parent: applicationWindow().overlay
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
ColumnLayout {
width: parent.width
spacing: 0
Kirigami.ListSectionHeader {
label: i18n("Create rooms and chats")
}
Delegates.RoundedItemDelegate {
action: exploreAction
onClicked: menuRoot.close()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
action: chatAction
onClicked: menuRoot.close()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
action: roomAction
onClicked: menuRoot.close()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
action: roomAction
onClicked: menuRoot.close()
Layout.fillWidth: true
}
Delegates.RoundedItemDelegate {
action: scanAction
onClicked: menuRoot.close()
Layout.fillWidth: true
}
text: i18n("Scan a QR Code")
icon.name: "view-barcode-qr"
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
connection: root.connection
}, {
title: i18nc("@title", "Scan a QR Code")
})
}
}
}

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