Compare commits

..

201 Commits

Author SHA1 Message Date
Heiko Becker
df8a09ce90 GIT_SILENT Update Appstream for new release
(cherry picked from commit b009b9aac0)
2026-02-28 13:22:00 +01:00
l10n daemon script
150b9c4038 GIT_SILENT Sync po/docbooks with svn 2026-02-28 01:48:45 +00:00
James Graham
7550b2b000 Apply 1 suggestion(s) to 1 file(s)
Co-authored-by: Tobias Fella <fella@posteo.de>
2026-02-27 14:15:30 +00:00
James Graham
daa66c4abc Treat all text in a code block as plain, i.e. don't format based on markdown. 2026-02-27 14:15:30 +00:00
James Graham
e0b229e040 Focus the charbar when clicking the reply button. 2026-02-27 13:29:04 +00:00
James Graham
c07db82552 When completing treat all text as plain so don't upgrade to markdown. 2026-02-27 13:06:26 +00:00
l10n daemon script
4c1301da51 GIT_SILENT Sync po/docbooks with svn 2026-02-27 01:49:53 +00:00
l10n daemon script
adf0544eb0 GIT_SILENT made messages (after extraction) 2026-02-27 00:43:33 +00:00
Tobias Fella
44ec1e3529 Cleanup logging in RoomListModel 2026-02-26 23:41:20 +01:00
Tobias Fella
7cfac2151e Cleanup logging in TimelineMessageModel 2026-02-26 23:41:20 +01:00
Tobias Fella
3f577adf34 Cleanup logging in MessageContentModel 2026-02-26 23:41:20 +01:00
Tobias Fella
0375ef99a5 Clean up chatbarcache 2026-02-26 23:41:18 +01:00
James Graham
23c0bdb647 Improve the label for showing the rich text controls and tell users what pressing enter will do. 2026-02-26 19:51:40 +00:00
James Graham
611374d4dd Fix funny behaviour when pasting. basically we should start but not end a cursor edit block. 2026-02-26 19:26:26 +00:00
James Graham
c3ea0db82d Make sure that the clickable area of the chatbar fill the whole space 2026-02-26 17:44:40 +00:00
Tobias Fella
eb58d96d77 Remove duplicate regexes 2026-02-26 16:04:35 +00:00
Tobias Fella
9cad195704 Fix many clazy warnings 2026-02-26 14:52:14 +00:00
James Graham
c08693c508 Fix the parsing of the chat cache so the lists are no longer broken and add tests. 2026-02-26 06:15:42 +00:00
James Graham
7f91c57945 Make sure that when a heading is fully deleted the style is returned to paragraph 2026-02-26 06:08:14 +00:00
l10n daemon script
8b9e470907 GIT_SILENT Sync po/docbooks with svn 2026-02-26 01:48:01 +00:00
Darshan Phaldesai
63ddfb6cb8 SendBar: make overflowWidth bigger.
also fix initial state for rich text button
2026-02-25 22:26:27 +00:00
Darshan Phaldesai
ce84562829 chatbar: make emojidialog dynamically loaded 2026-02-25 22:26:27 +00:00
Darshan Phaldesai
7eda07d788 chatbar: remove redundant code 2026-02-25 22:26:27 +00:00
Darshan Phaldesai
3105fb8aa8 chatbar: move richtext back to main chat 2026-02-25 22:26:27 +00:00
Darshan Phaldesai
e3fdcb8779 chatbar: move emojis back to main panel 2026-02-25 22:26:27 +00:00
leia uwu
594018b188 restore window maximized state on toggle
Otherwise the window will have a maximized size but not maximized state after toggling it from system tray
2026-02-25 14:33:50 +00:00
l10n daemon script
774eb6a505 GIT_SILENT Sync po/docbooks with svn 2026-02-25 01:50:06 +00:00
l10n daemon script
f9a6533ca9 GIT_SILENT made messages (after extraction) 2026-02-25 00:46:42 +00:00
Azhar Momin
c804b9ce14 Send typing notifications from ChatBar again 2026-02-24 22:03:46 +00:00
James Graham
d1acb97fe2 - Make sure that when adding characters before/after a link that it doesn't take the link style
- Make sure that when double clicking a link with a space the whole text is selected
- Make sure that shift selection with arrows works
- Make sure that ctrl left right (word jump) moves across the whole link even if multiple words
2026-02-24 16:54:57 +00:00
James Graham
0a99e90591 Treat links as a single character for the purposes of delete, backspace, left and right.
When backspacing at a link the first key selects the whole link the second deletes. Similar for delete, left and right.
2026-02-24 16:54:56 +00:00
James Graham
c6313d2951 Hook sending messages back up 2026-02-24 16:54:56 +00:00
James Graham
d2d48110cb Use the rich text char format to store mentions rather than a separate structure in ChatBarCache.
This removes mentions from ChatBarCache and instead sets mentions as an anchor using the QTextCursor. Saving and restoring the chatbar text content is then done using QTextDocumentFragments which retain the rich text formatting.
2026-02-24 16:54:48 +00:00
l10n daemon script
a235f39c84 GIT_SILENT Sync po/docbooks with svn 2026-02-24 01:47:14 +00:00
l10n daemon script
8e1303edbe GIT_SILENT made messages (after extraction) 2026-02-24 00:44:31 +00:00
l10n daemon script
f66acb5f78 GIT_SILENT Sync po/docbooks with svn 2026-02-23 01:46:24 +00:00
l10n daemon script
0fe4ae82ea GIT_SILENT made messages (after extraction) 2026-02-23 00:44:25 +00:00
James Graham
8e516422b7 Make sure the default menu of textArea isn't thrown for non editable TextComponents.
Note: I know this is horrible but it's all we got until discussions in QQC2-Desktop-Style are resolved.
2026-02-22 18:07:27 +00:00
l10n daemon script
1dc566f9a9 GIT_SILENT Sync po/docbooks with svn 2026-02-22 01:48:38 +00:00
l10n daemon script
cdfda55ac1 GIT_SILENT made messages (after extraction) 2026-02-22 00:45:12 +00:00
Joshua Goins
a27c8971b9 Fix TimelineEnd/TimelineBeginning models properly this time
Amends 3b00e14a9d with a fix that's more
sensible, as we need to take into account the various checks before
mindlessly inserting roles.
2026-02-21 12:41:55 -05:00
Tobias Fella
b7688d4373 Remove dependency on KStatusNotifierItem
We had two implementations of the system tray icon for no particularly good reason
2026-02-21 17:09:22 +00:00
James Graham
73e6a536db Make sure that we only insert a new block for headers. This makes sure that hitting return in a list just adds the next list item. 2026-02-21 14:42:02 +00:00
Joshua Goins
a60d6c4a60 Add report buttons to spaces
This allows you to report harmful spaces that you already joined in,
similar to the current UX for rooms.

Note that you can't report subspaces or rooms within said spaces yet,
that requires hover actions (or ideally a context menu) that don't exist
yet.

See #707
2026-02-20 23:28:16 -05:00
l10n daemon script
b49ce25246 GIT_SILENT Sync po/docbooks with svn 2026-02-21 01:49:38 +00:00
l10n daemon script
6c52cccf20 GIT_SILENT made messages (after extraction) 2026-02-21 00:44:35 +00:00
Joshua Goins
1eba52bb7a Add a way to block invites from all users
This is a useful safety feature, not just for people under attack but
also if you have no use for this feature. Currently this is gated for
servers that support Matrix CSAPI 1.18.

Fixes #662
2026-02-20 18:57:21 -05:00
Joshua Goins
3b00e14a9d Fix the TimelineEnd delegate not appearing when going back in history
It was possible to make this appear if you switched rooms while you had
all history loaded, but now it should show up naturally.
2026-02-20 18:38:45 -05:00
Joshua Goins
f6abbda1e3 Don't claim this is "the beginning of the chat" if a predecessor exists
This could be percieved as confusing since we show a nice blue banner
saying where to find the previous conversation.
2026-02-20 18:38:45 -05:00
Tobias Fella
2d0fa43f4f Minor refactoring 2026-02-21 00:26:31 +01:00
Tobias Fella
502e300d5f Remove leftover warning 2026-02-21 00:17:54 +01:00
Joshua Goins
b7ea6f265e Fix-up ImageComponent's hide media functionality
The Z-level for the "Hide media" and other overlay controls were lost
during the rich text refactor, and now restored. I also made it so the
blurhash is now used when the media is hidden, and when hiding media it
no longer allows you to jumpscare yourself by maximizing the media when
you probably didn't actually want to.

When the media doesn't have blurhash, it falls back to black.
2026-02-20 16:02:49 -05:00
James Graham
5f0f9135fe Stop pester in console when changing rooms regarding missing authors. 2026-02-20 17:45:28 +00:00
Tobias Fella
b0dce5fd0a Fix TimelineEndDelegate and SuccessorDelegate 2026-02-20 17:28:17 +00:00
Tobias Fella
2c5bef6e96 Close pinned message when leaving room
BUG: 516350
2026-02-20 17:04:04 +00:00
Tobias Fella
aaaaf91248 Don't try opening last room when this room is not available
When switching connection to a connection that is not loaded or where we have left the last room, trying to resolve it will not do anything, causing the room from the previous connection to stay open
2026-02-20 16:08:25 +00:00
Tobias Fella
7e3db19ead Require libQuotient 0.9.5 2026-02-20 15:32:41 +00:00
Tobias Fella
f0ea5d1e90 Disable FreeBSD CI 2026-02-20 15:22:41 +01:00
Tobias Fella
d2a6d8e447 Fix opening QR code 2026-02-20 12:46:58 +01:00
l10n daemon script
57c20d4c5a GIT_SILENT Sync po/docbooks with svn 2026-02-20 02:06:24 +00:00
l10n daemon script
d7d348322f GIT_SILENT made messages (after extraction) 2026-02-20 00:45:13 +00:00
Joshua Goins
ae9b2abdc7 Reduce layout shift when loading mutual rooms in user profiles
Instead of making the visibility of this section in user profiles
dependent on the model, its now checking if you can check mutual rooms
and using a busy indicator. There's also a label for when you have no
rooms in common, which is a rare case (for example, banned or left
users.)
2026-02-19 18:11:03 -05:00
Joshua Goins
3a964bae20 Move more security and safety-relevant options to a better suited page
This includes the "show link previews", and "enable typing
notifications" options. I also re-arranged the Security & Safety page
into more sensible groups, and in preparation for more invite options.
2026-02-19 18:09:16 -05:00
Joshua Goins
4078d3f2dc Remove attach dialog
This was used when you pressed the "Attach file/image" button but had an
image copied to your clipboard - allowing you to select from either
source. This is a weird thing to ask, the button should always prompt
you with a file dialog.

It's still possible to paste an image from your clipboard with CTRL+V,
but there isn't a way to do it via right-click yet.

Fixes #712
2026-02-19 17:17:00 -05:00
Joshua Goins
53989ff4fe Improve messaging around various encryption key options
Element has landed on calling these "recovery keys" and we should do the
same. Since these have had various names throughout the years and in
previous versions of NeoChat, they are still mentioned.

I also renamed "Secret Backup" to "Key Storage" which is also Element
terminology.
2026-02-19 17:10:27 -05:00
Azhar Momin
2d33cbf6b1 Show thread root event instead of latest thread event 2026-02-19 16:34:33 -05:00
Azhar Momin
b42a82a455 Fix segfault in ContentProvider
QCache in ContentProvider handles cleanup for ThreadModel.
Setting NeoChatRoom as parent for ThreadModel caused it to
be deleted outside of QCache control leading to a double-free.
2026-02-19 16:34:33 -05:00
l10n daemon script
99aed0993e GIT_SILENT Sync po/docbooks with svn 2026-02-19 01:51:57 +00:00
l10n daemon script
7ede740aa8 GIT_SILENT made messages (after extraction) 2026-02-19 00:45:31 +00:00
Darshan Phaldesai
7137b60da9 compactLayout: add spacing around messages and chatbar 2026-02-18 12:08:05 -05:00
l10n daemon script
30462ddebb GIT_SILENT Sync po/docbooks with svn 2026-02-18 01:55:53 +00:00
Joshua Goins
428a196f8d Disable send message when there's nothing to actually send
Now there's a way to check if the message content model has any useful
data inside of it.

BUG: 516118
FIXED-IN: 26.04
2026-02-17 12:16:09 -05:00
l10n daemon script
2d2291fd78 GIT_SILENT Sync po/docbooks with svn 2026-02-17 01:47:55 +00:00
l10n daemon script
646f5476c3 GIT_SILENT made messages (after extraction) 2026-02-17 00:44:41 +00:00
Joshua Goins
03382496b9 Fix tooltip and checked state of message overflow button
This was incorrectly labeled as "Format Text" during the rich text
refactor, but this is for attaching stuff to messages.

I also fixed the checked state not resetting when the menu was closed.

BUG: 516117
BUG: 516119
FIXED-IN: 26.04
2026-02-16 19:11:04 -05:00
Tobias Fella
52302f0f5c Fix binding loops 2026-02-17 00:06:58 +01:00
Joshua Goins
b1595a4556 Ensure most of our dialogs have cancel buttons on the right
This is standard for KDE dialogs, but we weren't super consistent about
it. Most of this is porting to DialogButtonBox, and using buttonRole. I
also made sure the close button is hidden in cases where we have a
cancel button already.

I also took the time to clean out some cruft like manually calling
close() and such. I probably missed a lot more spots, but this is a good
improvement.
2026-02-16 16:31:10 -05:00
Joshua Goins
436b3a1008 Use the same spacing in chat bar as we do for messages
This fixes the odd spacing around some components like replies,
especially after d2aa8d672d.
2026-02-16 21:13:17 +00:00
Joshua Goins
44ff2daad2 Remove unwanted padding from TextComponent
This fixes the padding for TextComponent as well, which was needed
post-rich text refactor.
2026-02-16 21:13:17 +00:00
Volker Krause
9f64457521 Optimize compile time of emoji data maps
Compiling the two files including the emoji data now doesn't take
significantly longer anymore than before. Size of the .text section
also drops by more than 1Mb, while runtime or memory cost remains
unchanged.

There's more that could be done here, but not having to wait minutes
for those files to build is worth it on its own already.
2026-02-16 14:38:14 -05:00
James Graham
d2aa8d672d Remove unwanted padding from ReplyComponent 2026-02-16 19:09:39 +00:00
Joshua Goins
942221b59f Don't attempt to navigate to a component that doesn't exist
Check for the size of our component list before trying to access it.

BUG: 516107
FIXED-IN: 26.04
2026-02-16 13:46:25 -05:00
James Graham
7e6b79d5d4 Release threads removing the feature flag.
This mr performs some final cleanup to make sure the threads are sized correctly and it all works with the new chatbar
2026-02-16 11:16:32 -05:00
Darshan Phaldesai
85b731e9fb TypingPane: fix spacing between elements 2026-02-16 11:16:05 -05:00
James Graham
38b3e65618 Make sure that when we post a message we refocus the now empty chatbar 2026-02-16 07:28:54 +00:00
James Graham
89e5a605c4 Basically we were applying the default values onComponentComplete therefore overriding any user value.
This patch applies the default on creation before properties are set so the user can now override.
2026-02-16 06:39:34 +00:00
James Graham
a438173403 Change the behaviour of chatmarkdownhelper to only work when a single character is added.
This is because if pasting a link for example * or _ could be treated as formatting. We now assume that any pasted text is done as plain text and is wanted as is.
2026-02-16 06:17:57 +00:00
l10n daemon script
98a224ebdf GIT_SILENT Sync po/docbooks with svn 2026-02-16 01:48:30 +00:00
l10n daemon script
7b8576e203 GIT_SILENT made messages (after extraction) 2026-02-16 00:45:45 +00:00
Joshua Goins
f1b5ad7392 Allow focusing and navigating the space drawer with a keyboard
CCBUG: 511591
2026-02-15 08:03:37 -05:00
Joshua Goins
78beb9ffff When favoriting/reprioritizng a room, make sure to re-sort the list
This fixes some odd issues like newly favorited rooms being sent to the
bottom of the room list, and so on.
2026-02-15 08:03:37 -05:00
Joshua Goins
431dbf6457 Stop tree view from attempting to expand sections too early
This fixes a consistent bug for me, where the expandRecursively call
(somehow) predates the room list being populated. Which resulted in some
or all sections being collapsed on start-up.
2026-02-15 08:03:37 -05:00
l10n daemon script
e1b267622d GIT_SILENT Sync po/docbooks with svn 2026-02-15 01:53:30 +00:00
James Graham
47a8221f53 Fix rebase issues and restore mic button 2026-02-14 20:12:21 +00:00
James Graham
c554c40b3b Fix markdown helper test 2026-02-14 19:53:05 +00:00
James Graham
0341da5868 Cleanup chatkeyhelp and tests 2026-02-14 19:53:05 +00:00
James Graham
c5457a893f Fix tests 2026-02-14 19:53:05 +00:00
James Graham
9f6853f771 Make sure that char format is properly maintained for multiple formats when swithching to quote and back 2026-02-14 19:53:05 +00:00
James Graham
526cb07840 Note where nestedlisthelper has been changed 2026-02-14 19:53:05 +00:00
James Graham
dcc394677e Restore image editor 2026-02-14 19:53:05 +00:00
James Graham
a9a73ab24d Disallow event menu in edit mode 2026-02-14 19:53:05 +00:00
James Graham
2289dbb3fe Restore placeholder messages 2026-02-14 19:53:05 +00:00
James Graham
7758175334 Don't allow links when rich formatting is diabled 2026-02-14 19:53:05 +00:00
James Graham
c79753716c Don't follow link in the chatbar 2026-02-14 19:53:05 +00:00
James Graham
3aa4a915b1 Fix cancel reply 2026-02-14 19:53:05 +00:00
James Graham
60cf12524f Improve images in the chatbar
- Don't show the hide button
- Shrink them to better fit
- Allow a little more maxh height in the chatbar for attachments
- Make sure that the button states work properly when adding and removing images
2026-02-14 19:53:04 +00:00
James Graham
f02366ee48 Improve file attachments
-Don't show download and save in chatbar
-Add a remove button
-Add missing properties to model
2026-02-14 19:53:04 +00:00
James Graham
13d7f9b322 Make sure that the style menu shows the right style name for code and quote.
Also when in quote mode the valid styles are now transformed to how they look in quote to show they are valid. Clicking quote style again in a quote block will return to paragrpah style from heading now
2026-02-14 19:53:04 +00:00
James Graham
7afce01a23 Fold extra send icons when thin for mobile 2026-02-14 19:53:04 +00:00
James Graham
0d1f5c950d Change link button tooltip text to Edit link when a link already exists 2026-02-14 19:53:04 +00:00
James Graham
690bc0d385 Revert to Paragraph format after a heading 2026-02-14 19:53:04 +00:00
James Graham
c32235ffe3 Improve application of text formats from buttons.
- If the bar is empty they are now applied as normal
- If there is a selection or the cursor is next to a word it is applied to it.
- If there is no selected text and no word it is applied properly to the next char
2026-02-14 19:53:04 +00:00
James Graham
dff6ab66f1 Comment updates 2026-02-14 19:53:03 +00:00
James Graham
80047acf87 Improve the code for completions 2026-02-14 19:52:46 +00:00
James Graham
234d823366 Really reenable editing messages, don't just do half a job. 2026-02-14 19:52:46 +00:00
James Graham
a3cd0c0e8d Fix memory leak 2026-02-14 19:52:46 +00:00
James Graham
cad90d0c4c Reenable message edits 2026-02-14 19:52:46 +00:00
James Graham
6e28ada1a4 Fix room switch so that if there is no saved text the user ends up with an empty chatbar 2026-02-14 19:52:46 +00:00
James Graham
007ebbc003 Fix merge issues 2026-02-14 19:52:46 +00:00
James Graham
289c6c4f20 Fix delete when selecting text 2026-02-14 19:52:45 +00:00
James Graham
39573c1650 Make the scroll bar of the chat bar move properly as you type. 2026-02-14 19:52:45 +00:00
James Graham
b7a329c199 Fix reply component 2026-02-14 19:52:45 +00:00
James Graham
6b318ec754 Update the look of the chatbar to be floating with the rich text controls on top and send buttons inline 2026-02-14 19:52:45 +00:00
James Graham
79de8a792c Simplify and fix chat markdown helper and add additional tests 2026-02-14 19:52:45 +00:00
James Graham
b45ded678e Fix multi line quotes 2026-02-14 19:52:45 +00:00
James Graham
f4cb660422 Disallow formatting when there is an attachment and when adding one if there is rich formatting warn the user it will be removed and remove if they accept. 2026-02-14 19:52:45 +00:00
James Graham
d64e6fc206 Change editing so that going up or down from a code or quote a para will be added if one doesn't exist so that the block can te typed around 2026-02-14 19:52:45 +00:00
James Graham
d0abfe60f9 Restore missing functionality 2026-02-14 19:52:45 +00:00
James Graham
d10fe4a684 Add more tests 2026-02-14 19:52:45 +00:00
James Graham
9ea76ca5d0 Move the remaining functionality of ChatDocumentHandler to ChatTextItemHelper or split into own objects 2026-02-14 19:52:45 +00:00
James Graham
22d7d90cf4 Separate ChatButtonHelper from ChatDocumentHandler 2026-02-14 19:52:45 +00:00
James Graham
45163944d0 Add tests for ChatMarkdownHelper and rework how formats are applied to make it more robust. 2026-02-14 19:52:45 +00:00
James Graham
f31e9062e6 Move ChatDocumentHandler to use QmlTextItemWrapper and create test 2026-02-14 19:52:45 +00:00
James Graham
416d85af3b Spearate completion from ChatDocumentHandler 2026-02-14 19:52:44 +00:00
James Graham
02bed79265 Fix link insertion 2026-02-14 19:52:44 +00:00
James Graham
aadc441686 Add qml test for chatdocumenthandler 2026-02-14 19:52:44 +00:00
James Graham
4db1e1c437 Add automatic markdown formatting 2026-02-14 19:52:44 +00:00
James Graham
11bf741554 Improve the style picker 2026-02-14 19:52:44 +00:00
James Graham
c128450cf5 Add ChatBarMessageContentModel and hook up 2026-02-14 19:52:43 +00:00
Tobias Fella
9cbe9f7280 Start implementing rich-text editor 2026-02-14 19:45:15 +00:00
James Graham
1f723d1fdf Allow replying to messages within a thread 2026-02-14 14:07:48 -05:00
Tobias Fella
b88e27d6d5 Refactor NeoChatRoom a bit 2026-02-14 12:23:50 -05:00
Joshua Goins
4e616d53b2 Fix the emoticon editor page
It wasn't possible to edit the shortcode or description anymore, because
signals weren't hooked up. I also added a separator to help separate
some of these controls visually.
2026-02-14 12:23:31 -05:00
Joshua Goins
57b6dbcbde Don't open room-specific profile in account menu
This should be your main profile, since visually and functionally this
is "outside" the current room.

Since the workaround is a bit estoeric, I added a comment so I remember
why I did this later.
2026-02-14 12:23:20 -05:00
Joshua Goins
49646c63f8 Improve read marker delegate, add button to mark as read
I added an icon to the read marker (to help distinguish it from
text-heavy chat rooms), and fixed up the padding. I also find myself
reaching to right-click rooms often to mark them as read, so why not do
this from the read marker itself?
2026-02-14 12:23:10 -05:00
Joshua Goins
9d2a427619 Improve the search pages, especially in error and searching states
Someone hit a nasty bug while attempting to find a KDE room on the
kde.org server, the error wouldn't come up normally and the dialog would
be blank. That's because that specific placeholder message doesn't
appear until you type something into the search field, which is weird.

There is a few other oddities with SearchPage that I squashed, including
showing the loading placeholder in more appropiate situations.
2026-02-14 11:04:29 -05:00
Tobias Fella
593ad27e8c Implement sending voice messages 2026-02-14 11:04:22 -05:00
Joshua Goins
3d07f723c8 Make member list sorted a bit more efficient
I don't have any hard numbers on what difference this makes, but it's
definitely a positive improvement. I noticed and fixed a few issues that
were made more glaring by recent changes in libQuotient:

1. Room::memberJoined is called during the historical loading or
whatever, when we only need that *after* stuff is settled.
2. We really don't need to sort the room's members immediately - it's
only relevant when UserListModel is used (and I think this was previous
behavior?) So now its done lazily.
3. We do not want to call Room::effectivePowerLevel willy-nilly. It may
become a more expensive lookup, and it's also varying levels of wasteful
depending on which sorting algorithm the STL uses. It doesn't cost much
for us to keep a temporary cache for the lambda function to use.
2026-02-14 09:55:05 -05:00
Joshua Goins
d47a4eb0de Fix weird scrolling behavior in room list
You may have noticed that the room list acts a bit... odd. You usually
can't scroll all the way down with a scrollwheel, it just
stops. It's possible to continue a little bit more with the scrollbar.
And sometimes the scrollbar doesn't know how big it's actually supposed
to be, commonly occuring when switching from a large room list to a
smaller one.

I narrowed it down the same usual problem with views in QtQuick:
variable sized delegates! Using GammaRay I figured out that for the
delegates currently in use they're slightly different: 46 pixels for
regular room delegates, 42 pixels for section headers and the "Find your
Friends" button was also different.

*Technically* TableView (and by extension TreeView) is supposed to allow
variable-sized delegates, and we should be able to advertise that with
rowHeightProvider. I tried a bunch of different solutions and none of
them worked reliably, so I took the usual sledgehammer approach of
making all of the delegates the same size.

This fixes all of the obvious bugs with the room list I could see, with
the one visual downside of making the section headers slightly taller.
But since I spent some time improving the tap targets, this is only a
visual change and not a functional one.

I also made sure to test it in compact mode, and everything shrinks as
expected.
2026-02-14 09:54:42 -05:00
l10n daemon script
11f8407ef7 GIT_SILENT Sync po/docbooks with svn 2026-02-14 01:52:45 +00:00
Joshua Goins
e4c9230c09 Re-settle the timeline view when replying to messages
Or changing the height of the chatbar in other ways, which I'm sure is
going to become more common with our new rich text system.
2026-02-13 19:58:40 -05:00
Joshua Goins
318432d561 Fix undefined reference in Bubble
This tends to spam the logs while loading/unloading timelines.
2026-02-13 19:58:19 -05:00
Joshua Goins
afa699381b Change loading delegate to animated progress bar
Gives a nicer visual indication instead of static text, in my opinion.
2026-02-13 19:58:19 -05:00
l10n daemon script
244ef77c4a GIT_SILENT Sync po/docbooks with svn 2026-02-13 01:47:10 +00:00
Joshua Goins
847db41fb3 Hide rooms with custom defined types in quick switcher
This matches the behavior in other room lists. I also tried to normalize
the constructor with SortFilterRoomTreeModel.
2026-02-12 16:33:22 -05:00
Joshua Goins
115d4e7466 Reduce the tap target of RoomTreeSection
For some reason I don't understand, the ItemDelegate used for these
sections are ginormous. There is quite a bit of padding which confuses
users as its unexpectedly used for the tap area.

I changed it so we only listen for taps inside of the contentItem
itself, which is a more suitable area.
2026-02-12 07:29:45 -05:00
Joshua Goins
346d311909 Replace 👀 with ❤️ in the quick reactions bar
I personally use this emoji more than the other, and have seen it more
in the wild too.
2026-02-12 07:29:33 -05:00
Joshua Goins
cff27ca7db Add user ID to the account editor page
For some reason this was never featured here, and it's always useful to
have another place to check and/or copy this ID.
2026-02-12 07:29:20 -05:00
Joshua Goins
35daae6b5d Replace "Show QR Code" in account menu with "Open Profile" action
When you just want to share or view your own profile, the UX has proven
to be a bit confusing. You could try to scroll to find a previous
message of yours, or hopelessly go down the rabbithole of settings (none
of which provide a copyable user ID currently.) Lets cut down on the
slack by providing a way to instantly open your profile from anywhere.

This replaces the "Show QR Code" action because this is duplicative
within the profile itself.
2026-02-12 07:29:10 -05:00
l10n daemon script
94fb429c47 GIT_SILENT Sync po/docbooks with svn 2026-02-12 01:47:38 +00:00
Joshua Goins
9f4146e5b1 Fix the emoticon editor looking visually broken
We need to set a minimum width/height here since the Image isn't
technically loaded, when creating a new one.
2026-02-11 18:52:21 -05:00
Darshan Phaldesai
a9b4a900c9 Move notification button to UserInfo
Move notifications button to a more appropriate position next to username. It used to live in the room/spaces bar, which doesn't make sense context wise. Also the mobile view moves to a bottom navbar anyway.

This is just a copy-paste of the adjacent settings tool button with appropriate icon and callback.
2026-02-11 08:56:04 -05:00
Joshua Goins
18d7d2f736 Add a way to open settings on mobile again
This is visible on desktop, but wasn't accessible on mobile.
2026-02-11 08:55:08 -05:00
Joshua Goins
be65d506c4 Fix the mobile title sometimes disappearing
I was depending on an implicit connection property from somewhere.
2026-02-11 08:55:08 -05:00
Azhar Momin
f5d726989f Add support for copying & deleting multiple messages at once
BUG: 496458
2026-02-11 08:00:09 -05:00
l10n daemon script
0f634ff795 GIT_SILENT Sync po/docbooks with svn 2026-02-11 01:47:49 +00:00
Darshan Phaldesai
1289194b3f Messages: Make the Date/Timestamps more usable
Previously timestamps were in the right-hand side of the messages which made it very hard to relate timestamps with their corresponding messages. 
Moving them right next to the name makes much better UX wise (and surprisingly didn't make the UI too crowded). I have tested this in dark light and bubbles mode, and it all looks good and comfortable to me.

I have also tweaked how the timestamps are formatted. 
- For messages on the same day, it will skip the date part.
- For recent days, it uses relative timestamp (yesterday, XX:XX) 
- For everything before its shows short form date and time

The tooltip now uses Long Format of Date and Time.
2026-02-10 19:32:36 -05:00
Tobias Fella
8ca1b8b1d3 Adapt to libquotient api changes 2026-02-10 12:48:50 +00:00
Joshua Goins
793f81e733 Focus code and location chooser maximize components too
I did this for images a while back, but not for these. Otherwise you
can't press escape or perform other key navigation functions easily.

BUG: 515462
FIXED-IN: 25.12.2
2026-02-09 21:32:24 -05:00
Tobias Fella
4c31f42144 Fix type name for NeoChatDateTime
QML doesn't want it to start with an uppercase letter
2026-02-09 21:22:23 -05:00
Azhar Momin
6b664b0547 Fix context menu not appearing in room media scroll view
MessageDelegate calls model.findEvent(root.eventId), which existed in
MessageFilterModel but was missing in MediaMessageFilterModel. This adds
a findEvent() implementation to MediaMessageFilterModel so the context
menu works correctly in the media scroll view.
2026-02-09 21:09:37 -05:00
l10n daemon script
44f78353ab GIT_SILENT Sync po/docbooks with svn 2026-02-10 01:45:39 +00:00
l10n daemon script
3b2df95df7 GIT_SILENT Sync po/docbooks with svn 2026-02-09 01:43:37 +00:00
James Graham
8edb248647 Fix use after free in message delegate. We can't delete the incubator in the completed callback because it then returns to the incubator we just deleted. 2026-02-08 18:21:12 +00:00
l10n daemon script
07bc06f4ff GIT_SILENT Sync po/docbooks with svn 2026-02-08 01:44:05 +00:00
l10n daemon script
0d9988013b GIT_SILENT Sync po/docbooks with svn 2026-02-07 01:44:26 +00:00
Tobias Fella
c1720bbaa7 Fix various qml warnings 2026-02-06 14:45:03 +01:00
Tobias Fella
692ce82717 Fix minor warnings 2026-02-06 14:19:08 +01:00
Tobias Fella
ac04fa6a13 Fixes 2026-02-06 14:18:54 +01:00
Tobias Fella
39aaced0f9 Fix unqualified access 2026-02-06 14:17:55 +01:00
Tobias Fella
9887665560 Prevent shadowing 2026-02-06 14:17:20 +01:00
Tobias Fella
c590cb76a0 Use Application.layoutDirection 2026-02-06 14:14:31 +01:00
Tobias Fella
f9b0c56fa0 Prevent shadowing 2026-02-06 14:12:55 +01:00
Tobias Fella
aa7ab6b2ec Fix minor warnings 2026-02-06 14:12:24 +01:00
Tobias Fella
ac6a5663a1 Use required properties 2026-02-06 14:11:23 +01:00
Tobias Fella
f09dc79995 Remove unused import 2026-02-06 14:09:34 +01:00
Tobias Fella
693b2e74fe SupportDialog: Set ComponentBehavior 2026-02-06 14:07:08 +01:00
Tobias Fella
3a54d58516 Fix pageStack warnings 2026-02-06 14:05:23 +01:00
Tobias Fella
6abb117989 Set componentBehavior 2026-02-06 14:03:47 +01:00
Tobias Fella
b2dee6a96b Fix unqualified access 2026-02-06 14:02:41 +01:00
Tobias Fella
1e1ba1dca3 Cast qml object where necessary 2026-02-06 13:58:55 +01:00
Tobias Fella
55676b06c2 QrScannerPage: Prevent shadowing 2026-02-06 13:57:20 +01:00
Tobias Fella
513496cf85 Add translation context 2026-02-06 13:56:26 +01:00
Tobias Fella
69215401dd Use let instead of var 2026-02-06 13:55:45 +01:00
Tobias Fella
d5ec37e1af Fix some unqualified access warnings 2026-02-06 13:53:20 +01:00
Tobias Fella
6a45e2532c Prevent more shadowing 2026-02-06 13:50:40 +01:00
Tobias Fella
322926e31c Remove invalid anchors 2026-02-06 13:49:44 +01:00
Tobias Fella
29592a7f92 Prevent shadowing 2026-02-06 13:48:54 +01:00
Tobias Fella
8fc8baa2d2 Fix a few minor qml warnings 2026-02-06 12:53:07 +01: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
270 changed files with 68131 additions and 41650 deletions

View File

@@ -12,7 +12,7 @@ include:
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml

View File

@@ -28,7 +28,6 @@ Dependencies:
'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'

View File

@@ -88,7 +88,7 @@ if(ANDROID)
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
@@ -106,7 +106,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.9.3)
find_package(QuotientQt6 0.9.5)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -178,7 +178,7 @@ add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer QuickTest)
add_subdirectory(autotests)
# add_subdirectory(appiumtests)
if (NOT ANDROID)

View File

@@ -45,12 +45,6 @@ ecm_add_test(
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test(
timelinemessagemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
@@ -110,3 +104,45 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
TEST_NAME modeltest
)
ecm_add_test(
blockcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME blockcachetest
)
macro(add_qml_tests)
if (WIN32)
set(_extra_args -platform offscreen)
endif()
foreach(test ${ARGV})
add_test(NAME ${test}
COMMAND qmltest
${_extra_args}
-input ${test}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
endforeach()
endmacro()
add_executable(qmltest qmltest.cpp
chatkeyhelpertesthelper.h
chatmarkdownhelpertestwrapper.h
chattextitemhelpertesthelper.h
)
qt_add_qml_module(qmltest URI NeoChatTestUtils)
target_link_libraries(qmltest
PRIVATE
Qt6::Qml
Qt6::QuickTest
LibNeoChat
LibNeoChatplugin
)
add_qml_tests(
chattextitemhelpertest.qml
chatmarkdownhelpertest.qml
chatkeyhelpertest.qml
)

View File

@@ -7,7 +7,9 @@
#include <QVariantList>
#include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "models/actionsmodel.h"
#include "server.h"
@@ -88,8 +90,8 @@ void ActionsTest::testActions()
QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache(this);
cache->setText(command);
auto cache = new ChatBarCache(room);
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)};
auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result));

View File

@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "blockcache.h"
#include "enums/messagecomponenttype.h"
using namespace Block;
class BlockCacheTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void toStringTest_data();
void toStringTest();
};
void BlockCacheTest::toStringTest_data()
{
QTest::addColumn<QString>("inputString");
QTest::addColumn<MessageComponentType::Type>("itemType");
QTest::addColumn<QString>("outputstring");
QTest::newRow("plainText") << u"test string"_s << MessageComponentType::Text << u"test string"_s;
QTest::newRow("list") << u"- list 1\n- list 2\n- list 3\n"_s << MessageComponentType::Text << u"- list 1\n- list 2\n- list 3"_s;
QTest::newRow("code") << u"for (some code) {\n\n do something\n\n}"_s << MessageComponentType::Code
<< u"```\nfor (some code) {\n do something\n}\n```"_s;
QTest::newRow("quote") << u"\"this is a quote\""_s << MessageComponentType::Quote << u"> this is a quote"_s;
QTest::newRow("heading") << u"# heading\n\nnext line"_s << MessageComponentType::Text << u"# heading\n\nnext line"_s;
}
void BlockCacheTest::toStringTest()
{
QFETCH(QString, inputString);
QFETCH(MessageComponentType::Type, itemType);
QFETCH(QString, outputstring);
Cache cache;
cache += CacheItem{
.type = itemType,
.content = QTextDocumentFragment::fromMarkdown(inputString),
};
QCOMPARE(cache.toString(), outputstring);
}
QTEST_MAIN(BlockCacheTest)
#include "blockcachetest.moc"

View File

@@ -14,6 +14,7 @@
#include <KLocalizedString>
#include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h"
#include "neochatroom.h"
@@ -36,8 +37,6 @@ private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void replyMissingUser();
void edit();
@@ -77,7 +76,7 @@ void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->cache().toString(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
@@ -87,47 +86,14 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
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(eventId);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
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(eventId);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->isEditing(), false);
@@ -141,11 +107,11 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::replyMissingUser()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->isEditing(), false);
@@ -172,7 +138,7 @@ void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setAttachmentPath(u"some/path"_s);
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
@@ -180,7 +146,7 @@ void ChatBarCacheTest::edit()
});
chatBarCache->setEditId(eventId);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
@@ -193,11 +159,11 @@ void ChatBarCacheTest::edit()
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setEditId(eventId);
chatBarCache->setAttachmentPath(u"some/path"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);

View File

@@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatKeyHelperTest"
TextEdit {
id: textEdit
Keys.onPressed: (event) => {
event.accepted = testHelper.keyHelper.handleKey(event.key, event.modifiers);
}
}
ChatTextItemHelper {
id: textItemHelper
textItem: textEdit
}
ChatKeyHelperTestHelper {
id: testHelper
textItem: textItemHelper
}
SignalSpy {
id: spyUp
target: testHelper.keyHelper
signalName: "unhandledUp"
}
SignalSpy {
id: spyDown
target: testHelper.keyHelper
signalName: "unhandledDown"
}
SignalSpy {
id: spyDelete
target: testHelper.keyHelper
signalName: "unhandledDelete"
}
SignalSpy {
id: spyBackSpace
target: testHelper.keyHelper
signalName: "unhandledBackspace"
}
function init(): void {
textEdit.clear();
spyUp.clear();
spyDown.clear();
spyDelete.clear();
spyBackSpace.clear();
textEdit.forceActiveFocus();
}
function cleanupTestCase(): void {
testHelper.textItem = null;
textItemHelper.textItem = null;
}
function test_upDown(): void {
textEdit.insert(0, "line 1\nline 2\nline 3")
textEdit.cursorPosition = 0;
keyClick(Qt.Key_Up);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 1);
}
}

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QTextDocumentFragment>
#include "chatkeyhelper.h"
#include "chattextitemhelper.h"
class ChatKeyHelperTestHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT)
public:
explicit ChatKeyHelperTestHelper(QObject *parent = nullptr)
: QObject(parent)
, m_keyHelper(new ChatKeyHelper(this))
{
}
ChatTextItemHelper *textItem() const
{
return m_keyHelper->textItem();
}
void setTextItem(ChatTextItemHelper *textItem)
{
if (textItem == m_keyHelper->textItem()) {
return;
}
m_keyHelper->setTextItem(textItem);
Q_EMIT textItemChanged();
}
ChatKeyHelper *keyHelper() const
{
return m_keyHelper;
}
Q_SIGNALS:
void textItemChanged();
private:
QPointer<ChatKeyHelper> m_keyHelper;
};

View File

@@ -0,0 +1,173 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatMarkdownHelperTest"
TextEdit {
id: textEdit
textFormat: TextEdit.RichText
}
TextEdit {
id: textEdit2
}
ChatMarkdownHelperTestWrapper {
id: chatMarkdownHelper
textItem: textEdit
}
SignalSpy {
id: spyItem
target: chatMarkdownHelper
signalName: "textItemChanged"
}
SignalSpy {
id: spyUnhandledFormat
target: chatMarkdownHelper
signalName: "unhandledBlockFormat"
}
function initTestCase(): void {
textEdit.forceActiveFocus();
}
function cleanup(): void {
chatMarkdownHelper.clear();
compare(chatMarkdownHelper.checkText(""), true);
compare(chatMarkdownHelper.checkFormats([]), true);
compare(textEdit.cursorPosition, 0);
}
function test_item(): void {
spyItem.clear();
compare(chatMarkdownHelper.textItem, textEdit);
chatMarkdownHelper.textItem = textEdit2;
compare(chatMarkdownHelper.textItem, textEdit2);
chatMarkdownHelper.textItem = textEdit;
compare(chatMarkdownHelper.textItem, textEdit);
}
function test_textFormat_data() {
return [
{tag: "bold", input: "**b** ", outText: ["*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold], [RichFormat.Bold], []], unhandled: 0},
{tag: "italic", input: "*i* ", outText: ["*", "i", "i*", "i "], outFormats: [[], [RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
{tag: "heading 1", input: "# h", outText: ["#", "# ", "h"], outFormats: [[], [], [RichFormat.Bold, RichFormat.Heading1]], unhandled: 0},
{tag: "heading 2", input: "## h", outText: ["#", "##", "## ", "h"], outFormats: [[], [], [], [RichFormat.Bold, RichFormat.Heading2]], unhandled: 0},
{tag: "heading 3", input: "### h", outText: ["#", "##", "###", "### ", "h"], outFormats: [[], [], [], [], [RichFormat.Bold, RichFormat.Heading3]], unhandled: 0},
{tag: "heading 4", input: "#### h", outText: ["#", "##", "###", "####", "#### ", "h"], outFormats: [[], [], [], [], [], [RichFormat.Bold, RichFormat.Heading4]], unhandled: 0},
{tag: "heading 5", input: "##### h", outText: ["#", "##", "###", "####", "#####", "##### ", "h"], outFormats: [[], [], [], [], [], [], [RichFormat.Bold, RichFormat.Heading5]], unhandled: 0},
{tag: "heading 6", input: "###### h", outText: ["#", "##", "###", "####", "#####", "######", "###### ", "h"], outFormats: [[], [], [], [], [], [] ,[], [RichFormat.Bold, RichFormat.Heading6]], unhandled: 0},
{tag: "quote", input: "> q", outText: [">", "> ", "q"], outFormats: [[], [], []], unhandled: 1},
{tag: "quote - no space", input: ">q", outText: [">", "q"], outFormats: [[], [], []], unhandled: 1},
{tag: "unorderedlist 1", input: "* l", outText: ["*", "* ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "unorderedlist 2", input: "- l", outText: ["-", "- ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "orderedlist 1", input: "1. l", outText: ["1", "1.", "1. ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
{tag: "orderedlist 2", input: "1) l", outText: ["1", "1)", "1) ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
{tag: "inline code", input: "`c` ", outText: ["`", "c", "c`", "c "], outFormats: [[], [RichFormat.InlineCode], [RichFormat.InlineCode], []], unhandled: 0},
{tag: "code", input: "``` ", outText: ["`", "``", "```", " "], outFormats: [[], [], [], []], unhandled: 1},
{tag: "strikethrough", input: "~~s~~ ", outText: ["~", "~~", "s", "s~", "s~~", "s "], outFormats: [[], [], [RichFormat.Strikethrough], [RichFormat.Strikethrough], [RichFormat.Strikethrough], []], unhandled: 0},
{tag: "underline", input: "_u_ ", outText: ["_", "u", "u_", "u "], outFormats: [[], [RichFormat.Underline], [RichFormat.Underline], []], unhandled: 0},
{tag: "multiple closable", input: "***_~~t~~_*** ", outText: ["*", "**", "*", "_", "~", "~~", "t", "t~", "t~~", "t_", "t*", "t**", "t*", "t "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
{tag: "nonclosable closable", input: "* **b** ", outText: ["*", "* ", "*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.UnorderedList], [RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "not at line start", input: " 1) ", outText: [" ", " 1", " 1)", " 1) "], outFormats: [[], [], [], []], unhandled: 0},
]
}
function test_textFormat(data): void {
spyUnhandledFormat.clear();
compare(spyUnhandledFormat.count, 0);
for (let i = 0; i < data.input.length; i++) {
keyClick(data.input[i]);
compare(chatMarkdownHelper.checkText(data.outText[i]), true);
compare(chatMarkdownHelper.checkFormats(data.outFormats[i]), true);
}
compare(spyUnhandledFormat.count, data.unhandled);
}
function test_backspace(): void {
keyClick("*");
compare(chatMarkdownHelper.checkText("*"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("**"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("b");
compare(chatMarkdownHelper.checkText("b"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("o");
compare(chatMarkdownHelper.checkText("bo"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("l");
compare(chatMarkdownHelper.checkText("bol"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("d");
compare(chatMarkdownHelper.checkText("bold"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(Qt.Key_Backspace);
compare(chatMarkdownHelper.checkText("bol"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(Qt.Key_Backspace);
compare(chatMarkdownHelper.checkText("bo"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("bo*"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("bo**"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(" ");
compare(chatMarkdownHelper.checkText("bo "), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
function test_cursorMove(): void {
keyClick("t");
keyClick("e");
keyClick("s");
keyClick("t");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("*");
keyClick("*");
keyClick("b");
compare(chatMarkdownHelper.checkText("testb"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
textEdit.cursorPosition = 2;
keyClick("*");
keyClick("*");
keyClick("b");
compare(chatMarkdownHelper.checkText("tebstb"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
function test_insertText(): void {
textEdit.insert(0, "test");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.insert(4, "**b");
compare(chatMarkdownHelper.checkText("test**b"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.clear();
textEdit.insert(0, "test");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.insert(2, "**b");
compare(chatMarkdownHelper.checkText("te**bst"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
}

View File

@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QTextCursor>
#include "chatmarkdownhelper.h"
#include "chattextitemhelper.h"
#include "enums/richformat.h"
class ChatMarkdownHelperTestWrapper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QML text Item the ChatMerkdownHelper is handling.
*/
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
public:
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
: QObject(parent)
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
, m_textItem(new ChatTextItemHelper(this))
{
m_chatMarkdownHelper->setTextItem(m_textItem);
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
}
QQuickItem *textItem() const
{
return m_textItem->textItem();
}
void setTextItem(QQuickItem *textItem)
{
m_textItem->setTextItem(textItem);
}
Q_INVOKABLE bool checkText(const QString &text)
{
const auto doc = m_textItem->document();
if (!doc) {
return false;
}
return text == doc->toPlainText();
}
Q_INVOKABLE bool checkFormats(QList<RichFormat::Format> formats)
{
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
return RichFormat::formatsAtCursor(cursor) == formats;
}
Q_INVOKABLE void clear()
{
auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return;
}
cursor.select(QTextCursor::Document);
cursor.removeSelectedText();
cursor.setBlockCharFormat(RichFormat::charFormatForFormat(RichFormat::Paragraph));
cursor.setBlockFormat(RichFormat::blockFormatForFormat(RichFormat::Paragraph));
}
Q_SIGNALS:
void textItemChanged();
void unhandledBlockFormat(RichFormat::Format format);
private:
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
QPointer<ChatTextItemHelper> m_textItem;
};

View File

@@ -0,0 +1,301 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatTextItemHelperTest"
TextEdit {
id: textEdit
}
TextEdit {
id: textEdit2
}
ChatTextItemHelper {
id: textItemHelper
textItem: textEdit
}
ChatTextItemHelperTestHelper {
id: testHelper
textItem: textItemHelper
}
SignalSpy {
id: spyItem
target: textItemHelper
signalName: "textItemChanged"
}
SignalSpy {
id: spyContentsChanged
target: textItemHelper
signalName: "contentsChanged"
}
SignalSpy {
id: spyContentsChange
target: textItemHelper
signalName: "contentsChange"
}
SignalSpy {
id: spyCursor
target: textItemHelper
signalName: "cursorPositionChanged"
}
function init(): void {
testHelper.setFixedChars("", "");
textEdit.clear();
textEdit2.clear();
spyItem.clear();
spyContentsChange.clear();
spyContentsChanged.clear();
spyCursor.clear();
}
function cleanupTestCase(): void {
testHelper.textItem = null;
textItemHelper.textItem = null;
}
function test_item(): void {
compare(textItemHelper.textItem, textEdit);
compare(spyItem.count, 0);
textItemHelper.textItem = textEdit2;
compare(textItemHelper.textItem, textEdit2);
compare(spyItem.count, 1);
textItemHelper.textItem = textEdit;
compare(textItemHelper.textItem, textEdit);
compare(spyItem.count, 2);
}
function test_fixedChars(): void {
textEdit.forceActiveFocus();
testHelper.setFixedChars("1", "2");
compare(textEdit.text, "12");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 0);
keyClick("b");
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 1);
keyClick(Qt.Key_Left);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 2);
keyClick(Qt.Key_Left);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 3);
keyClick(Qt.Key_Right);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 4);
keyClick(Qt.Key_Right);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 5);
}
function test_document(): void {
// We can't get to the QTextDocument from QML so we have to use a helper function.
compare(testHelper.compareDocuments(textEdit.textDocument), true);
textEdit.insert(0, "test text");
compare(testHelper.lineCount(), 1);
textEdit.insert(textEdit.text.length, "\ntest text");
compare(testHelper.lineCount(), 2);
textEdit.clear()
compare(textEdit.text.length, 0);
}
function test_takeFirstBlock(): void {
textEdit.insert(0, "test text");
compare(testHelper.firstBlockText(), "test text");
compare(textEdit.text.length, 0);
textEdit.insert(0, "test text\nmore test text");
compare(testHelper.firstBlockText(), "test text");
compare(textEdit.text, "more test text");
compare(testHelper.firstBlockText(), "more test text");
compare(textEdit.text, "");
compare(textEdit.text.length, 0);
}
function test_fillFragments(): void {
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
textEdit.clear();
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
textEdit.cursorPosition = 16;
compare(testHelper.checkFragments("before fragment", "mid fragment", "after fragment"), true);
textEdit.clear();
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
textEdit.cursorPosition = 29;
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
textEdit.clear();
}
function test_insertFragment(): void {
testHelper.insertFragment("test text");
compare(textEdit.text, "test text");
compare(textEdit.cursorPosition, 9);
testHelper.insertFragment("beginning ", 1);
compare(textEdit.text, "beginning test text");
compare(textEdit.cursorPosition, 10);
testHelper.insertFragment(" end", 2);
compare(textEdit.text, "beginning test text end");
compare(textEdit.cursorPosition, 23);
textEdit.clear();
testHelper.insertFragment("test text", 0, true);
compare(textEdit.text, "test text");
compare(textEdit.cursorPosition, 0);
}
function test_cursor(): void {
// We can't get to the QTextCursor from QML so we have to use a helper function.
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get the appropriate content and cursor change signals when inserting text.
textEdit.insert(0, "test text")
compare(spyContentsChange.count, 1);
compare(spyContentsChange.signalArguments[0][0], 0);
compare(spyContentsChange.signalArguments[0][1], 0);
compare(spyContentsChange.signalArguments[0][2], 9);
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 1);
compare(spyCursor.signalArguments[0][0], true);
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get only get a cursor change signal when moving the cursor.
textEdit.cursorPosition = 4;
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 2);
compare(spyCursor.signalArguments[1][0], false);
textEdit.selectAll();
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 2);
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get the appropriate content and cursor change signals when removing text.
textEdit.clear();
compare(spyContentsChange.count, 2);
compare(spyContentsChange.signalArguments[1][0], 0);
compare(spyContentsChange.signalArguments[1][1], 9);
compare(spyContentsChange.signalArguments[1][2], 0);
compare(spyContentsChanged.count, 2);
compare(spyCursor.count, 3);
compare(spyCursor.signalArguments[2][0], true);
}
function test_setCursor(): void {
textEdit.insert(0, "test text");
compare(textEdit.cursorPosition, 9);
compare(spyCursor.count, 1);
testHelper.setCursorPosition(5);
compare(textEdit.cursorPosition, 5);
compare(spyCursor.count, 2);
testHelper.setCursorPosition(1);
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 3);
textEdit.cursorVisible = false;
compare(textEdit.cursorVisible, false);
testHelper.setCursorVisible(true);
compare(textEdit.cursorVisible, true);
testHelper.setCursorVisible(false);
compare(textEdit.cursorVisible, false);
}
function test_setCursorFromTextItem(): void {
textEdit.insert(0, "line 1\nline 2");
textEdit2.insert(0, "line 1\nline 2");
testHelper.setCursorFromTextItem(textEdit2, false, 0);
compare(textEdit.cursorPosition, 7);
testHelper.setCursorFromTextItem(textEdit2, true, 7);
compare(textEdit.cursorPosition, 0);
testHelper.setCursorFromTextItem(textEdit2, false, 1);
compare(textEdit.cursorPosition, 8);
testHelper.setCursorFromTextItem(textEdit2, true, 8);
compare(textEdit.cursorPosition, 1);
testHelper.setFixedChars("1", "2");
testHelper.setCursorFromTextItem(textEdit2, false, 0);
compare(textEdit.cursorPosition, 8);
testHelper.setCursorFromTextItem(textEdit2, true, 7);
compare(textEdit.cursorPosition, 1);
}
function test_mergeFormat(): void {
textEdit.insert(0, "lots of text");
testHelper.setCursorPosition(0);
testHelper.mergeFormatOnCursor(RichFormat.Bold);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold]), true);
testHelper.mergeFormatOnCursor(RichFormat.Italic);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Italic]), true);
testHelper.setCursorPosition(6);
compare(testHelper.checkFormatsAtCursor([]), true);
testHelper.mergeFormatOnCursor(RichFormat.Underline);
compare(testHelper.checkFormatsAtCursor([RichFormat.Underline]), true);
testHelper.setCursorPosition(9);
compare(testHelper.checkFormatsAtCursor([]), true);
testHelper.mergeFormatOnCursor(RichFormat.Strikethrough);
compare(testHelper.checkFormatsAtCursor([RichFormat.Strikethrough]), true);
textEdit.clear();
textEdit.insert(0, "heading");
testHelper.mergeFormatOnCursor(RichFormat.Heading1);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading1]), true);
testHelper.mergeFormatOnCursor(RichFormat.Heading2);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading2]), true);
testHelper.mergeFormatOnCursor(RichFormat.Paragraph);
compare(testHelper.checkFormatsAtCursor([]), true);
textEdit.clear();
textEdit.insert(0, "text");
testHelper.mergeFormatOnCursor(RichFormat.UnorderedList);
compare(testHelper.checkFormatsAtCursor([RichFormat.UnorderedList]), true);
compare(testHelper.markdownText(), "- text");
testHelper.mergeFormatOnCursor(RichFormat.OrderedList);
compare(testHelper.checkFormatsAtCursor([RichFormat.OrderedList]), true);
compare(testHelper.markdownText(), "1. text");
textEdit.clear();
}
function test_list(): void {
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), false);
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), false);
}
function test_forceActiveFocus(): void {
textEdit2.forceActiveFocus();
compare(textEdit.activeFocus, false);
testHelper.forceActiveFocus();
compare(textEdit.activeFocus, true);
}
}

View File

@@ -0,0 +1,218 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QTextDocumentFragment>
#include "chattextitemhelper.h"
class ChatTextItemHelperTestHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QML text Item the TextItemHelper is handling.
*/
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
public:
explicit ChatTextItemHelperTestHelper(QObject *parent = nullptr)
: QObject(parent)
{
}
ChatTextItemHelper *textItem() const
{
return m_textItem;
}
void setTextItem(ChatTextItemHelper *textItem)
{
if (textItem == m_textItem) {
return;
}
m_textItem = textItem;
Q_EMIT textItemChanged();
}
Q_INVOKABLE void setFixedChars(const QString &startChars, const QString &endChars)
{
if (!m_textItem) {
return;
}
m_textItem->setFixedChars(startChars, endChars);
}
Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document)
{
if (!m_textItem) {
return false;
}
return document->textDocument() == m_textItem->document();
}
Q_INVOKABLE int lineCount()
{
if (!m_textItem) {
return -1;
}
return m_textItem->lineCount();
}
Q_INVOKABLE QString firstBlockText()
{
if (!m_textItem) {
return {};
}
return m_textItem->takeFirstBlock().toPlainText();
}
Q_INVOKABLE bool checkFragments(const QString &before, const QString &mid, const QString &after)
{
if (!m_textItem) {
return false;
}
bool hasBefore = false;
QTextDocumentFragment midFragment;
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
m_textItem->fillFragments(hasBefore, midFragment, afterFragment);
return hasBefore && m_textItem->document()->toPlainText() == before && midFragment.toPlainText() == mid && after.isEmpty()
? !afterFragment
: afterFragment->toPlainText() == after;
}
Q_INVOKABLE void insertFragment(const QString &text, ChatTextItemHelper::InsertPosition position = ChatTextItemHelper::Cursor, bool keepPosition = false)
{
if (!m_textItem) {
return;
}
const auto fragment = QTextDocumentFragment::fromPlainText(text);
m_textItem->insertFragment(fragment, position, keepPosition);
}
Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd)
{
if (!m_textItem) {
return false;
}
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
const auto posSame = cursor.position() == cursorPosition;
const auto startSame = cursor.selectionStart() == selectionStart;
const auto endSame = cursor.selectionEnd() == selectionEnd;
return posSame && startSame && endSame;
}
Q_INVOKABLE int cursorPosition() const
{
if (!m_textItem) {
return -1;
}
return *m_textItem->cursorPosition();
}
Q_INVOKABLE void setCursorPosition(int pos)
{
if (!m_textItem) {
return;
}
m_textItem->setCursorPosition(pos);
}
Q_INVOKABLE void setCursorVisible(bool visible)
{
if (!m_textItem) {
return;
}
m_textItem->setCursorVisible(visible);
}
Q_INVOKABLE void setCursorFromTextItem(QQuickItem *item, bool infront, int cursorPos)
{
if (!m_textItem) {
return;
}
const auto textItem = new ChatTextItemHelper(this);
textItem->setTextItem(item);
textItem->setCursorPosition(cursorPos);
m_textItem->setCursorFromTextItem(textItem, infront);
textItem->deleteLater();
}
Q_INVOKABLE void mergeFormatOnCursor(RichFormat::Format format)
{
if (!m_textItem) {
return;
}
m_textItem->mergeFormatOnCursor(format);
}
Q_INVOKABLE bool checkFormatsAtCursor(QList<RichFormat::Format> formats)
{
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
return RichFormat::formatsAtCursor(cursor) == formats;
}
Q_INVOKABLE bool canIndentListMoreAtCursor() const
{
if (!m_textItem) {
return false;
}
return m_textItem->canIndentListMoreAtCursor();
}
Q_INVOKABLE bool canIndentListLessAtCursor() const
{
if (!m_textItem) {
return false;
}
return m_textItem->canIndentListLessAtCursor();
}
Q_INVOKABLE void indentListMoreAtCursor()
{
if (!m_textItem) {
return;
}
m_textItem->indentListMoreAtCursor();
}
Q_INVOKABLE void indentListLessAtCursor()
{
if (!m_textItem) {
return;
}
m_textItem->indentListLessAtCursor();
}
Q_INVOKABLE void forceActiveFocus() const
{
if (!m_textItem) {
return;
}
m_textItem->forceActiveFocus();
}
Q_INVOKABLE QString markdownText() const
{
if (!m_textItem) {
return {};
}
return m_textItem->markdownText();
}
Q_SIGNALS:
void textItemChanged();
private:
QPointer<ChatTextItemHelper> m_textItem;
};

View File

@@ -29,6 +29,7 @@
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/messagecontentfiltermodel.h"
#include "models/messagecontentmodel.h"
#include "models/notificationsmodel.h"
#include "models/permissionsmodel.h"
#include "models/pinnedmessagemodel.h"
@@ -179,7 +180,7 @@ void ModelTest::testRoomTreeModel()
void ModelTest::testMessageContentModel()
{
auto contentModel = std::make_unique<MessageContentModel>(room, nullptr, eventId);
auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
tester->setUseFetchMore(true);
}
@@ -193,21 +194,23 @@ void ModelTest::testEventMessageContentModel()
void ModelTest::testThreadModel()
{
auto model = new ThreadModel(eventId, room);
auto tester = new QAbstractItemModelTester(model, model);
auto model = std::make_unique<ThreadModel>(eventId, room);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testThreadFetchModel()
{
auto model = new ThreadFetchModel(new ThreadModel(eventId, room));
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
auto model = new ThreadFetchModel(threadModel.get());
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadChatBarModel()
{
auto model = new ThreadChatBarModel(new ThreadModel(eventId, room), room);
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
auto model = new ThreadChatBarModel(threadModel.get(), room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
@@ -397,9 +400,7 @@ 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);

9
autotests/qmltest.cpp Normal file
View File

@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <quicktest.h>
QUICK_TEST_MAIN(NeoChat)

View File

@@ -121,7 +121,7 @@ void Server::start()
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly));
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).constFirst());
m_sslServer.setSslConfiguration(config);
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
qFatal() << "Server failed to listen on a port.";
@@ -227,7 +227,8 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
for (const auto &change : m_state.mid(token)) {
const auto changes = m_state.mid(token);
for (const auto &change : changes) {
for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents;
stateEvents += QJsonObject{
@@ -272,7 +273,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
for (const auto &invitation : change.invitations) {
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
@@ -299,7 +300,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
for (const auto &ban : change.bans) {
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
@@ -326,7 +327,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
for (const auto &join : change.joins) {
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
@@ -353,7 +354,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
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.
@@ -365,7 +366,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();

View File

@@ -73,6 +73,16 @@ void WindowControllerTest::toggle()
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), false);
// make sure we restore maximized state when toggling
instance.toggleWindow();
window.setVisibility(QWindow::Maximized);
QCOMPARE(window.windowState(), Qt::WindowMaximized);
instance.toggleWindow();
QCOMPARE(window.isVisible(), false);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowMaximized);
QCOMPARE(window.isVisible(), true);
}
QTEST_MAIN(WindowControllerTest)

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

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/AttachmentPane.qml
qml/QuickFormatBar.qml
qml/UserDetailDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
qml/ConfirmLogoutDialog.qml
qml/VerificationMessage.qml
@@ -79,7 +78,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EmojiSas.qml
qml/VerificationCanceled.qml
qml/MessageSourceSheet.qml
qml/LocationChooser.qml
qml/InvitationView.qml
qml/AvatarTabButton.qml
qml/OsmLocationPlugin.qml
@@ -105,7 +103,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
qml/NewPollDialog.qml
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
@@ -174,12 +171,7 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
target_link_libraries(neochat PUBLIC KF6::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif()

View File

@@ -29,10 +29,8 @@
#include "proxycontroller.h"
#include "roommanager.h"
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
#if !defined(Q_OS_ANDROID)
#include "trayicon.h"
#elif !defined(Q_OS_ANDROID)
#include "trayicon_sni.h"
#endif
#ifdef HAVE_KUNIFIEDPUSH

View File

@@ -227,6 +227,7 @@ int main(int argc, char *argv[])
Registration::instance().setAccountManager(accountManager.get());
qml_register_types_org_kde_neochat();
qmlRegisterUncreatableMetaObject(Quotient::staticMetaObject, "Quotient", 1, 0, "JoinRule", u"Access to JoinRule enum only"_s);
QQmlApplicationEngine engine;

View File

@@ -74,6 +74,11 @@ QHash<int, QByteArray> CommonRoomsModel::roleNames() const
};
}
bool CommonRoomsModel::loading() const
{
return m_loading;
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {
@@ -89,15 +94,26 @@ void CommonRoomsModel::reload()
return;
}
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
});
m_loading = true;
Q_EMIT loadingChanged();
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId)
.then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
m_loading = false;
Q_EMIT loadingChanged();
})
.onFailure([this] {
m_loading = false;
Q_EMIT loadingChanged();
});
}
#include "moc_commonroomsmodel.cpp"

View File

@@ -21,6 +21,7 @@ class CommonRoomsModel : public QAbstractListModel
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
enum Roles {
@@ -43,10 +44,13 @@ public:
QHash<int, QByteArray> roleNames() const override;
bool loading() const;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();
void countChanged();
void loadingChanged();
private:
void reload();
@@ -54,4 +58,5 @@ private:
QPointer<NeoChatConnection> m_connection;
QString m_userId;
QList<QString> m_commonRooms;
bool m_loading = false;
};

View File

@@ -207,10 +207,6 @@
</entry>
</group>
<group name="FeatureFlags">
<entry name="Threads" type="bool">
<label>Enable threads</label>
<default>false</default>
</entry>
<entry name="Phone3PId" type="bool">
<label>Enable add phone numbers as 3PIDs</label>
<default>false</default>

View File

@@ -24,17 +24,9 @@ KirigamiComponents.ConvergentContextMenu {
}
Kirigami.Action {
text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
(Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: "https://matrix.to/#/" + root.connection.localUser.id,
title: root.connection.localUser.displayName,
subtitle: root.connection.localUser.id,
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : ""
}) as QrCodeMaximizeComponent).open();
}
text: i18nc("@action:button", "Open Profile")
icon.name: "im-user-symbolic"
onTriggered: RoomManager.resolveResource(root.connection.localUserId, "qr") // Use "qr" action to make sure a room isn't passed, see RoomManager::visitUser
}
Kirigami.Action {
@@ -97,9 +89,9 @@ KirigamiComponents.ConvergentContextMenu {
text: i18nc("@action:inmenu Open support dialog", "Support")
icon.name: "help-contents-symbolic"
onTriggered: {
Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
(Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection,
}).open();
}) as SupportDialog).open();
}
}

View File

@@ -21,6 +21,7 @@ Delegates.RoundedItemDelegate {
signal contextMenuRequested
signal selected
activeFocusOnTab: true
padding: Kirigami.Units.largeSpacing
QQC2.ToolTip.visible: hovered

View File

@@ -4,9 +4,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.neochat

View File

@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
property NeochatRoomMember author
/**
* @brief The timestamp of the event as a NeoChatDateTime.
* @brief The timestamp of the event as a neoChatDateTime.
*/
required property NeoChatDateTime dateTime
required property neoChatDateTime dateTime
/**
* @brief The code text to show.
@@ -79,7 +79,7 @@ Components.AbstractMaximizeComponent {
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.TextArea {
id: codeText
id: codeTextEdit
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
@@ -100,15 +100,15 @@ Components.AbstractMaximizeComponent {
SyntaxHighlighter {
property string definitionName: Repository.definitionForName(root.language).name
textEdit: definitionName == "None" ? null : codeText
textEdit: definitionName == "None" ? null : codeTextEdit
definition: definitionName
}
ColumnLayout {
id: lineNumberColumn
anchors {
top: codeText.top
topMargin: codeText.topPadding + 1
left: codeText.left
top: codeTextEdit.top
topMargin: codeTextEdit.topPadding + 1
left: codeTextEdit.left
leftMargin: Kirigami.Units.smallSpacing
}
spacing: 0
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
id: repeater
model: LineModel {
id: lineModel
Component.onCompleted: setDocument(codeText.textDocument)
Component.onCompleted: setDocument(codeTextEdit.textDocument)
}
delegate: QQC2.Label {
id: label
@@ -150,4 +150,6 @@ Components.AbstractMaximizeComponent {
color: Kirigami.Theme.backgroundColor
}
}
onOpened: forceActiveFocus()
}

View File

@@ -3,8 +3,6 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQml.Models
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents

View File

@@ -16,19 +16,18 @@ Kirigami.PromptDialog {
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
standardButtons: QQC2.Dialog.Cancel
onRejected: {
root.close();
}
onAccepted: root.room.forget()
footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
text: i18nc("@action:button", "Leave Room")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
text: i18nc("@action:button Leave this room/space", "Leave")
icon.name: "arrow-left-symbolic"
onClicked: root.room.forget();
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
}

View File

@@ -15,22 +15,16 @@ Kirigami.PromptDialog {
title: i18nc("@title:dialog", "Sign out")
subtitle: i18n("Are you sure you want to sign out?")
dialogType: Kirigami.PromptDialog.Warning
standardButtons: QQC2.Dialog.Cancel
onRejected: {
root.close();
}
onAccepted: root.connection.logout(true)
footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button {
text: i18nc("@action:button", "Sign out")
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.connection.logout(true);
root.close();
root.accepted();
}
}
}
}

View File

@@ -17,12 +17,5 @@ Kirigami.PromptDialog {
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
onAccepted: {
Qt.openUrlExternally(root.link);
root.close();
}
onRejected: {
root.close();
}
onAccepted: Qt.openUrlExternally(root.link)
}

View File

@@ -41,13 +41,11 @@ ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They match")
icon.name: "dialog-ok"
onClicked: root.accept()
}
QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They don't match")
icon.name: "dialog-cancel"
onClicked: root.reject()

View File

@@ -9,8 +9,6 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat
Delegates.RoundedItemDelegate {
id: root

View File

@@ -166,7 +166,7 @@ ColumnLayout {
}
RowLayout {
visible: root.currentRoom.connection.canCheckMutualRooms
visible: (root.currentRoom.connection as NeoChatConnection).canCheckMutualRooms
spacing: 0
Layout.topMargin: Kirigami.Units.largeSpacing * 2

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -8,7 +10,6 @@ import QtQuick.Window
import QtQml
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.Page {

View File

@@ -100,7 +100,7 @@ Kirigami.ApplicationWindow {
function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
let roomPage = root.pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
roomPage.forceActiveFocus();
roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom();
@@ -151,8 +151,6 @@ Kirigami.ApplicationWindow {
}
contextDrawer: RoomDrawer {
id: contextDrawer
// This is a memory for all user initiated actions on the drawer, i.e. clicking the button
// It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
@@ -178,9 +176,9 @@ Kirigami.ApplicationWindow {
// Connect to the onClicked function of the RoomDrawer handle button
Connections {
target: contextDrawer.handle.children[0]
target: root.contextDrawer.handle.children[0]
function onClicked() {
contextDrawer.drawerUserState = contextDrawer.drawerOpen;
root.contextDrawer.drawerUserState = root.contextDrawer.drawerOpen;
}
}

View File

@@ -4,6 +4,7 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
@@ -24,6 +25,8 @@ Kirigami.Dialog {
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
title: i18nc("@title", "Manually Enter a Room")
showCloseButton: false
standardButtons: QQC2.Dialog.Cancel
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
leftPadding: 0
@@ -31,35 +34,26 @@ Kirigami.Dialog {
topPadding: 0
bottomPadding: 0
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: [
Kirigami.Action {
enabled: roomIdAliasText.isValidText
text: i18n("OK")
icon.name: "dialog-ok"
onTriggered: {
// We don't necessarily have all the info so fill out the best we can.
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
let displayName = "";
let avatarUrl = "";
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
let topic = "";
let memberCount = -1;
let isJoined = false;
if (roomIdAliasText.room) {
roomId = roomIdAliasText.room.id;
displayName = roomIdAliasText.room.displayName;
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
alias = roomIdAliasText.room.canonicalAlias;
topic = roomIdAliasText.room.topic;
memberCount = roomIdAliasText.room.joinedCount;
isJoined = true;
}
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.close();
}
onAccepted: {
// We don't necessarily have all the info so fill out the best we can.
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
let displayName = "";
let avatarUrl = "";
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
let topic = "";
let memberCount = -1;
let isJoined = false;
if (roomIdAliasText.room) {
roomId = roomIdAliasText.room.id;
displayName = roomIdAliasText.room.displayName;
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
alias = roomIdAliasText.room.canonicalAlias;
topic = roomIdAliasText.room.topic;
memberCount = roomIdAliasText.room.joinedCount;
isJoined = true;
}
]
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
}
contentItem: ColumnLayout {
spacing: 0
@@ -110,4 +104,16 @@ Kirigami.Dialog {
roomIdAliasText.forceActiveFocus();
timer.restart();
}
footer: QQC2.DialogButtonBox {
QQC2.Button {
text: i18nc("@action:button Join this room/space", "Join")
icon.name: "checkmark"
enabled: roomIdAliasText.isValidText
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
}

View File

@@ -24,25 +24,16 @@ Kirigami.Dialog {
signal userSelected(string userId)
title: i18nc("@title", "User ID")
showCloseButton: false
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
standardButtons: QQC2.Dialog.Cancel
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: [
Kirigami.Action {
enabled: userIdText.isValidText
text: i18n("OK")
icon.name: "dialog-ok"
onTriggered: {
root.userSelected(userIdText.text)
root.accept();
}
}
]
onAccepted: root.userSelected(userIdText.text)
contentItem: ColumnLayout {
spacing: 0
@@ -79,4 +70,16 @@ Kirigami.Dialog {
userIdText.forceActiveFocus();
timer.restart();
}
footer: QQC2.DialogButtonBox {
QQC2.Button {
text: i18nc("@action:button Perform an action with this user ID", "Ok")
icon.name: "checkmark"
enabled: userIdText.isValidText
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
}

View File

@@ -1,7 +1,10 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
@@ -12,11 +15,16 @@ Kirigami.PromptDialog {
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.")
standardButtons: Kirigami.Dialog.Cancel
standardButtons: QQC2.Dialog.Cancel
customFooterActions: Kirigami.Action {
icon.name: "camera-video-symbolic"
text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
onTriggered: root.accept()
footer: QQC2.DialogButtonBox {
QQC2.Button {
icon.name: "camera-video-symbolic"
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
}

View File

@@ -18,7 +18,7 @@ Kirigami.Page {
required property NeoChatConnection connection
padding: 0
Component.onCompleted: camera.start()
Component.onCompleted: session.camera.start()
Connections {
target: root.QQC2.ApplicationWindow.window
@@ -66,12 +66,8 @@ Kirigami.Page {
CaptureSession {
id: session
camera: Camera {
id: camera
}
imageCapture: ImageCapture {
id: imageCapture
}
camera: Camera {}
imageCapture: ImageCapture {}
videoOutput: viewFinder
}
}

View File

@@ -30,7 +30,7 @@ Kirigami.Page {
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)
text: xi18nc("@info", "This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
}
QQC2.TextArea {

View File

@@ -80,6 +80,12 @@ Kirigami.Page {
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.
resetViewSettling();
}
// Resets the view settling of the timeline.
// This should be called whenever the apparent height of the timeline changes, or else the view will scroll on its own!
function resetViewSettling(): void {
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
@@ -104,8 +110,8 @@ Kirigami.Page {
enabled: hasExistingMeeting || canStartNewMeeting
visible: root.currentRoom && !root.currentRoom.isSpace
onTriggered: {
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting });
dialog.onAccepted.connect(doAction);
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting }) as MeetingDialog;
dialog.accepted.connect(doAction);
dialog.open();
}
@@ -172,7 +178,7 @@ Kirigami.Page {
spacing: 0
readonly property bool shouldShowPins: root.currentRoom.pinnedMessage.length > 0 && !Kirigami.Settings.isMobile
readonly property bool shouldShowPins: root.currentRoom?.pinnedMessage.length > 0 && !Kirigami.Settings.isMobile
QQC2.Control {
id: pinControl
@@ -215,7 +221,7 @@ Kirigami.Page {
}
TapHandler {
onTapped: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
onTapped: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
room: root.currentRoom
}, {
title: i18nc("@title", "Pinned Messages")
@@ -229,6 +235,58 @@ Kirigami.Page {
Layout.fillWidth: true
}
Kirigami.InlineMessage {
id: selectedMessagesControl
Layout.fillWidth: true
showCloseButton: false
visible: root.currentRoom?.selectedMessageCount > 0
position: Kirigami.InlineMessage.Position.Header
type: Kirigami.MessageType.Positive
icon.name: "edit-select-all-symbolic"
text: i18nc("@info", "Selected Messages: %1", root.currentRoom?.selectedMessageCount)
actions: [
Kirigami.Action {
text: i18nc("@action:button", "Copy Conversation")
icon.name: "edit-copy"
onTriggered: {
Clipboard.saveText(root.currentRoom.getFormattedSelectedMessages())
showPassiveNotification(i18nc("@info", "Conversation copied to clipboard"));
}
},
Kirigami.Action {
text: i18nc("@action:button", "Delete Messages")
icon.name: "trash-empty-symbolic"
icon.color: Kirigami.Theme.negativeTextColor
enabled: root.currentRoom?.canDeleteSelectedMessages
onTriggered: {
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing these messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete",
reporting: false,
connection: root.currentRoom.connection,
}, {
title: i18nc("@title:dialog", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.currentRoom.deleteSelectedMessages(reason);
});
}
},
Kirigami.Action {
icon.name: "dialog-close"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: root.currentRoom.clearSelectedMessages()
}
]
}
Kirigami.InlineMessage {
id: banner
@@ -303,7 +361,9 @@ Kirigami.Page {
id: chatBar
width: parent.width
currentRoom: root.currentRoom
connection: root.currentRoom.connection as NeoChatConnection
// Creating a reply (or doing anything in the chat bar) can change the height, but this isn't picked up on the root's onHeightChanged.
onHeightChanged: root.resetViewSettling()
}
}

View File

@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts

View File

@@ -13,7 +13,7 @@ FormCard.FormCardPage {
property bool processing: false
title: i18nc("@title:window", "Manage Secret Backup")
title: i18nc("@title:window", "Manage Key Storage")
topPadding: Kirigami.Units.gridUnit
leftPadding: 0
@@ -32,7 +32,7 @@ FormCard.FormCardPage {
function onKeyBackupError(): void {
securityKeyField.clear()
root.processing = false
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
banner.text = i18nc("@info:status", "The recovery key was not correct.")
banner.visible = true
}
@@ -45,20 +45,20 @@ FormCard.FormCardPage {
}
FormCard.FormHeader {
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
title: i18nc("@title", "Unlock using Recovery Key")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
description: i18nc("@info", "If you have a recovery key (also known as a “security key or backup passphrase”), enter it below or upload it as a file.")
}
FormCard.FormTextFieldDelegate {
id: securityKeyField
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
label: i18nc("@label:textbox", "Recovery Key:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
id: uploadSecurityKeyButton
text: i18nc("@action:button", "Upload from File")
text: i18nc("@action:button", "Upload From File")
icon.name: "cloud-upload"
enabled: !root.processing
onClicked: {
@@ -83,12 +83,12 @@ FormCard.FormCardPage {
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have previously verified this device, you can try loading the backup key from other devices by clicking the button below.")
description: i18nc("@info", "If you have previously verified this device, you request encryption keys from other verified devices.")
}
FormCard.FormButtonDelegate {
id: unlockCrossSigningButton
icon.name: "emblem-shared-symbolic"
text: i18nc("@action:button", "Request from other Devices")
text: i18nc("@action:button", "Request From Other Devices")
enabled: !root.processing
onClicked: {
root.processing = true

View File

@@ -8,7 +8,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.prison
import org.kde.neochat
@@ -126,7 +125,7 @@ Kirigami.Dialog {
text: root.shareUrl,
title: root.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarColor: root.room?.member(root.user.id).color ?? null,
avatarSource: avatar.source,
}) as QrCodeMaximizeComponent;
root.close();
@@ -402,18 +401,36 @@ Kirigami.Dialog {
Kirigami.Heading {
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
level: 4
visible: !root.isSelf && root.hasMutualRooms
visible: !root.isSelf && root.connection.canCheckMutualRooms
Layout.topMargin: Kirigami.Units.largeSpacing
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: !root.isSelf && root.hasMutualRooms
visible: !root.isSelf && root.connection.canCheckMutualRooms
Layout.topMargin: Kirigami.Units.smallSpacing
QQC2.BusyIndicator {
visible: roomRepeater.count === 0 && root.model.loading
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
}
QQC2.Label {
visible: roomRepeater.count === 0 && !root.model.loading
text: i18nc("@info:label", "No rooms in common")
verticalAlignment: Text.AlignVCenter
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.fillHeight: true
}
Repeater {
id: roomRepeater
model: root.limiterModel
delegate: KirigamiComponents.AvatarButton {

View File

@@ -16,7 +16,7 @@ KirigamiComponents.ConvergentContextMenu {
id: root
required property Kirigami.ApplicationWindow window
required property var author
required property NeochatRoomMember author
headerContentItem: RowLayout {
id: detailRow
@@ -68,7 +68,7 @@ KirigamiComponents.ConvergentContextMenu {
text: i18nc("@action:button", "Mention")
icon.name: "username-copy-symbolic"
onTriggered: {
RoomManager.currentRoom.mainCache.mentionAdded(root.author.id);
RoomManager.currentRoom.mainCache.mentionAdded(root.author.disambiguatedName, "https://matrix.to/#/" + root.author.id);
}
}
}

View File

@@ -129,15 +129,6 @@ RoomManager::RoomManager(QObject *parent)
m_messageFilterModel->invalidate();
}
});
ContentProvider::self().setThreadsEnabled(NeoChatConfig::threads());
MessageModel::setThreadsEnabled(NeoChatConfig::threads());
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this] {
ContentProvider::self().setThreadsEnabled(NeoChatConfig::threads());
MessageModel::setThreadsEnabled(NeoChatConfig::threads());
if (m_timelineModel) {
Q_EMIT m_timelineModel->threadsEnabledChanged();
}
});
connect(NeoChatConfig::self(), &NeoChatConfig::SortOrderChanged, this, [this]() {
m_sortFilterRoomTreeModel->invalidate();
});
@@ -546,13 +537,14 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
LastRoomBlocker blocker(this);
// 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()) {
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString());
!lastRoom.isEmpty() && m_connection->room(lastRoom)) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
// If no last room was opened, go to the space home:
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
if (!spaceId.isEmpty() && spaceId != u"DM"_s && m_connection->room(spaceId)) {
resolveResource(spaceId, "no_join"_L1);
return;
}
@@ -604,6 +596,7 @@ QString RoomManager::findSpaceIdForCurrentRoom() const
void RoomManager::setCurrentRoom(const QString &roomId)
{
if (m_currentRoom != nullptr) {
m_currentRoom->clearSelectedMessages();
m_currentRoom->disconnect(this);
}

View File

@@ -190,7 +190,7 @@ public:
*
* @sa Quotient::UriResolverBase::visitResource()
*/
Q_INVOKABLE void resolveResource(Uri uri, const QString &action = {});
Q_INVOKABLE void resolveResource(Quotient::Uri uri, const QString &action = {});
bool hasOpenRoom() const;
@@ -235,7 +235,7 @@ public:
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
viewEventMenu(QObject *parent, const Quotient::RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
@@ -316,7 +316,7 @@ Q_SIGNALS:
const QString &plainText,
const QString &richtText,
const QString &mimeType,
const FileTransferInfo &progressInfo,
const Quotient::FileTransferInfo &progressInfo,
const QString &selectedText,
const QString &hoveredLink);

View File

@@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
// SPDX-License-Identifier: GPL-3.0-only
#include "trayicon_sni.h"
#include <KWindowSystem>
#include "windowcontroller.h"
using namespace Qt::StringLiterals;
TrayIcon::TrayIcon(QObject *parent)
: KStatusNotifierItem(parent)
{
setCategory(KStatusNotifierItem::ItemCategory::Communications);
setIconByName(u"org.kde.neochat.tray"_s);
connect(&WindowController::instance(), &WindowController::windowChanged, this, [this] {
setAssociatedWindow(WindowController::instance().window());
});
}
void TrayIcon::show()
{
setStatus(Active);
}
void TrayIcon::hide()
{
setStatus(Passive);
}
#include "moc_trayicon_sni.cpp"

View File

@@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <KStatusNotifierItem>
/**
* @class TrayIcon
*
* A class inheriting KStatusNotifierItem to provide a tray icon.
*
* @sa KStatusNotifierItem
*/
class TrayIcon : public KStatusNotifierItem
{
Q_OBJECT
public:
explicit TrayIcon(QObject *parent = nullptr);
/**
* @brief Show the tray icon.
*/
void show();
/**
* @brief Hide the tray icon.
*/
void hide();
};

View File

@@ -32,6 +32,16 @@ void WindowController::setWindow(QWindow *window)
{
m_window = window;
if (window != nullptr) {
// to restore maximized state after reopening from the system tray
connect(m_window, &QWindow::windowStateChanged, this, [this]() {
if (m_window->isVisible()) {
m_wasMaximized = m_window->windowStates() & Qt::WindowMaximized;
}
});
m_wasMaximized = m_window->windowStates() & Qt::WindowMaximized;
}
Q_EMIT windowChanged();
}
@@ -46,7 +56,11 @@ void WindowController::showAndRaiseWindow(const QString &startupId)
return;
}
if (!m_window->isVisible()) {
m_window->show();
if (m_wasMaximized) {
m_window->showMaximized();
} else {
m_window->show();
}
}
#ifdef HAVE_WINDOWSYSTEM

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