Compare commits

...

98 Commits

Author SHA1 Message Date
Tobias Fella
092e092e18 WIP 2026-02-06 05:02:58 -05:00
l10n daemon script
de7f2654f4 GIT_SILENT Sync po/docbooks with svn 2026-02-06 01:55:03 +00:00
l10n daemon script
ccf9ed66f8 GIT_SILENT Sync po/docbooks with svn 2026-02-04 01:55:11 +00:00
Azhar Momin
259c60f669 Fix barcode tooltip text
This was missed in e5a48bae01
2026-02-03 20:19:41 +05:30
l10n daemon script
2ca121ede9 GIT_SILENT Sync po/docbooks with svn 2026-02-03 01:50:48 +00:00
l10n daemon script
fba01c17ae GIT_SILENT Sync po/docbooks with svn 2026-02-02 01:46:05 +00:00
Joshua Goins
5de394b4b7 Don't scroll the timeline when reacting to messages
BUG: 515306
FIXED-IN: 25.12.2
2026-02-01 19:01:32 -05:00
Joshua Goins
edd64d9b8f Improve UserListModel performance and other preparations
In a future patch I want to add support for viewing banned/invited
users, and it's also been mentioned that UserListModel is quite slow
too.

The biggest cost is sorting the member list (power level and
alphabetically) and this happened in a few different ways:
* When the member list updated
* The user switches rooms
* Misc events such as the palette changing

But this was pretty inefficient, because internally Quotient::Room keeps
a list of members, and we kept re-sorting that same list. Our
connections were also too broad and despite having signals for members
joining and leaving we just reloaded the entire list anyway.

So my new solution is to keep the list persistently sorted in
NeoChatRoom, and reload that in UserListModel. This model also keeps
track of *all* members - including ones that left - which will be used
for the aforementioned feature. So UserFilterModel now filters out only
the joined members, and that will be configurable in the future.

I also added two new roles to UserListModel for membership and color
respectively (which makes some dead code useful again) and fixed us
overwriting the built-in Qt roles accidentally.
2026-02-01 17:43:53 -05:00
l10n daemon script
5c614e59f2 GIT_SILENT Sync po/docbooks with svn 2026-02-01 01:45:02 +00:00
Joshua Goins
853d48e768 Improve the structure of the welcome page slightly
I don't really like these pages in NeoChat much, there's only a few
buttons, and they really blend together. In an attempt to alleviate
this problem, I did the following:

* Added icons to the Login and Register actions, which does complement
the other buttons on this page.
* Removed the icons from the "Continue" and "Go back" buttons, which did
nothing but add confusing arrows.
* Moved the "Go back" button, fixed the capitalization and moved it to a
separate FormCard.
* Made it so the "Settings" button is only shown on the initial page, to
reduce the amount of UI clutter while logging in.
2026-01-31 13:18:23 -05:00
l10n daemon script
295ecf0f18 GIT_SILENT Sync po/docbooks with svn 2026-01-31 01:43:41 +00:00
l10n daemon script
7c0ab697d7 GIT_SILENT Sync po/docbooks with svn 2026-01-30 01:54:31 +00:00
l10n daemon script
75fceaccea GIT_SILENT Sync po/docbooks with svn 2026-01-29 01:55:50 +00:00
James Graham
275d221f75 Improve time handling in NeoChat
This introduces a new NeoChatDateTime object that wraps a QDateTime. The intent is that it can be passed to QML and has a series of functions that format the QDateTime into the various string representations we need.

This means we only have to send the single object to QML and then the correct string can be grabbed from there, simplifying the backend. It is also easy to add a new representation if needed as a function with a QString output and Q_PROPERTY can be added and then it will be available.
2026-01-28 16:16:40 +00:00
Joshua Goins
72416884d4 Add comment about push rule check in contextAwareNotificationCount()
I thought this was unnecessary as the push rules should take care of it
for us, but that's not entirely true. I added a comment to reflect this
reality.
2026-01-28 15:58:46 +01:00
Tobias Fella
537ce772af Remove leak in ChatDocumentHandler 2026-01-28 12:29:18 +00:00
Tobias Fella
37b8d8d813 Remove unused variables in linkpreviewertest 2026-01-28 12:29:18 +00:00
Tobias Fella
d64e22c270 Fix leak in actionstest 2026-01-28 12:29:18 +00:00
Tobias Fella
fa4533e757 Refactor ChatBarCacheTest 2026-01-28 12:29:18 +00:00
Tobias Fella
8b6f5447e1 Refactor NeoChatRoomTest 2026-01-28 12:29:18 +00:00
Tobias Fella
6dce1564b7 Fix memory leaks 2026-01-28 12:29:18 +00:00
Tobias Fella
7313386903 Enable lsan on CI 2026-01-28 12:29:18 +00:00
l10n daemon script
c0f5db7fd2 GIT_SILENT Sync po/docbooks with svn 2026-01-28 01:46:58 +00:00
Joshua Goins
551d827dee Use correct terminology when leaving space
BUG: 514888
FIXED-IN: 26.04
2026-01-27 12:05:42 -05:00
Joshua Goins
332a822996 Remove single tap to maximize code component
This is just more ergonomic (in my opinion) as you usually want to
select some text from a code block, instead of maximizing it. There's
already an easy-to-access button for maximizing if you want to.

BUG: 499048
FIXED-IN: 25.12.2
2026-01-27 12:05:22 -05:00
Joshua Goins
f145bbe8db Fix transparency blur not applying to the timeline anymore
We still had the default opaque background for RoomPage. I added a
comment too so it isn't removed accidentally in the future.

BUG: 513363
FIXED-IN: 25.12.2
2026-01-27 12:05:03 -05:00
l10n daemon script
381b119ad1 GIT_SILENT Sync po/docbooks with svn 2026-01-27 01:46:07 +00:00
Albert Astals Cid
b64dcdb004 GIT_SILENT Update Appstream for new release
(cherry picked from commit 4c4f406c41)
2026-01-27 02:13:43 +01:00
l10n daemon script
b33f1cf5e1 GIT_SILENT Sync po/docbooks with svn 2026-01-26 01:43:12 +00:00
James Graham
f22cafbce1 Revert "Improve time handling in NeoChat"
This reverts commit 92c58b0ea0.
2026-01-25 13:07:53 +00:00
James Graham
92c58b0ea0 Improve time handling in NeoChat
This introduces a new NeoChatDateTime object that wraps a QDateTime. The intent is that it can be passed to QML and has a series of functions that format the QDateTime into the various string representations we need.

This means we only have to send the single object to QML and then the correct string can be grabbed from there, simplifying the backend. It is also easy to add a new representation if needed as a function with a QString output and Q_PROPERTY can be added and then it will be available.
2026-01-25 13:04:58 +00:00
l10n daemon script
c797ecea3d GIT_SILENT Sync po/docbooks with svn 2026-01-25 01:44:45 +00:00
l10n daemon script
b65590a9b9 GIT_SILENT made messages (after extraction) 2026-01-25 00:44:24 +00:00
Joshua Goins
2bacbe7ac7 Improve the bottom mobile navigation bar
The previous set of actions seems like a random selection, how many
rooms is someone creating to be that important?

I have redone it to have way fewer actions, mostly notification and
settings.
2026-01-24 10:55:36 -05:00
Joshua Goins
74c12e89ea Improve other space button texts
Remove some extra verbiage, ensuring ellipses and so on.

CCBUG: 497044
2026-01-24 10:52:31 -05:00
Joshua Goins
02b95c921d Add ellipses to "Remove" button in space hierarchy
This can be somewhat unclear if NeoChat prompts you (it does!)

CCBUG: 497044
2026-01-24 10:52:31 -05:00
Joshua Goins
7f58ee3793 Improve space suggestions editing
This is confusing to users as there may be two "remove" icons, but one
of them is actually for suggestions.

BUG: 497044
FIXED-IN: 26.04
2026-01-24 10:52:31 -05:00
l10n daemon script
7312bf183d GIT_SILENT Sync po/docbooks with svn 2026-01-24 01:44:54 +00:00
Tobias Fella
40b7853338 Fix binding loop 2026-01-23 23:15:00 +01:00
Azhar Momin
ade5750550 Add a button to mark spaces as read
[BUG: 508122](https://bugs.kde.org/show_bug.cgi?id=508122)
2026-01-23 15:29:51 -05:00
l10n daemon script
fb8ee02e3b GIT_SILENT Sync po/docbooks with svn 2026-01-23 01:45:20 +00:00
Tobias Fella
8b27323488 Extend testing 2026-01-22 23:33:45 +00:00
Tobias Fella
96d24f5c3a Minor fixes to various models 2026-01-22 23:33:45 +00:00
Tobias Fella
45cee495a5 Adapt LineModel to being autotested 2026-01-22 23:33:45 +00:00
Tobias Fella
53dc9c1944 Use getter and setter for property in LiveLocationsModel 2026-01-22 23:33:45 +00:00
Tobias Fella
1fb215dae7 Add test that runs each model with a QAbstractItemModelTester 2026-01-22 23:33:45 +00:00
Tobias Fella
971875c8a2 PowerLevelModel: Use Qt::UserRole for role value
Otherwise it's equal to Qt::DecorationRole, which QAbstractItemModelTester expects to be convertible to certain types
2026-01-22 23:33:45 +00:00
Tobias Fella
9ad64b990d NotificationsModel: Don't crash if connection is nullptr
This can legitimately happen
2026-01-22 23:33:45 +00:00
Tobias Fella
1ceffe6a2e Start adapting to libquotient crypto api changes 2026-01-22 23:26:55 +00:00
l10n daemon script
9810b3dee0 GIT_SILENT Sync po/docbooks with svn 2026-01-22 02:00:17 +00:00
Tobias Fella
76954c162a NotificationsManager: Improve some comments
(as suggested by CLion)
2026-01-21 16:11:07 +00:00
Tobias Fella
0a7978f4f5 NotificationsManager: Improve function parameters 2026-01-21 16:11:07 +00:00
Tobias Fella
bf41e1083d NotificationsManager: Make a function static 2026-01-21 16:11:07 +00:00
Tobias Fella
593f772845 NotificationsManager: Minor refactoring 2026-01-21 16:11:07 +00:00
Tobias Fella
98a277ac63 const auto a few things 2026-01-21 16:11:07 +00:00
Tobias Fella
cfe5182a65 Remove redundant check 2026-01-21 16:11:07 +00:00
Joshua Goins
9ace01f74a Fix closeToYEnd check
The comparison operator was reversed, and this was seen with mark as
read being broken and buttons showing up at the wrong times.
2026-01-21 08:21:56 -05:00
l10n daemon script
4632a9f9bb GIT_SILENT Sync po/docbooks with svn 2026-01-21 01:49:34 +00:00
Azhar Momin
a92587cc50 Fix some typos 2026-01-20 17:42:51 +00:00
Tobias Fella
889b7dd2e6 Fix crash when logging out active connection 2026-01-20 14:40:57 +00:00
Tobias Fella
44fa196a26 Use QPointer to store room in WidgetModel 2026-01-20 14:05:30 +00:00
Tobias Fella
2bc8c6a379 Fix various qml warnings 2026-01-20 13:48:46 +00:00
l10n daemon script
3f1ba8d067 GIT_SILENT Sync po/docbooks with svn 2026-01-20 01:53:20 +00:00
Azhar Momin
a1c9b63d1e Fix emojis being too small in EmojiDelegate
BUG: 514170
2026-01-19 09:40:18 +00:00
l10n daemon script
73fdc72ce7 GIT_SILENT Sync po/docbooks with svn 2026-01-19 01:44:12 +00:00
Tobias Fella
08fc8be09c Cleanup README.md a bit 2026-01-18 22:51:55 +01:00
Carl Schwan
e5a48bae01 UserDetailDialog: Remove double QR code action
Now it's part of the header
2026-01-18 22:08:56 +01:00
Carl Schwan
581f5be410 UserDetailDialog: Improve consistency
Use same spacing and sizing as GroupChatDrawerHeader and add a QR code
to share the contact.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-01-18 21:58:34 +01:00
l10n daemon script
f4e857519b GIT_SILENT Sync po/docbooks with svn 2026-01-18 01:50:53 +00:00
Joshua Goins
93e932c09c Add hack to fix crash when sending long text reactions
This is some bug in Flow (that is really hard to debug, I can't get it
to exit at all) but we can work around it for a minor visual impact. It
seems to me allow the reaction list to become slightly larger, but
that's about it.

BUG: 504344
FIXED-IN: 25.12.2
2026-01-17 15:01:44 -05:00
Joshua Goins
3b8930c2bc Cleanup few remaining atYEnd usages in TimelineView
These were either mistakes or rebase errors, but we should be using
closeToYEnd here.
2026-01-17 13:49:40 -05:00
Joshua Goins
7e34570a05 Improve placeholder text for WidgetsPage
This shortens the text and adds an icon.
2026-01-17 13:49:29 -05:00
Joshua Goins
0b5de13c36 Improve Shared Location messaging
This changes the icon for the shared locations page, adds better
placeholder text and so on.
2026-01-17 13:49:29 -05:00
Joshua Goins
6eb2b2e739 Add hack to fix room sidebar not sticking to the top
This is similar to the TimelineView hacks, but this time its the header
item that's changing height as our room topics and such wrap.
2026-01-17 13:49:18 -05:00
Joshua Goins
be89362fdd Fix left padding for "Rooms" label in the room list
This now emulates the default Kirigami heading behavior now, with the
correct amount of padding.
2026-01-17 13:14:05 -05:00
Joshua Goins
e53c84d30c Fix Quick Switcher not being activatable by Enter/Return key 2026-01-17 13:13:52 -05:00
Joshua Goins
a90c26f566 Don't show the Share action for non-file messages
This only shares files, if you try it on anything else it crashes
NeoChat.
2026-01-17 13:13:43 -05:00
Darshan Phaldesai
c2ae5afa73 ReactionComponent: visual changes to make it look consistent 2026-01-17 12:19:46 -05:00
Joshua Goins
5f20a86b62 Reduce the amount of items in the account menu
The devices entry gone (not even Element has this) and so is the logout
action. The Switch account menu item is moved to the bottom so its
easier to access by mouse. The "Open Secret Backup" is moved under
Security settings where it lives next to the other crypto-related
settings.
2026-01-17 11:03:38 -05:00
l10n daemon script
f2d3c9706e GIT_SILENT Sync po/docbooks with svn 2026-01-17 01:44:32 +00:00
Joshua Goins
39de4d10e4 Add hack around the timeline never settling just right
This is due to some kind of bug in ListView that never resettles
properly for bottom-to-top views. This can arise when the pinned message
is loaded (because that squishes the view) or the window is resized
(because that also resizes the view.)

We can work around it by assuming the following:
1. The RoomPage knows the window is resizing because it gets its height
changed before TimelineView.
2. The first height change can be a marker to position the view at the
beginning.

This fixes the issue for me, I did the following in order to test this:
* Switch between many rooms, especially ones with a pinned message. Now
all of them start at the bottom as they should.
* Resize the window, ensure that if you scrolled it stays around that
position - otherwise it sticks at the bottom.
2026-01-16 18:34:05 -05:00
Joshua Goins
4c37dcf518 Improve reliability of restoring the last space and room (again)
I found that 50% of the time, NeoChat won't restore the last space but
instead get stuck at Home. Even worse, it will overwrite Home's last
opened room with the one from the space - resulting in really buggy
behavior.

The reason why this happens is partly due to the space hierarchy cache
(I think) but that's not the real problem in my opinion. During
setCurrentSpace, we needlessly update the last space & room config
despite us being the ones already reading it.

In addition to that I also refactored this code a bit to be more
consolidated and readable.
2026-01-16 18:14:07 -05:00
Joshua Goins
3c77711417 Add hack around atYEnd
This fixes the annoying "I just scrolled down to the bottom, how come
NeoChat doesn't think I did?"

From what I can tell this is also ListView bug (or something caused by
our style/Kirigami) that creates cases like contentY being -643.2 (for a
ListView of height 643) thus that's not "at Y's end". For our case
though, we don't care and can safely round it.
2026-01-16 17:07:03 -05:00
Yuri Chornoivan
136063bd37 Fix minor typo 2026-01-16 05:29:13 +02:00
l10n daemon script
730a9e97fd GIT_SILENT Sync po/docbooks with svn 2026-01-16 01:53:34 +00:00
Joshua Goins
d5260376d2 Support replying and editing messages directly from room search
There's two parts to making this work mainly:
1. Use getEvent instead of findInTimeline so the related event is
actually found.
2. Close the dialog once a reply relation is found, so you can easily
reply in the chat bar.
2026-01-15 20:13:50 -05:00
Joshua Goins
dc935e09b7 Use countedNotifications instead of our own calculations w/ DMs
This fixes an odd disconnect you can sometimes see when the notification
isn't an invite or a "direct chat notification", which conflicts with
what we use to control the tooltip and visibility.
2026-01-15 16:51:57 -05:00
Joshua Goins
5759f7d82b Add a way to view server support information
This is useful if your server has said information, for matrix.org
includes abuse and administrator e-mails.

See #707
2026-01-15 16:21:03 -05:00
Joshua Goins
ed4b77c184 Remove the three item hamburger menu, re-distribute remaining items
There are only three, somewhat odd menu items remaining in this menu.
(Two if you don't have a camera.)

* Find your Friends, which is already accessible in a few other places
and currently has dubious utility.
* Create a Room, which also is barely used and can be combined with the
Create Space button in the space drawer.
* Scan a QR code, which can be placed in the account menu. I know this
isn't the most ideal place, but I can't think of anything better at the
moment.
2026-01-15 16:18:57 -05:00
Tobias Fella
716ee2e494 When "always allow verifying devices" option is enabled, show a less confusing message in the devices page 2026-01-15 09:12:17 -05:00
Joshua Goins
c15860cac3 Give DelegateContextMenu an actual room
This allows me to hide the "Reply" action for read-only rooms. Don't ask
me how it even worked before, I don't know.
2026-01-15 09:00:35 -05:00
Joshua Goins
f5c991c55c Pass room through the model, not when creating the delegate
This is another thing that enables us to view multiple rooms in a single
timeline. Specifically, this improves the experience in room search
going across room versions and getting a correct readOnly status (for
hiding certain hover actions.)
2026-01-15 09:00:35 -05:00
Joshua Goins
41609749d8 Fix a crash when grabbing relationAuthor
There's a bug in how we're using this function in room search, but we
definitely don't want it to crash. The event is technically not in the
timeline, so we were dereferencing an invalid iterator or whatever.
2026-01-15 09:00:35 -05:00
Joshua Goins
644df80090 Don't show "Configure Web Shortcuts" if there are none
This prevents some weird edge cases where the Configure action is
visible, but nothing is actually searchable - like for images.
2026-01-15 09:00:23 -05:00
Joshua Goins
e3307326ef Close the message menu after selecting a quick reaction
And also ensure the "select an emoji" menu doesn't close the message
menu after *not* choosing an emoji, so it acts more like a submenu.
2026-01-15 09:00:13 -05:00
Joshua Goins
74d4e786d3 Clarify where reports are sent to
Contrary to popular belief (unfortunately) these reports are *only* sent
to your own server, which is then opaquely handled in some unknownable
way.

See #707
2026-01-15 09:00:02 -05:00
Joshua Goins
1e461658b8 Hide "Reply in Thread" message action if we don't have threads enabled 2026-01-15 08:16:41 -05:00
l10n daemon script
f305cb849f GIT_SILENT Sync po/docbooks with svn 2026-01-15 01:52:21 +00:00
160 changed files with 29337 additions and 21703 deletions

View File

@@ -43,3 +43,4 @@ Options:
per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True
enable-lsan: True

View File

@@ -25,15 +25,10 @@ Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
## Features
NeoChat aims to be a fully featured application for the Matrix specification. As such most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the fact that the Matrix spec is constantly
NeoChat aims to be a fully featured application for the Matrix specification. As such, most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the Matrix spec constantly
evolving, but the aim remains to provide eventual support for the entire spec.
Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:
- Polls - MSC3381
- Sticker Packs - MSC2545
- Location Events - MSC3488
## Get it
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
@@ -48,12 +43,12 @@ The best way to build KDE apps during development is to use `kdesrc-build`. The
the KDE community website's get involved section under [development](https://community.kde.org/Get_Involved/development). This
is primarily aimed at Linux development.
For Windows and Android [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
For Windows and Android, [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Running
Just start the executable in your preferred way - either from the build directory or from the installed location.
Start the executable in your preferred way either from the build directory or from the installed location.
## Tests
@@ -66,12 +61,12 @@ be complete.
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg)
Currently the number of tests is limited, but growing. If anyone wants to help improve this, those
Currently, the number of tests is limited but growing. If anyone wants to help improve this, those
contributions would be especially welcome.
## Contributing
As is the case throughout the KDE ecosystem contributions are welcome from all. The code base is managed in the
As is the case throughout the KDE ecosystem, contributions are welcome from all. The code base is managed in the
[NeoChat repository](https://invent.kde.org/network/neochat) of the KDE Gitlab instance.
- [Code of Conduct](https://kde.org/code-of-conduct)
@@ -86,7 +81,7 @@ The best place to reach the maintainers is on the KDE Matrix instance in the Neo
## Acknowledgement
NeoChat utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
NeoChat uses [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).

View File

@@ -11,7 +11,7 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
TEST_NAME neochatroomtest
)
@@ -41,7 +41,7 @@ ecm_add_test(
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
TEST_NAME chatbarcachetest
)
@@ -104,3 +104,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)
ecm_add_test(
modeltest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
TEST_NAME modeltest
)

View File

@@ -88,7 +88,7 @@ void ActionsTest::testActions()
QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache();
auto cache = new ChatBarCache(this);
cache->setText(command);
auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result));

View File

@@ -11,9 +11,13 @@
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "chatbarcache.h"
#include "neochatroom.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
@@ -24,7 +28,9 @@ class ChatBarCacheTest : public QObject
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
NeoChatRoom *room = nullptr;
Server server;
QString eventId;
private Q_SLOTS:
void initTestCase();
@@ -40,8 +46,31 @@ private Q_SLOTS:
void ChatBarCacheTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
eventId = server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
server.joinUser(room->id(), u"@foo:server.com"_s);
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
}
void ChatBarCacheTest::empty()
@@ -60,8 +89,9 @@ void ChatBarCacheTest::empty()
void ChatBarCacheTest::noRoom()
{
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(u"$153456789:example.org"_s);
chatBarCache->setReplyId(eventId);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
@@ -75,9 +105,10 @@ void ChatBarCacheTest::noRoom()
void ChatBarCacheTest::badParent()
{
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(u"$153456789:example.org"_s);
chatBarCache->setReplyId(eventId);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
@@ -94,15 +125,15 @@ void ChatBarCacheTest::reply()
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
chatBarCache->setReplyId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
}
@@ -112,22 +143,26 @@ void ChatBarCacheTest::replyMissingUser()
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
chatBarCache->setReplyId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room
room->syncNewEvents(u"test-min-sync-extra-sync.json"_s);
QSignalSpy syncSpy(connection, &Connection::syncDone);
server.sendStateEvent(room->id(), u"m.room.member"_s, u"@foo:server.com"_s, {{u"membership"_s, u"leave"_s}});
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
@@ -139,19 +174,19 @@ void ChatBarCacheTest::edit()
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, QString(u"$153456789:example.org"_s));
QCOMPARE(newEventId, eventId);
});
chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setEditId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->editId(), eventId);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -159,7 +194,7 @@ void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setEditId(eventId);
chatBarCache->setAttachmentPath(u"some/path"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);

View File

@@ -1,20 +0,0 @@
{
"state": {
"events": [
{
"content": {
"membership": "leave"
},
"event_id": "$1432735824666PhrSA:example.org",
"origin_server_ts": 1432735824666,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$143273582443PhrSn:example.org"
}
}
]
}
}

View File

@@ -40,7 +40,6 @@ private Q_SLOTS:
void nullSingleLineDisplayName();
void time();
void nullTime();
void timeString();
void highlighted();
void nullHighlighted();
void hidden();
@@ -100,12 +99,12 @@ void EventHandlerTest::time()
{
const auto event = room->messageEvents().at(0).get();
QCOMPARE(EventHandler::time(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
QCOMPARE(EventHandler::dateTime(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::time(room, pendingIt->event(), true), pendingIt->lastUpdated());
QCOMPARE(EventHandler::dateTime(room, pendingIt->event(), true), pendingIt->lastUpdated());
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
@@ -114,40 +113,10 @@ void EventHandlerTest::time()
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr.");
QCOMPARE(EventHandler::time(nullptr, nullptr), QDateTime());
QCOMPARE(EventHandler::dateTime(nullptr, nullptr), QDateTime());
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
QCOMPARE(EventHandler::time(room, nullptr), QDateTime());
}
void EventHandlerTest::timeString()
{
const auto event = room->messageEvents().at(0).get();
KFormat format;
QCOMPARE(EventHandler::timeString(room, event, false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s),
QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::LocalTime)).toString(u"hh:mm"_s));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::ShortFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::ShortFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::LongFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::LongFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::LongFormat));
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
QCOMPARE(EventHandler::dateTime(room, nullptr), QDateTime());
}
void EventHandlerTest::highlighted()

View File

@@ -19,13 +19,7 @@ class LinkPreviewerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
@@ -36,12 +30,6 @@ private Q_SLOTS:
void linkPreviewsReject();
};
void LinkPreviewerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:example.org"_s);
room = new TestUtils::TestRoom(connection, u"!test:example.org"_s);
}
void LinkPreviewerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("inputString");

620
autotests/modeltest.cpp Normal file
View File

@@ -0,0 +1,620 @@
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QAbstractItemModelTester>
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <QVariantList>
#include <Quotient/connection.h>
#include "accountmanager.h"
#include "contentprovider.h"
#include "enums/powerlevel.h"
#include "enums/roomsortparameter.h"
#include "models/accountemoticonmodel.h"
#include "models/actionsmodel.h"
#include "models/commonroomsmodel.h"
#include "models/completionmodel.h"
#include "models/completionproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/eventmessagecontentmodel.h"
#include "models/imagepacksmodel.h"
#include "models/linemodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/messagecontentfiltermodel.h"
#include "models/notificationsmodel.h"
#include "models/permissionsmodel.h"
#include "models/pinnedmessagemodel.h"
#include "models/pollanswermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/readmarkermodel.h"
#include "models/roomsortparametermodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/spacechildrenmodel.h"
#include "models/spacechildsortfiltermodel.h"
#include "models/statefiltermodel.h"
#include "models/statekeysmodel.h"
#include "models/statemodel.h"
#include "models/stickermodel.h"
#include "models/threadmodel.h"
#include "models/threepidmodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/webshortcutmodel.h"
#include "neochatroom.h"
#include "pollhandler.h"
#include "roommanager.h"
#include "server.h"
using namespace Quotient;
// TODO: Add data to all models as relevant.
// Performs basic tests on all models in NeoChat
// When adding a new test, create the model first, then the tester, then initialize the model (e.g., setConnection and setRoom).
// That way, the models are also tested for whether they can handle having no connection etc.
class ModelTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
NeoChatRoom *room = nullptr;
QString eventId;
Server server;
private Q_SLOTS:
void initTestCase();
void testRoomTreeModel();
void testMessageContentModel();
void testEventMessageContentModel();
void testThreadModel();
void testThreadFetchModel();
void testThreadChatBarModel();
void testReactionModel();
void testPollAnswerModel();
void testLineModel();
void testSpaceChildrenModel();
void testItineraryModel();
void testPublicRoomListModel();
void testMessageFilterModel();
void testThreePIdModel();
void testMediaMessageFilterModel();
void testWebshortcutModel();
void testTimelineMessageModel();
void testReadMarkerModel();
void testSearchModel();
void testStateModel();
void testTimelineModel();
void testStateKeysModel();
void testPinnedMessageModel();
void testUserListModel();
void testStickerModel();
void testPowerLevelModel();
void testImagePacksModel();
void testCompletionModel();
void testRoomListModel();
void testCommonRoomsModel();
void testNotificationsModel();
void testLocationsModel();
void testServerListModel();
void testEmojiModel();
void testCustomEmojiModel();
void testPushRuleModel();
void testActionsModel();
void testDevicesModel();
void testUserDirectoryListModel();
void testAccountEmoticonModel();
void testPermissionsModel();
void testLiveLocationsModel();
void testRoomSortParameterModel();
void testSortFilterRoomTreeModel();
void testSortFilterSpaceListModel();
void testSortFilterRoomListModel();
void testSpaceChildSortFilterModel();
void testStateFilterModel();
void testMessageContentFilterModel();
void testUserFilterModel();
void testEmoticonFilterModel();
void testDevicesProxyModel();
void testCompletionProxyModel();
};
void ModelTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
eventId = server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"asdf"_s},
{u"m.relates_to"_s,
QJsonObject{
{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s},
{u"is_falling_back"_s, true},
{u"m.in_reply_to"_s, QJsonObject{{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s}}},
{u"rel_type"_s, u"m.thread"_s},
}},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void ModelTest::testRoomTreeModel()
{
auto roomTreeModel = new RoomTreeModel(this);
auto tester = new QAbstractItemModelTester(roomTreeModel, roomTreeModel);
tester->setUseFetchMore(true);
roomTreeModel->setConnection(connection);
}
void ModelTest::testMessageContentModel()
{
auto contentModel = std::make_unique<MessageContentModel>(room, nullptr, eventId);
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
tester->setUseFetchMore(true);
}
void ModelTest::testEventMessageContentModel()
{
auto model = std::make_unique<EventMessageContentModel>(room, eventId);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testThreadModel()
{
auto model = new ThreadModel(eventId, room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadFetchModel()
{
auto model = new ThreadFetchModel(new ThreadModel(eventId, room));
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadChatBarModel()
{
auto model = new ThreadChatBarModel(new ThreadModel(eventId, room), room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testReactionModel()
{
auto messageContentModel = std::make_unique<MessageContentModel>(room);
auto model = new ReactionModel(messageContentModel.get(), eventId, room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testPollAnswerModel()
{
auto handler = std::make_unique<PollHandler>(room, eventId);
auto model = new PollAnswerModel(handler.get());
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testLineModel()
{
auto model = new LineModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto document = new QTextDocument(this);
model->setDocument(document);
document->setPlainText(u"foo\nbar\n\nbaz"_s);
}
void ModelTest::testSpaceChildrenModel()
{
auto model = new SpaceChildrenModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSpace(room);
}
void ModelTest::testItineraryModel()
{
auto model = new ItineraryModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testPublicRoomListModel()
{
auto model = new PublicRoomListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testMessageFilterModel()
{
auto timelineModel = new TimelineModel(this);
auto model = new MessageFilterModel(this, timelineModel);
auto tester = new QAbstractItemModelTester(model, model);
timelineModel->setRoom(room);
tester->setUseFetchMore(true);
}
void ModelTest::testThreePIdModel()
{
auto model = new ThreePIdModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testMediaMessageFilterModel()
{
auto timelineModel = new TimelineModel(this);
auto messageFilterModel = new MessageFilterModel(this, timelineModel);
auto model = new MediaMessageFilterModel(this, messageFilterModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
timelineModel->setRoom(room);
}
void ModelTest::testWebshortcutModel()
{
auto model = new WebShortcutModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSelectedText(u"Foo"_s);
}
void ModelTest::testTimelineMessageModel()
{
auto model = new TimelineMessageModel();
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testReadMarkerModel()
{
auto model = std::make_unique<ReadMarkerModel>(eventId, room);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testSearchModel()
{
auto model = new SearchModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSearchText(u"foo"_s);
model->setRoom(room);
}
void ModelTest::testStateModel()
{
auto model = new StateModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testTimelineModel()
{
auto model = new TimelineModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testStateKeysModel()
{
auto model = new StateKeysModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setEventType(u"m.room.member"_s);
model->setRoom(room);
}
void ModelTest::testPinnedMessageModel()
{
auto model = new PinnedMessageModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testUserListModel()
{
auto model = new UserListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testStickerModel()
{
auto model = new StickerModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setPackIndex(0);
model->setRoom(room);
auto imagePacksModel = new ImagePacksModel(this);
model->setModel(imagePacksModel);
imagePacksModel->setRoom(room);
imagePacksModel->setShowEmoticons(true);
imagePacksModel->setShowStickers(true);
}
void ModelTest::testPowerLevelModel()
{
auto model = new PowerLevelModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testImagePacksModel()
{
auto model = new ImagePacksModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
model->setShowEmoticons(true);
model->setShowStickers(true);
}
void ModelTest::testCompletionModel()
{
auto model = new CompletionModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
model->setAutoCompletionType(CompletionModel::Room);
model->setText(u"foo"_s, u"#foo"_s);
auto roomListModel = new RoomListModel(this);
roomListModel->setConnection(connection);
model->setRoomListModel(roomListModel);
}
void ModelTest::testRoomListModel()
{
auto model = new RoomListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testCommonRoomsModel()
{
auto model = new CommonRoomsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setUserId(u"@user:example.com"_s);
}
void ModelTest::testNotificationsModel()
{
auto model = new NotificationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testLocationsModel()
{
auto model = new LocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testServerListModel()
{
auto model = new ServerListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testEmojiModel()
{
auto tester = new QAbstractItemModelTester(&EmojiModel::instance(), &EmojiModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testCustomEmojiModel()
{
auto tester = new QAbstractItemModelTester(&CustomEmojiModel::instance(), &CustomEmojiModel::instance());
tester->setUseFetchMore(true);
CustomEmojiModel::instance().setConnection(connection);
}
void ModelTest::testPushRuleModel()
{
auto model = new PushRuleModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testActionsModel()
{
auto tester = new QAbstractItemModelTester(&ActionsModel::instance(), &ActionsModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testDevicesModel()
{
auto model = new DevicesModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testUserDirectoryListModel()
{
auto model = new UserDirectoryListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setSearchText(u"foo"_s);
}
void ModelTest::testAccountEmoticonModel()
{
auto model = new AccountEmoticonModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testPermissionsModel()
{
auto model = new PermissionsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testLiveLocationsModel()
{
auto model = new LiveLocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testRoomSortParameterModel()
{
auto model = new RoomSortParameterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testSortFilterRoomTreeModel()
{
auto sourceModel = new RoomTreeModel(this);
auto model = new SortFilterRoomTreeModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterSpaceListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterSpaceListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterRoomListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterRoomListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSpaceChildSortFilterModel()
{
auto model = new SpaceChildSortFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto spaceChildrenModel = new SpaceChildrenModel(this);
model->setSourceModel(spaceChildrenModel);
spaceChildrenModel->setSpace(nullptr);
}
void ModelTest::testStateFilterModel()
{
auto model = new StateFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto stateModel = new StateModel(this);
model->setSourceModel(stateModel);
stateModel->setRoom(room);
}
void ModelTest::testMessageContentFilterModel()
{
auto model = new MessageContentFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(ContentProvider::self().contentModelForEvent(room, eventId));
}
void ModelTest::testUserFilterModel()
{
auto model = new UserFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto userListModel = new UserListModel(this);
model->setSourceModel(userListModel);
userListModel->setRoom(room);
}
void ModelTest::testEmoticonFilterModel()
{
auto model = new EmoticonFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto accountEmoticonModel = new AccountEmoticonModel(this);
model->setSourceModel(accountEmoticonModel);
model->setShowEmojis(true);
model->setShowStickers(true);
accountEmoticonModel->setConnection(connection);
}
void ModelTest::testDevicesProxyModel()
{
auto model = new DevicesProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto devicesModel = new DevicesModel(this);
model->setSourceModel(devicesModel);
devicesModel->setConnection(dynamic_cast<NeoChatConnection *>(connection));
}
void ModelTest::testCompletionProxyModel()
{
auto model = new CompletionProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(&EmojiModel::instance());
}
QTEST_MAIN(ModelTest)
#include "modeltest.moc"

View File

@@ -9,6 +9,10 @@
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
@@ -18,7 +22,8 @@ class NeoChatRoomTest : public QObject {
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
NeoChatRoom *room = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
@@ -27,8 +32,27 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void NeoChatRoomTest::eventTest()

View File

@@ -127,7 +127,7 @@ void Server::start()
qFatal() << "Server failed to listen on a port.";
return;
} else {
qWarning() << "Server listening";
qInfo() << "Server listening";
}
}
@@ -203,6 +203,25 @@ QString Server::sendEvent(const QString &roomId, const QString &eventType, const
return eventId;
}
QString Server::sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
const auto json = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"state_key"_s, stateKey}};
changes.events += Changes::Event{
.fullJson = json,
};
changes.stateEvents += Changes::Event{.fullJson = json};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
@@ -334,6 +353,18 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &state : change.stateEvents) {
const auto &roomId = state.fullJson[u"room_id"_s].toString();
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[roomId][u"state"_s][u"events"_s].toArray();
stateEvents.append(state.fullJson);
auto room = joinRooms[roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[roomId] = room;
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
@@ -366,6 +397,5 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
}

View File

@@ -35,6 +35,7 @@ struct Changes {
QJsonObject fullJson;
};
QList<Event> events;
QList<Event> stateEvents;
};
struct RoomData {
@@ -67,6 +68,7 @@ public:
*/
QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
QString sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content);
private:
QHttpServer m_server;

View File

@@ -193,6 +193,7 @@
<li xml:lang="ar">التصويت - MSC3381</li>
<li xml:lang="ca">Votacions - MSC3381</li>
<li xml:lang="ca-valencia">Votacions - MSC3381</li>
<li xml:lang="de">Umfragen MSC3381</li>
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="eo">Enketoj - MSC3381</li>
@@ -227,6 +228,7 @@
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
<li xml:lang="de">Sticker-Pakete MSC2545</li>
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
@@ -487,6 +489,7 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="25.12.2" date="2026-02-05"/>
<release version="25.12.1" date="2026-01-08"/>
<release version="25.12.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/>

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

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

@@ -37,6 +37,8 @@ qt_add_library(neochat STATIC
texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -107,6 +109,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES
QtCore
QtQuick

View File

@@ -92,7 +92,9 @@ void NotificationsModel::setConnection(NeoChatConnection *connection)
void NotificationsModel::loadData()
{
Q_ASSERT(m_connection);
if (!m_connection) {
return;
}
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
return;
}

View File

@@ -38,7 +38,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{
}
void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection)
void NotificationsManager::handleNotifications(const QPointer<NeoChatConnection> &connection)
{
if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) {
startNotificationJob(connection);
@@ -68,7 +68,7 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
}
}
void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnection> &connection, const GetNotificationsJob *job, const bool initialization)
{
if (!job || !connection || !connection->isLoggedIn()) {
return;
@@ -82,8 +82,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
if (!m_initialTimestamp.contains(connectionId)) {
m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong();
} else {
qint64 timestamp = notification["ts"_L1].toVariant().toLongLong();
if (timestamp > m_initialTimestamp[connectionId]) {
if (const auto timestamp = notification["ts"_L1].toVariant().toLongLong(); timestamp > m_initialTimestamp[connectionId]) {
m_initialTimestamp[connectionId] = timestamp;
}
}
@@ -160,29 +159,29 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
}
}
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification)
{
if (connection == nullptr || !connection->isLoggedIn()) {
return false;
}
auto room = connection->room(notification["room_id"_L1].toString());
const auto room = connection->room(notification["room_id"_L1].toString());
if (room == nullptr) {
return false;
}
// If the room is the current room and the application is active the notification
// If the room is the current room and the application is active, the notification
// should not be shown.
// This is setup so that if the application is inactive the notification will
// This is set up so that if the application is inactive, the notification will
// always be posted, even if the room is the current room.
bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
if (RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
&& QGuiApplication::applicationState() == Qt::ApplicationActive) {
return false;
}
// If the notification timestamp is earlier than the initial timestamp assume
// If the notification timestamp is earlier than the initial timestamp, assume
// the notification is old and shouldn't be posted.
qint64 timestamp = notification["ts"_L1].toDouble();
const auto timestamp = notification["ts"_L1].toDouble();
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
return false;
}
@@ -199,7 +198,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
const bool canReply,
qint64 timestamp)
{
const QString roomId = room->id();
@@ -271,10 +270,8 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
if (NeoChatConfig::rejectUnknownInvites()) {
auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, room] {
QJsonObject replyData = job->jsonData();
if (replyData.contains(u"joined"_s)) {
const bool inAnyOfOurRooms = !replyData["joined"_L1].toArray().isEmpty();
if (inAnyOfOurRooms) {
if (QJsonObject replyData = job->jsonData(); replyData.contains(u"joined"_s)) {
if (!replyData["joined"_L1].toArray().isEmpty()) {
doPostInviteNotification(room);
} else {
room->forget();
@@ -286,7 +283,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
}
}
void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom> &room)
{
const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id());
if (roomMemberEvent == nullptr) {
@@ -295,18 +292,18 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image;
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
if (!room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else {
qWarning() << "using this room's avatar";
avatar_image = room->avatar(128);
}
KNotification *notification = new KNotification(u"invite"_s);
const auto notification = new KNotification(u"invite"_s);
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
notification->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
const auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) {
return;
@@ -367,11 +364,9 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
{
const auto json = QJsonDocument::fromJson(message).object();
const auto type = json["notification"_L1]["type"_L1].toString();
// the only two types of push notifications we support right now
if (type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
auto notification = new KNotification("message"_L1);
if (const auto type = json["notification"_L1]["type"_L1].toString(); type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
const auto notification = new KNotification("message"_L1);
const auto sender = json["notification"_L1]["sender_display_name"_L1].toString();
const auto roomName = json["notification"_L1]["room_name"_L1].toString();
@@ -391,13 +386,13 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
#ifdef HAVE_KIO
auto openAction = notification->addAction(i18n("Open NeoChat"));
const auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, notification, [=]() {
QString properId = roomId;
properId = properId.replace(u"#"_s, QString());
properId = properId.replace(u"!"_s, QString());
auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s));
const auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s));
job->setUrls({QUrl::fromUserInput(u"matrix:r/%1"_s.arg(properId))});
job->start();
});
@@ -428,13 +423,12 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
QBrush brush(icon.scaledToHeight(biggestDimension));
const QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
if (room) {
if (const auto roomAvatar = room->avatar(imageRect.width(), imageRect.height()); !roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);

View File

@@ -58,7 +58,7 @@ public:
/**
* @brief Handle the notifications for the given connection.
*/
void handleNotifications(QPointer<NeoChatConnection> connection);
void handleNotifications(const QPointer<NeoChatConnection> &connection);
private:
QHash<QString, qint64> m_initialTimestamp;
@@ -67,8 +67,8 @@ private:
QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection);
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
static QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification);
void postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
@@ -77,7 +77,7 @@ private:
bool canReply,
qint64 timestamp);
void doPostInviteNotification(QPointer<NeoChatRoom> room);
void doPostInviteNotification(const QPointer<NeoChatRoom> &room);
QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
@@ -85,5 +85,5 @@ private:
bool permissionAsked = false;
private Q_SLOTS:
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
void processNotificationJob(const QPointer<NeoChatConnection> &connection, const Quotient::GetNotificationsJob *job, bool initialization);
};

View File

@@ -5,6 +5,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtMultimedia
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -18,6 +19,10 @@ KirigamiComponents.ConvergentContextMenu {
required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window
data: MediaDevices {
id: devices
}
Kirigami.Action {
text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
@@ -33,12 +38,14 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
text: i18nc("@action:inmenu", "Scan a QR Code")
icon.name: "document-scan-symbolic"
visible: devices.videoInputs.length > 0
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
connection: root.connection
}) as Kirigami.Dialog).open();
}, {
title: i18nc("@title", "Scan a QR Code")
})
}
Kirigami.Action {
@@ -55,14 +62,6 @@ KirigamiComponents.ConvergentContextMenu {
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Devices")
icon.name: "computer-symbolic"
onTriggered: {
NeoChatSettingsView.open('devices');
}
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Developer Tools")
icon.name: "tools"
@@ -76,14 +75,6 @@ KirigamiComponents.ConvergentContextMenu {
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock"
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup")
})
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low"
@@ -103,10 +94,25 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Logout…")
icon.name: "im-kick-user"
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
text: i18nc("@action:inmenu Open support dialog", "Support")
icon.name: "help-contents-symbolic"
onTriggered: {
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection,
}).open();
}
}
Kirigami.Action {
separator: true
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection
}) as Kirigami.Dialog).open()
}) as Kirigami.Dialog).open();
}
}

View File

@@ -61,10 +61,10 @@ Kirigami.Dialog {
}
onClicked: {
root.close();
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
title: i18nc("@title:window", "Login")
});
root.close();
}
Keys.onUpPressed: {
accountView.currentIndex = accountView.count - 1;

View File

@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
property NeochatRoomMember author
/**
* @brief The timestamp of the message.
* @brief The timestamp of the event as a NeoChatDateTime.
*/
property var time
required property NeoChatDateTime dateTime
/**
* @brief The code text to show.
@@ -64,7 +64,7 @@ Components.AbstractMaximizeComponent {
}
QQC2.Label {
id: dateTimeLabel
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
text: root.dateTime.relativeDateTime
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
}
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
id: repeater
model: LineModel {
id: lineModel
document: codeText.textDocument
Component.onCompleted: setDocument(codeText.textDocument)
}
delegate: QQC2.Label {
id: label

View File

@@ -13,7 +13,7 @@ Kirigami.PromptDialog {
required property NeoChatRoom room
title: i18nc("@title:dialog", "Confirm Leaving Room")
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
dialogType: Kirigami.PromptDialog.Warning

View File

@@ -39,7 +39,7 @@ Kirigami.Page {
icon.name: "document-edit"
onTriggered: {
root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text);
root.closeDialog();
root.Kirigami.PageStack.closeDialog();
}
enabled: QmlUtils.isValidJson(sourceTextArea.text)
}
@@ -85,7 +85,7 @@ Kirigami.Page {
id: repeater
model: LineModel {
id: lineModel
document: sourceTextArea.textDocument
Component.onCompleted: setDocument(sourceTextArea.textDocument)
}
delegate: QQC2.Label {
id: label

View File

@@ -72,9 +72,9 @@ Kirigami.Dialog {
return null;
}
if (isAlias()) {
return root.connection.roomByAlias(text);
return root.connection.roomByAlias(text) as NeoChatRoom;
} else {
return root.connection.room(text);
return root.connection.room(text) as NeoChatRoom;
}
}

View File

@@ -103,7 +103,7 @@ Kirigami.Page {
id: repeater
model: LineModel {
id: lineModel
document: sourceTextArea.textDocument
Component.onCompleted: setDocument(sourceTextArea.textDocument)
}
delegate: QQC2.Label {
id: label

View File

@@ -55,10 +55,10 @@ Kirigami.Page {
formats: Prison.Format.QRCode | Prison.Format.Aztec
onResultChanged: {
if (result.text.length > 0 && result.text != scanner.previousText) {
root.Kirigami.PageStack.closeDialog();
RoomManager.resolveResource(result.text, "qr");
scanner.previousText = result.text;
}
root.closeDialog();
}
videoSink: viewFinder.videoSink
}

View File

@@ -22,7 +22,7 @@ Kirigami.SearchDialog {
}
onAccepted: if (currentItem) {
(currentItem as QQC2.ItemDelegate).clicked();
(root.currentItem as RoomDelegate).clicked();
}
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
@@ -13,6 +14,8 @@ Kirigami.Page {
required property string placeholder
required property string actionText
required property string icon
required property bool reporting
required property NeoChatConnection connection
signal accepted(reason: string)
@@ -21,6 +24,15 @@ Kirigami.Page {
topPadding: 0
bottomPadding: 0
header: Kirigami.InlineMessage {
showCloseButton: false
visible: root.reporting
type: Kirigami.MessageType.Information
position: Kirigami.InlineMessage.Position.Header
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
}
QQC2.TextArea {
id: reason
placeholderText: root.placeholder

View File

@@ -75,6 +75,14 @@ Kirigami.Page {
focus: true
padding: 0
background: null // This needs to stay null, because of transparency blur
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
actions: [
Kirigami.Action {
id: jitsiMeetingAction
@@ -349,8 +357,9 @@ Kirigami.Page {
});
}
function onShowDelegateMenu(parent: QtObject, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(parent, {
room: room,
author: author,
eventId: eventId,
plainText: plainText,
@@ -373,10 +382,10 @@ Kirigami.Page {
popup.open();
}
function onShowMaximizedCode(author, time, codeText, language) {
function onShowMaximizedCode(author, dateTime, codeText, language) {
(Qt.createComponent('org.kde.neochat', 'CodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
author: author,
time: time,
dateTime: dateTime,
codeText: codeText,
language: language
}) as CodeMaximizeComponent).open();

View File

@@ -27,7 +27,7 @@ Kirigami.Page {
QQC2.Action {
shortcut: 'Escape'
onTriggered: root.closeDialog()
onTriggered: Kirigami.PageStack.closeDialog()
}
Notification {
@@ -53,20 +53,16 @@ Kirigami.Page {
model: root.model
anchors.fill: parent
onStateChanged: {
root.Kirigami.PageStack.closeDialog();
if (state === Purpose.PurposeJobController.Finished) {
if (jobView.job?.output?.url?.length > 0) {
sharingSuccess.text = i18nc("@info", "Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
sharingSuccess.sendEvent();
Clipboard.saveText(jobView.job.output.url);
}
root.closeDialog();
} else if (state === Purpose.PurposeJobController.Error) {
// Show failure notification
sharingFailed.sendEvent();
root.closeDialog();
} else if (state === Purpose.PurposeJobController.Cancelled) {
// Do nothing
root.closeDialog();
}
}
}

View File

@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// 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.formcard as FormCard
import org.kde.neochat
Kirigami.Dialog {
id: root
required property NeoChatConnection connection
readonly property SupportController supportController: SupportController {
connection: root.connection
}
readonly property bool hasSupportResources: supportController.supportPage.length > 0 && supportController.contacts.length > 0
title: i18nc("@title Support information", "Support")
width: Math.min(Kirigami.Units.gridUnit * 30, QQC2.ApplicationWindow.window.width)
ColumnLayout {
spacing: 0
FormCard.FormTextDelegate {
id: explanationTextDelegate
text: root.hasSupportResources ?
i18nc("@info:label %1 is the domain of the server", "Official support resources provided by %1:", root.connection.domain)
: i18nc("@info:label %1 is the domain of the server", "%1 has no support resources.", root.connection.domain)
}
FormCard.FormDelegateSeparator {
above: explanationTextDelegate
below: openSupportPageDelegate
visible: openSupportPageDelegate.visible
}
FormCard.FormLinkDelegate {
id: openSupportPageDelegate
icon.name: "help-contents-symbolic"
text: i18nc("@action:button Open support webpage", "Open Support")
url: root.supportController.supportPage
visible: root.supportController.supportPage.length > 0
}
FormCard.FormDelegateSeparator {
above: openSupportPageDelegate
visible: root.supportController.contacts.length > 0
}
Repeater {
model: root.supportController.contacts
delegate: FormCard.AbstractFormDelegate {
id: contactDelegate
required property string role
required property string matrixId
required property string emailAddress
background: null
Layout.fillWidth: true
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
source: "user"
}
QQC2.Label {
text: {
// Translate known keys
if (contactDelegate.role === "m.role.admin") {
return i18nc("@info:label Adminstrator contact", "Admin")
} else if (contactDelegate.role === "m.role.security") {
return i18nc("@info:label Security contact", "Security")
}
return contactDelegate.role;
}
elide: Text.ElideRight
Layout.fillWidth: true
}
QQC2.ToolButton {
visible: contactDelegate.matrixId.length > 0
icon.name: "document-send-symbolic"
onClicked: {
root.close();
root.connection.requestDirectChat(contactDelegate.matrixId);
}
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is a Matrix ID", "Contact via Matrix (%1)", contactDelegate.matrixId)
}
QQC2.ToolButton {
visible: contactDelegate.emailAddress.length > 0
icon.name: "mail-sent-symbolic"
onClicked: Qt.openUrlExternally("mailto:%1".arg(contactDelegate.emailAddress))
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is an e-mail address", "Contact via e-mail (%1)", contactDelegate.emailAddress)
}
}
}
}
}
}

View File

@@ -13,7 +13,7 @@ FormCard.FormCardPage {
property bool processing: false
title: i18nc("@title:window", "Load your encrypted messages")
title: i18nc("@title:window", "Manage Secret Backup")
topPadding: Kirigami.Units.gridUnit
leftPadding: 0

View File

@@ -39,6 +39,7 @@ Kirigami.Dialog {
readonly property bool hasMutualRooms: root.model.count > 0
readonly property bool isRoomProfile: root.room
readonly property string shareUrl: "https://matrix.to/#/" + root.user.id
readonly property string displayName: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
leftPadding: Kirigami.Units.largeSpacing * 2
rightPadding: Kirigami.Units.largeSpacing * 2
@@ -64,10 +65,11 @@ Kirigami.Dialog {
KirigamiComponents.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
name: root.displayName
source: {
if (root.room) {
return root.room.member(root.user.id).avatarUrl;
@@ -80,27 +82,29 @@ Kirigami.Dialog {
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Kirigami.Heading {
level: 1
Layout.fillWidth: true
font.bold: true
clip: true // Intentional to limit insane Unicode in display names
elide: Text.ElideRight
wrapMode: Text.NoWrap
text: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
text: root.displayName
textFormat: Text.PlainText
Layout.fillWidth: true
}
Kirigami.SelectableLabel {
id: idLabel
textFormat: TextEdit.PlainText
text: idLabelTextMetrics.elidedText
color: Kirigami.Theme.disabledTextColor
font: Kirigami.Theme.smallFont
Layout.fillWidth: true
TextMetrics {
id: idLabelTextMetrics
@@ -109,108 +113,121 @@ Kirigami.Dialog {
elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
}
}
Kirigami.ActionToolBar {
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
}
QQC2.AbstractButton {
contentItem: Barcode {
barcodeType: Barcode.QRCode
content: root.shareUrl
}
onClicked: {
const map = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: avatar.source,
}) as QrCodeMaximizeComponent;
root.close();
map.open();
}
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.rightMargin: Kirigami.Units.largeSpacing
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: root.shareUrl
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
Kirigami.ActionToolBar {
Layout.topMargin: Kirigami.Units.largeSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
Kirigami.Heading {
@@ -244,9 +261,11 @@ Kirigami.Dialog {
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
icon: "im-kick-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
@@ -271,9 +290,11 @@ Kirigami.Dialog {
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
icon: "im-ban-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
@@ -309,9 +330,11 @@ Kirigami.Dialog {
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
icon: "delete",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25

View File

@@ -29,6 +29,28 @@
#include <KIO/OpenUrlJob>
#endif
/**
* @brief Stops RoomManager from updating the last room and space config.
*/
class LastRoomBlocker
{
public:
explicit LastRoomBlocker(RoomManager *manager)
: m_manager(manager)
{
Q_ASSERT(manager);
m_manager->m_dontUpdateLastRoom = true;
}
~LastRoomBlocker()
{
m_manager->m_dontUpdateLastRoom = false;
}
private:
RoomManager *m_manager;
};
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openStateConfig())
@@ -264,12 +286,12 @@ void RoomManager::maximizeMedia(const QString &eventId)
Q_EMIT showMaximizedMedia(index);
}
void RoomManager::maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language)
void RoomManager::maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language)
{
if (codeText.isEmpty()) {
return;
}
Q_EMIT showMaximizedCode(author, time, codeText, language);
Q_EMIT showMaximizedCode(author, dateTime, codeText, language);
}
void RoomManager::requestFullScreenClose()
@@ -290,6 +312,7 @@ void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChat
}
Q_EMIT showDelegateMenu(parent,
room,
event->id(),
room->qmlSafeMember(event->senderId()),
MessageComponentType::typeForEvent(*event),
@@ -319,17 +342,6 @@ void RoomManager::loadInitialRoom()
resolveResource(m_arg);
}
if (m_isMobile) {
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
// We can't have empty keys in KConfig, so we stored it as "Home"
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, false);
// We don't want to open a room on startup on mobile
return;
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
@@ -343,15 +355,15 @@ void RoomManager::loadInitialRoom()
void RoomManager::openRoomForActiveConnection()
{
if (!m_connection) {
setCurrentRoom({});
setCurrentSpace({}, false);
return;
}
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, true);
// We don't want to open a room on startup on mobile
setCurrentSpace(lastSpace, !m_isMobile);
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
@@ -508,7 +520,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
Q_EMIT connectionChanged();
}
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
{
m_currentSpaceId = spaceId;
@@ -528,25 +540,26 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
}
if (!setRoom) {
return;
}
// If we requested to change to the last opened room, do so:
if (goToLastUsedRoom) {
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
LastRoomBlocker blocker(this);
// We intentionally don't want to open the last room on mobile
if (m_isMobile) {
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home":
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home"
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
// If no last room was opened, go to the space home:
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
// Fallback to no room opened:
setCurrentRoom({});
}
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
setCurrentRoom({});
}
QString RoomManager::findSpaceIdForCurrentRoom() const
@@ -606,21 +619,23 @@ void RoomManager::setCurrentRoom(const QString &roomId)
Q_EMIT currentRoomChanged();
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
return;
}
if (!m_dontUpdateLastRoom) {
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
return;
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
}
}
}

View File

@@ -23,6 +23,7 @@
#include "models/timelinemodel.h"
#include "models/userlistmodel.h"
#include "models/widgetmodel.h"
#include "neochatdatetime.h"
#include "neochatroommember.h"
class NeoChatRoom;
@@ -218,7 +219,7 @@ public:
*/
Q_INVOKABLE void maximizeMedia(const QString &eventId);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &time, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay currently open closes.
@@ -292,7 +293,7 @@ Q_SIGNALS:
/**
* @brief Request a block of code is shown maximized.
*/
void showMaximizedCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
void showMaximizedCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay closes.
@@ -308,6 +309,7 @@ Q_SIGNALS:
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(QObject *parent,
NeoChatRoom *room,
const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
@@ -339,6 +341,11 @@ Q_SIGNALS:
void currentSpaceChanged();
protected:
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
friend class LastRoomBlocker;
private:
bool m_isMobile = false;
@@ -384,8 +391,13 @@ private:
*/
QString findSpaceIdForCurrentRoom() const;
// Space ID, "DM", or empty string
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
/**
* @brief Sets the current space.
*
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
*/
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
/**
* @brief Resolve a user URI.

View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#include "supportcontroller.h"
#include <Quotient/csapi/support.h>
#include <QDebug>
using namespace Quotient;
void SupportController::setConnection(NeoChatConnection *connection)
{
if (m_connection != connection) {
m_connection = connection;
Q_EMIT connectionChanged();
load();
}
}
NeoChatConnection *SupportController::connection() const
{
return m_connection;
}
QString SupportController::supportPage() const
{
return m_supportPage;
}
QList<SupportContact> SupportController::contacts() const
{
return m_contacts;
}
void SupportController::load()
{
if (!m_connection) {
qWarning() << "Tried to load support information without a valid connection?";
return;
}
m_connection->callApi<GetWellknownSupportJob>()
.onResult([this](const auto &job) {
m_supportPage = job->supportPage();
m_contacts.reserve(job->contacts().size());
for (const auto &contact : job->contacts()) {
m_contacts.push_back(SupportContact{
.role = contact.role,
.matrixId = contact.matrixId,
.emailAddress = contact.emailAddress,
});
}
Q_EMIT loaded();
})
.onFailure([this](const auto &job) {
Q_UNUSED(job)
// Just do nothing, our properties will be empty.
Q_EMIT loaded();
});
}
#include "moc_supportcontroller.cpp"

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "neochatconnection.h"
class SupportContact
{
Q_GADGET
QML_NAMED_ELEMENT(supportContact)
QML_UNCREATABLE("")
Q_PROPERTY(QString role MEMBER role)
Q_PROPERTY(QString matrixId MEMBER matrixId)
Q_PROPERTY(QString emailAddress MEMBER emailAddress)
public:
QString role;
QString matrixId;
QString emailAddress;
};
class SupportController : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString supportPage READ supportPage NOTIFY loaded)
Q_PROPERTY(QList<SupportContact> contacts READ contacts NOTIFY loaded)
public:
void setConnection(NeoChatConnection *connection);
NeoChatConnection *connection() const;
QString supportPage() const;
QList<SupportContact> contacts() const;
Q_SIGNALS:
void connectionChanged();
void loaded();
private:
void load();
QPointer<NeoChatConnection> m_connection = nullptr;
QList<SupportContact> m_contacts;
QString m_supportPage;
};

View File

@@ -412,7 +412,6 @@ QQC2.Control {
Component {
id: replyPane
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent

View File

@@ -30,7 +30,7 @@ QQC2.ItemDelegate {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5

View File

@@ -18,6 +18,7 @@ class StateFilterModel : public QSortFilterProxyModel
QML_ELEMENT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
/**
* @brief Custom filter function checking if an event type has been filtered out.
*

View File

@@ -28,7 +28,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
TypeRole = 0, /**< The type of the state event. */
TypeRole = Qt::UserRole, /**< The type of the state event. */
EventCountRole, /**< Number of events of this type. */
StateKeyRole, /**<State key. Only valid if there's exactly one event of this type. */
};

View File

@@ -17,6 +17,7 @@ target_sources(LibNeoChat PRIVATE
filetransferpseudojob.cpp
filetype.cpp
linkpreviewer.cpp
neochatdatetime.cpp
roomlastmessageprovider.cpp
spacehierarchycache.cpp
texthandler.cpp

View File

@@ -150,7 +150,12 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
if (m_relationId.isEmpty()) {
return room->member(QString());
}
return room->member((*room->findInTimeline(m_relationId))->senderId());
const auto [event, _] = room->getEvent(m_relationId);
if (event != nullptr) {
return room->member(event->senderId());
}
qWarning() << "Failed to find relation" << m_relationId << "in timeline?";
return room->member(QString());
}
bool ChatBarCache::relationAuthorIsPresent() const
@@ -173,8 +178,8 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
return EventHandler::markdownBody(&**event);
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
return EventHandler::markdownBody(event);
}
return {};
}
@@ -280,11 +285,6 @@ void ChatBarCache::postMessage()
return;
}
const auto replyIt = room->findInTimeline(replyId());
if (replyIt == room->historyEdge()) {
isReply = false;
}
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
room->post<Quotient::RoomMessageEvent>(text(), *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), std::move(content), relatesTo);

View File

@@ -28,7 +28,7 @@ class SyntaxHighlighter : public QSyntaxHighlighter
public:
QTextCharFormat mentionFormat;
QTextCharFormat errorFormat;
Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
Sonnet::BackgroundChecker checker;
Sonnet::Settings settings;
QList<QPair<int, QString>> errors;
QString previousText;
@@ -48,11 +48,11 @@ public:
errorFormat.setForeground(m_theme->negativeTextColor());
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
connect(&checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
errors += {start, word};
checker->continueChecking();
checker.continueChecking();
});
connect(checker, &Sonnet::BackgroundChecker::done, this, [this]() {
connect(&checker, &Sonnet::BackgroundChecker::done, this, [this]() {
rehighlightTimer.start();
});
rehighlightTimer.setInterval(100);
@@ -64,9 +64,9 @@ public:
if (settings.checkerEnabledByDefault()) {
if (text != previousText) {
previousText = text;
checker->stop();
checker.stop();
errors.clear();
checker->setText(text);
checker.setText(text);
}
for (const auto &error : errors) {
setFormat(error.first, error.second.size(), errorFormat);

View File

@@ -74,7 +74,7 @@ public:
*/
enum Roles {
NameRole = Qt::DisplayRole, /**< The power level name. */
ValueRole, /**< The power level value. */
ValueRole = Qt::UserRole, /**< The power level value. */
};
Q_ENUM(Roles)

View File

@@ -93,7 +93,7 @@ QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const
return displayName;
}
QDateTime EventHandler::time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
NeoChatDateTime EventHandler::dateTime(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
{
if (room == nullptr) {
qCWarning(EventHandling) << "time called with room set to nullptr.";
@@ -114,25 +114,6 @@ QDateTime EventHandler::time(const NeoChatRoom *room, const Quotient::RoomEvent
return event->originTimestamp();
}
QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending)
{
auto ts = time(room, event, isPending);
if (ts.isValid()) {
if (relative) {
KFormat formatter;
return formatter.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
}
return {};
}
QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending)
{
return time(room, event, isPending).toLocalTime().toString(format);
}
bool EventHandler::isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (room == nullptr) {

View File

@@ -7,6 +7,8 @@
#include <QString>
#include <Quotient/events/eventcontent.h>
#include "neochatdatetime.h"
namespace Quotient
{
namespace EventContent
@@ -64,41 +66,7 @@ public:
/**
* @brief Return a QDateTime object for the event timestamp.
*/
static QDateTime time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param relative whether the string is realtive to the current date, i.e.
* Yesterday or Wednesday, etc.
* @param format the QLocale::FormatType to use.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool relative,
QLocale::FormatType format = QLocale::ShortFormat,
bool isPending = false);
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param format the format to use as a string.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending = false);
static NeoChatDateTime dateTime(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
/**
* @brief Whether the event should be highlighted in the timeline.

View File

@@ -13,7 +13,7 @@
CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, m_filterModel(new CompletionProxyModel(this))
, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);

View File

@@ -26,6 +26,8 @@ class CompletionProxyModel : public QSortFilterProxyModel
Q_OBJECT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
/**
* @brief Wether a row should be shown or not.
*

View File

@@ -178,4 +178,19 @@ void LiveLocationsModel::updateLocationData(LiveLocationData &&data)
Q_EMIT dataChanged(idx, idx);
}
NeoChatRoom *LiveLocationsModel::room() const
{
return m_room;
}
void LiveLocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
#include "moc_livelocationsmodel.cpp"

View File

@@ -27,7 +27,7 @@ class LiveLocationsModel : public QAbstractListModel
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatRoom *room MEMBER m_room NOTIFY roomChanged)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/** The event id of the beacon start event, ie. the one all suspequent
* events use to relate to the same beacon.
* If this is set only this specific beacon will be coverd by this model,
@@ -57,6 +57,9 @@ public:
QRectF boundingBox() const;
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
void roomChanged();
void eventIdChanged();

View File

@@ -215,7 +215,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return QVariant();
}
NeoChatRoom *room = m_rooms.at(index.row());
if (role == DisplayNameRole) {
if (role == DisplayNameRole || role == Qt::DisplayRole) {
return room->displayName();
}
if (role == EscapedDisplayNameRole) {

View File

@@ -36,7 +36,7 @@ public:
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
DisplayNameRole = Qt::UserRole, /**< The display name of the room. */
EscapedDisplayNameRole, /**< HTML-Escaped display name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
CanonicalAliasRole, /**< The room canonical alias. */

View File

@@ -11,6 +11,9 @@ bool UserFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceP
if (!m_allowEmpty && m_filterText.length() < 1) {
return false;
}
if (sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::MembershipRole).value<Quotient::Membership>() != Quotient::Membership::Join) {
return false;
}
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|| sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::UserIdRole).toString().contains(m_filterText, Qt::CaseInsensitive);
}

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