Compare commits

...

156 Commits

Author SHA1 Message Date
Joshua Goins
8ff23239f7 Render placeholder avatars in notifications, like we do in app
Right now if someone or a room doesn't have an avatar, there's a good
chance it will end up falling back to the NeoChat app icon (not very
descriptive.) It also tends to break the flow of conversations, that's
the whole reason to have an avatar in the notification in the first
place.

So I made it render a similar-looking avatar like it does in-app, by
re-implementing it in QPainter. I also took the time to refactor and
clean up the avatar generation for notifications so the logic should be
easier to follow.

To reduce the maintenance required, we re-use some functionality from
Kirigami Add-ons. The rest of the drawing logic is custom but sensible,
but some creative liberty was used to ensure it looks decent.
2026-01-17 12:18:34 -05:00
Joshua Goins
5f20a86b62 Reduce the amount of items in the account menu
The devices entry gone (not even Element has this) and so is the logout
action. The Switch account menu item is moved to the bottom so its
easier to access by mouse. The "Open Secret Backup" is moved under
Security settings where it lives next to the other crypto-related
settings.
2026-01-17 11:03:38 -05:00
l10n daemon script
f2d3c9706e GIT_SILENT Sync po/docbooks with svn 2026-01-17 01:44:32 +00:00
Joshua Goins
39de4d10e4 Add hack around the timeline never settling just right
This is due to some kind of bug in ListView that never resettles
properly for bottom-to-top views. This can arise when the pinned message
is loaded (because that squishes the view) or the window is resized
(because that also resizes the view.)

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

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

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

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

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

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

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

See #707
2026-01-15 09:00:02 -05:00
Joshua Goins
1e461658b8 Hide "Reply in Thread" message action if we don't have threads enabled 2026-01-15 08:16:41 -05:00
l10n daemon script
f305cb849f GIT_SILENT Sync po/docbooks with svn 2026-01-15 01:52:21 +00:00
l10n daemon script
e1bbbfe4fd GIT_SILENT Sync po/docbooks with svn 2026-01-14 01:44:13 +00:00
Kai Uwe Broulik
7a2211f8e0 RoomPage: Fix selected text and hovered link in context menu
They were not forwarded to the menu.
Also, "isThread" argument is no longer there in the signal.
2026-01-13 17:37:30 -05:00
Azhar Momin
4155e9116a Fix some runtime qml warnings 2026-01-13 17:35:02 -05:00
Azhar Momin
a989ef42b2 Fix pushDialogLayer failing in DelegateContextMenu 2026-01-13 17:35:02 -05:00
Joshua Goins
2babf44b28 Grab the correct room in MessageModel::data
Not all events that are processed in this model belong to the room, e.g.
searching through multiple room versions. Now the model finds the
correct room based on the reported room in the event.

This fixes searching through upgraded rooms, and unblocks searching
through multiple rooms in the future.
2026-01-13 09:25:49 -05:00
Joshua Goins
89e42dbc53 Decrypt when downloading single events from the server
This fixes issues like not being able to view pinned messages in
encrypted rooms.
2026-01-13 07:52:07 -05:00
l10n daemon script
ba57570dbf GIT_SILENT Sync po/docbooks with svn 2026-01-13 09:49:08 +00:00
Joshua Goins
1a500a087b Separate priviled members list, and more useful permissions
Having both the member list and permission controls is troublesome, and
scales the larger your moderation team is. We eventually may want to
manage banned/muted users too so I think it warrants having a new page.
I also moved the search field to the top so it's more accessible.

As for permissions, I tried to improve the UX generally while not
changing it too heavily. First the easy change is to the text, hopefully
the sections should be clearer (especially for "state" events.) The
bigger change here is the new sections, I tried to make it more useful
and organized. Additionally, I added more permissions like sharing live
locations and polls so they're more easily configurable.

One other change is that permissions are visible regardless of whether
you can set them or not, matching Element's behavior.
2026-01-11 21:14:15 -05:00
l10n daemon script
e54955ec0c GIT_SILENT Sync po/docbooks with svn 2026-01-12 01:55:14 +00:00
Joshua Goins
8608b3b62e Remove extra arguments in StateComponent's viewEventMenu call 2026-01-11 18:30:02 -05:00
Joshua Goins
fea0cfbf4e Fix opening message menus for popup windows
We were previously assuming that we always want to parent these menus to
RoomPage, but that only exists on the main window. If you tried to open
the menu for say - the search window - then it would confusingly still
open on the main menu.

Thankfully the way to fix this is simple, by passing a parent QtObject
around.
2026-01-11 18:30:02 -05:00
Joshua Goins
5b6e5a25e5 Allow opening message menus for out-of-room events
These are more common than we thought, good examples are pinned or
searched messages - which are not going to be in the room's history
unless you happen to have them loaded. But currently our message menu
infrastructure expects them to be, since its looked up by the room +
event ID.

To fix this is simple, we now move the job of finding the event to the
caller which may use a model instead. I didn't fix all existing
call-sites yet, mainly the message menu opening one since that was the
most obvious bug. But this opens up the door for other assumptions about
room history to be fixed too.

I had to do a bit of C++ re-jiggering in order to expose useful
functions to QML.
2026-01-11 18:30:02 -05:00
Joshua Goins
58b47b8711 Rename indexforEventId to indexForEventId
This is a simple change to ensure its properly camelCase.
2026-01-11 18:30:02 -05:00
Joshua Goins
b45967508c Fix reply colors being broken if you're faster than the server
This is that bug that causes reply colors to be white, and this error to
print in the log:

qrc:/qt/qml/org/kde/neochat/messagecontent/ReplyComponent.qml:41: TypeError: Cannot read property 'color' of null

The reason why this happens is inside of EventMessageContentModel, it
needs to be able to find the relevant event in the room to fetch the
room member (and then their color.) Dependent on many variables to
align, this can happen easily if you are faster than your server giving
you said events.

But this is an easy fix, we obviously get the event afterwards and just
need to re-evaluate the the author property. I also made sure it falls
back to some color instead of white, which will also quiet the error.
2026-01-11 16:27:44 -05:00
Lorenz Wildberg
2ec1fa92fa fix bug: room settings don't open 2026-01-11 16:08:57 +01:00
l10n daemon script
7602a56594 GIT_SILENT Sync po/docbooks with svn 2026-01-11 01:44:50 +00:00
Joshua Goins
e8da02be7d Add dialog to see who has read this message
You were previously relegated to looking at any avatars or a buggy
tooltip, but suffer no longer! If you tap on a message's read marker, a
dialog will pop up listing the users who have read it.

Uou can also view their profiles from here, etc.
2026-01-10 13:33:23 -05:00
Veres Károly
71c84be4b4 Null check decoded messages in loadPinnedMessage
`decryptMessage` returns null if it fails to decode the passed message. This value
then got fed into `EventHandler::richBody` which logged a warning and cleared `m_pinnedMessage`.
If we instead retain the value as an `EncryptedEvent`, the UI will pin the encrypted event
placeholder instead of hiding the existence of pins.
2026-01-10 14:48:09 +00:00
Veres Károly
b684fb451d Null check pinned messages after decryption when filling PinnedMessageModel.
`room()->decryptMessage()` returns null if the message fails to decode. Since elements of `m_pinnedEvents`
get directly dereferenced in getEventForIndex, storing null values leads to a segfault.
In this case we should retain the EncryptedEvent to let the UI report the error.
2026-01-10 14:48:09 +00:00
Joshua Goins
3a416990ca Make clicking room list section headers more reliable
ListSectionHeader itself is an ItemDelegate, which eats up input events.
We can work around this by also listening to onClicked there too.
2026-01-09 23:01:11 -05:00
Joshua Goins
9b7d16973d Improve the Advanced Room settings page more
I added a separator, made it so the "Copy Room ID to clipboard" button
is always shown, and made it more obvious that "Upgrade Room" prompts
you first.
2026-01-09 23:01:02 -05:00
Joshua Goins
dd59e63574 Refer to user's power level as "Power Level", not "Role"
This is a more accurate name for this.
2026-01-09 23:00:52 -05:00
Joshua Goins
7f7e48dfd4 Fix undefined references in UserDetailDialog
This can happen when viewing non-room profiles.
2026-01-09 23:00:52 -05:00
Joshua Goins
7119132e4b Add a way to add private notes for users on their profile
This is a common feature in other chat applications (like Discord) as a
way to keep track of important information. While there isn't a standard
API for this, we can use account data to store notes.
2026-01-09 21:06:10 -05:00
l10n daemon script
6001cc6d4f GIT_SILENT Sync po/docbooks with svn 2026-01-10 01:53:22 +00:00
l10n daemon script
989f1ad020 GIT_SILENT Sync po/docbooks with svn 2026-01-09 01:43:14 +00:00
l10n daemon script
a119563ba7 GIT_SILENT Sync po/docbooks with svn 2026-01-08 01:44:03 +00:00
Joshua Goins
5e473aae0a Avoid assert check when switching to main profile
This can happen if the user doesn't have an avatar set, as there's an
assert check inside of makeMediaUrl. We can avoid this by checking if
the string is empty before calling that.
2026-01-07 17:05:13 -05:00
Joshua Goins
aa0272a02d Remove some duplicate buttons from the room sidebar
The favorites action doesn't really deserve space here IMO, and that's a
pretty list-specific action you're more likely to do elsewhere.

For the Pinned Messages action that can be opened at the top on desktop,
but we still need it on mobile. So now it's selectively hidden based on
that.
2026-01-07 17:04:23 -05:00
Joshua Goins
fc6f345036 Small notification improvements
Changed a check to use isDirectChat (which is a clearer indication of
what we want.) I also made sure not to show the account name if you only
have one, since that's just useless noise.
2026-01-07 15:42:19 -05:00
l10n daemon script
4fa011c266 GIT_SILENT Sync po/docbooks with svn 2026-01-07 01:44:45 +00:00
l10n daemon script
44ebcc6b9a GIT_SILENT Sync po/docbooks with svn 2026-01-06 01:51:54 +00:00
Joshua Goins
32cc5d4e48 Allow switching between viewing main profiles and room profiles
Since Matrix allows users to have different profiles on a room and
"outside of room" level, it's nice to have the ability to switch between
them on-the-fly.
2026-01-05 09:52:31 -05:00
l10n daemon script
bbc5a92f62 GIT_SILENT Sync po/docbooks with svn 2026-01-05 01:51:29 +00:00
Joshua Goins
4656bf4ee7 Rename "Explore Rooms" dialog to just "Explore"
There's a bit of a discrepancy here, that directory will list both rooms
and spaces. I also removed duplicated titles where applicable.
2026-01-04 17:41:55 -05:00
Azhar Momin
5f7967363f Fix notification count refresh for low-priority and mentions-only rooms 2026-01-04 17:00:52 -05:00
Joshua Goins
a02a04d966 Fix icons on Windows
KirigamiApp currently calls KIconTheme::initTheme too late for Windows,
as a workaround we can go back to calling it ourselves.
2026-01-04 13:48:30 +00:00
l10n daemon script
dffec2f0d5 GIT_SILENT Sync po/docbooks with svn 2026-01-04 01:51:40 +00:00
Veres Károly
68b00b9fc5 Extract the space selection logic from setCurrentRoom and use it for setting lastRoomConfig too.
If a setCurrentRoom call changed the active space at the end of its execution, the new room's ID ended up still being written to the old space's lastRoomConfig.

By extracting this space selection logic into a helper function, we can now calculate this value earlier and use it as the space id when writing lastRoomConfig.
2026-01-03 07:42:56 -05:00
Heiko Becker
aa823c7629 GIT_SILENT Update Appstream for new release
(cherry picked from commit 321561fd89)
2026-01-03 12:14:48 +01:00
l10n daemon script
73b82b69d8 GIT_SILENT Sync po/docbooks with svn 2026-01-03 01:41:59 +00:00
Nate Graham
bd0588ca99 Improve hamburger menu button
- Open the menu right beneath the button
- Use pressed state for the button while the menu is open
- Close the menu when clicking the button again
- Hide the tooltip while the menu is open
2026-01-02 08:07:32 -07:00
l10n daemon script
e5b7601dac GIT_SILENT Sync po/docbooks with svn 2026-01-02 01:51:12 +00:00
l10n daemon script
f4ac3346f5 GIT_SILENT Sync po/docbooks with svn 2026-01-01 01:40:42 +00:00
Azhar Momin
58ea229b67 Add a button to cycle through unread highlights
BUG: 465095
2025-12-31 13:57:35 +00:00
l10n daemon script
fd44ff972a GIT_SILENT Sync po/docbooks with svn 2025-12-31 01:51:00 +00:00
l10n daemon script
c5eb63da15 GIT_SILENT Sync po/docbooks with svn 2025-12-30 01:49:46 +00:00
l10n daemon script
79acceb29a SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-12-30 01:34:45 +00:00
l10n daemon script
8022a7aae5 GIT_SILENT Sync po/docbooks with svn 2025-12-29 01:42:10 +00:00
l10n daemon script
d26862dfb6 GIT_SILENT Sync po/docbooks with svn 2025-12-28 01:39:52 +00:00
l10n daemon script
c25c095c1b GIT_SILENT Sync po/docbooks with svn 2025-12-27 01:40:07 +00:00
Tobias Fella
e09e4fb7dc Modernize PropertyChanges 2025-12-26 18:58:28 +00:00
Tobias Fella
657c8a0dcd Fixes for v12 power levels 2025-12-26 18:30:46 +00:00
l10n daemon script
82f54b4f2c GIT_SILENT Sync po/docbooks with svn 2025-12-26 01:41:25 +00:00
Joshua Goins
9e7cd0eb09 Overhaul user profile UI
Our previous iteration is hitting some limitations as the power of
profiles on Matrix grows. For example, where do we put common rooms or
extra profile fields?

I re-arranged everything to group similar actions together - instead of
throwing it all into one big list. We basically trade vertical for
horizontal space, and gives us more headroom for extra fields when we
want to add more.
2025-12-25 12:34:21 -05:00
Joshua Goins
9bdb9b6a7c Improve wording around the various link preview options
In my opinion, the significance of these options was still not super
clear. It wasn't obvious that the setting under Appearance will disable
them globally, and that the per-room option was discoverable.

Another change was using the term "Link preview" instead of "URL
preview", which is more common verbiage in chat applications other than
Element.
2025-12-25 12:33:00 -05:00
Joshua Goins
fa4a93140d Fix not being able to view the DM's profile
This button was broken for a while, so now that's fixed.
2025-12-24 18:46:06 -05:00
Joshua Goins
a41966ecd7 Adjust text for the ignore user option in invitiations
We use the "ignore" terminology elsewhere, so saying "block" here is
kinda weird. I also made it clearer that this also rejects the invite,
in case that wasn't obvious.
2025-12-24 18:43:12 -05:00
Joshua Goins
edcf81f0da Add a few ways to open profile for inviting users
I recieved a few invites from unknown users, and I had no idea who they
were/came from. I could check their profile to see if we had any rooms
in common, but there was no way to actually check their profile!

This makes it possible to open their profile by clicking their avatar
(like elsewhere) but also adds a button to make it even more obvious.
2025-12-24 18:42:26 -05:00
l10n daemon script
0a98c732e0 GIT_SILENT Sync po/docbooks with svn 2025-12-24 01:50:27 +00:00
l10n daemon script
d734e114a6 GIT_SILENT Sync po/docbooks with svn 2025-12-23 01:41:20 +00:00
Joshua Goins
946e505a24 Add missing supportsMatrixSpecVersion QML function
This was rebased out or something in 197d7ce8e8
2025-12-22 16:31:42 -05:00
Joshua Goins
092980c0ff Shorten room subtitles for direct chats
Direct chats by their very nature is between two users, and you usually
can keep track of who said what. There's no point in including the
sender's name here, and most other chat applications exclude it for this
reason (including Element X.)
2025-12-22 15:02:24 -05:00
Joshua Goins
6fcb1cc1e3 Fix assumption about unstable feature reporting
The key can be in the unstable features list, but it can be false. This
stops some features showing up and hitting API that isn't actually
implemented.
2025-12-22 07:26:27 -05:00
Olivier De Cannière
c572480d10 cmake: Explicitly link in neochatplugin and use qt_* cmake commands
The QML code of the various QML modules is compiled to C++ by
qmlcachegen and then compiled into the final binary. Unfortunately, in
the current setup, the plugin for the org.kde.neochat QML module is not
linked into the app because qmlimportscanner can't find usages of it. No
generated C++ code for that module is then run and the QML is
interpreted from its bytecode instead.

This is likely due to the project not following recommended QML
structure and because it is not using qt_* variants of certain cmake
commands. It should not be necessary to link against the plugins
manually.

As a quick fix, link against the plugin explicitly and use qt_*
variants of cmake commands to help qmlimportscanner and to pick up the
existing C++ code.
2025-12-22 11:11:35 +01:00
Joshua Goins
531df7a3b2 Send beautiful red ❤️'s when quick reacting
I wondered for a while (and could tell) when people were using NeoChat
because they would react with cold, monochrome hearts. Let's add more
color to our world!
2025-12-21 13:57:59 -05:00
Veres Károly
706f1f7836 Fix missing escape sequence in /shrug command
Before the fix, the upper arm _ characters in the command's output would be parsed as Markdown italic formatting around the (ツ).
2025-12-21 15:03:00 +01:00
l10n daemon script
45c5806c5a GIT_SILENT Sync po/docbooks with svn 2025-12-19 01:42:17 +00:00
l10n daemon script
abf37c90cb GIT_SILENT Sync po/docbooks with svn 2025-12-18 01:41:35 +00:00
Jimmy Bergström
5d9508b165 Hide quick format bar when selection is cleared
I noticed a bit of an annoying behavior with the quick format bar. Whenever I make a typo when writing a message I usually go back with a ctrl+shift+arrow selection to correct it, and that of course triggers the bar, but then there wasn't any obvious way to make it go away (other than pressing backspace or delete, [ChatBar.qml#L339](https://invent.kde.org/network/neochat/-/blob/master/src/chatbar/ChatBar.qml?ref_type=heads#L339)), so it'd always be stuck visible until I clicked somewhere with my mouse, which I found somewhat annoying.

This intends to fix that, by hiding it whenever the selection is cleared, which seems like a reasonable and expected behavior to me. I would also be okay with making it dismissable with escape, if this current suggestion has some unintended side-effect that I've missed.

I also think that this should in practice "solve" this bug https://bugs.kde.org/show_bug.cgi?id=511590 but I didn't link it directly as their suggested solution is smarter placement of the bar.
2025-12-17 21:33:08 +00:00
l10n daemon script
f5a652f7a1 GIT_SILENT Sync po/docbooks with svn 2025-12-16 14:25:07 +00:00
Paul Brown
f165cb1f10 Removed list of supporters, as we now have a less annoying way of showing them on the apps website. 2025-12-14 16:41:47 +00:00
l10n daemon script
11132c3e89 GIT_SILENT Sync po/docbooks with svn 2025-12-14 01:40:32 +00:00
l10n daemon script
089ab2009a GIT_SILENT Sync po/docbooks with svn 2025-12-13 01:44:24 +00:00
l10n daemon script
6ebedc5dfc GIT_SILENT Sync po/docbooks with svn 2025-12-12 01:42:29 +00:00
Nicolas Fella
e5b2fb7316 Guard against currentIndex being -1 in NeochatMaximizeComponent
Sometimes the ListView's currentIndex is -1, which results in us
querying data() for an invalid index, causing an assert in Qt model
code

BUG: 513104
2025-12-11 18:01:19 +01:00
l10n daemon script
5da3aff607 GIT_SILENT Sync po/docbooks with svn 2025-12-10 01:47:45 +00:00
l10n daemon script
3fa46d52fe GIT_SILENT Sync po/docbooks with svn 2025-12-09 01:41:23 +00:00
Justin Zobel
ece9811d82 CI - Flatpak - Remove Kirigami Addons as it is now in the runtime 2025-12-08 19:55:46 +10:30
Justin Zobel
b543b1c2f9 CI - Flatpak - Use -DCMAKE_POLICY_VERSION_MINIMUM=3.5 for olm 2025-12-08 19:55:46 +10:30
Justin Zobel
5065bccb64 CI - Flatpak - Fix broken sha256sum 2025-12-08 19:55:46 +10:30
Justin Zobel
10c3125668 CI - Flatpak - Fix broken sha256sums 2025-12-08 19:55:46 +10:30
Justin Zobel
73019cfea2 CI - Flatpak - Update Runtime to 6.10 2025-12-08 19:55:46 +10:30
Justin Zobel
4bc6a88acf CI - Flatpak - Update Versions 2025-12-08 19:55:46 +10:30
l10n daemon script
56a49cfc50 GIT_SILENT Sync po/docbooks with svn 2025-12-08 01:41:51 +00:00
Nate Graham
c50380b448 Tell room page header message to fill the width
Otherwise it scrunches up as small as possible and breaks the layout.
2025-12-07 09:27:58 -05:00
l10n daemon script
673a8c8bc8 GIT_SILENT Sync po/docbooks with svn 2025-12-07 01:45:09 +00:00
l10n daemon script
c298c348a4 GIT_SILENT Sync po/docbooks with svn 2025-12-04 01:45:11 +00:00
Heiko Becker
2fcd535aeb GIT_SILENT Update Appstream for new release
(cherry picked from commit 0e4b52ee62)
2025-12-04 00:20:05 +01:00
l10n daemon script
8f5e68de5d GIT_SILENT Sync po/docbooks with svn 2025-12-03 01:45:04 +00:00
l10n daemon script
5fe36a9057 GIT_SILENT Sync po/docbooks with svn 2025-12-02 01:41:01 +00:00
l10n daemon script
8d8c6444e0 GIT_SILENT Sync po/docbooks with svn 2025-12-01 01:44:51 +00:00
Joshua Goins
03d5955c8d Add support for reporting rooms and users
This was added in recent Matrix versions, and a desperately needed
feature.
2025-11-30 14:01:13 -05:00
Joshua Goins
197d7ce8e8 Add a new function to check supported Matrix spec in QML 2025-11-30 14:01:13 -05:00
l10n daemon script
efbf6d96d4 GIT_SILENT Sync po/docbooks with svn 2025-11-30 01:45:44 +00:00
l10n daemon script
b8cd3c69c2 GIT_SILENT Sync po/docbooks with svn 2025-11-29 01:42:47 +00:00
renner 03
1da24191f0 Fix krunner integration with Flatpak 2025-11-28 09:38:51 -05:00
l10n daemon script
2ecc567792 GIT_SILENT Sync po/docbooks with svn 2025-11-28 01:41:47 +00:00
l10n daemon script
66e1fe067d GIT_SILENT Sync po/docbooks with svn 2025-11-27 01:43:17 +00:00
Nate Graham
d08f014def Rephrase a British English wording for the base string
"X has put Y out of the room" is a British English style sentence. Which
is fine, but I'm using en_US, not en_UK.

Re-phrase this to sound a bit more American (which is the linguistic
style we use for the base strings), and then count on the en_UK
translators bringing back the old phrasing for en_UK.
2025-11-25 22:15:45 -07:00
l10n daemon script
63941d6685 GIT_SILENT Sync po/docbooks with svn 2025-11-26 01:41:17 +00:00
Tobias Fella
38523c97c5 Implement drag&drop support for flatpaks
BUG: 495552
2025-11-25 10:27:38 +00:00
l10n daemon script
65c6f4c1d3 GIT_SILENT Sync po/docbooks with svn 2025-11-25 01:41:04 +00:00
Heiko Becker
0cb3fd32f4 Drop unused dependencies
Both KF6Crash and KF6IconThemes aren't used anymore after porting to
KirigamiApp in eab45e761a.
2025-11-24 21:51:55 +01:00
l10n daemon script
b02cb0157e GIT_SILENT Sync po/docbooks with svn 2025-11-24 01:42:36 +00:00
l10n daemon script
ca163fc169 GIT_SILENT Sync po/docbooks with svn 2025-11-23 01:43:18 +00:00
l10n daemon script
5687eb33e1 GIT_SILENT Sync po/docbooks with svn 2025-11-21 01:43:31 +00:00
l10n daemon script
fc795e50b3 GIT_SILENT Sync po/docbooks with svn 2025-11-20 01:41:22 +00:00
l10n daemon script
c4adfe7939 GIT_SILENT Sync po/docbooks with svn 2025-11-19 01:39:53 +00:00
l10n daemon script
ce96232bbd GIT_SILENT Sync po/docbooks with svn 2025-11-18 02:00:53 +00:00
l10n daemon script
95653cf3f7 GIT_SILENT Sync po/docbooks with svn 2025-11-17 13:25:28 +00:00
l10n daemon script
0c11cd331f GIT_SILENT Sync po/docbooks with svn 2025-11-17 01:40:07 +00:00
Tobias Fella
3c8ca0d421 Simplify key backup unlocking
Replaces the separate text fields for security keys and backup passphrases with a single on; internally, both methods are then tried.
2025-11-16 14:21:26 +00:00
l10n daemon script
52f71d5c55 GIT_SILENT Sync po/docbooks with svn 2025-11-16 01:40:16 +00:00
l10n daemon script
00f7dd5175 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2025-11-16 01:34:16 +00:00
l10n daemon script
52da8415e9 GIT_SILENT Sync po/docbooks with svn 2025-11-15 01:41:08 +00:00
l10n daemon script
953e4cd792 GIT_SILENT Sync po/docbooks with svn 2025-11-14 01:41:28 +00:00
Tobias Fella
ee4b1578b1 Consider highlights when determining whether a room can be marked as read
BUG: 501080
2025-11-13 20:11:06 +00:00
Paul Brown
ae24424a32 Adding anthropy@mastodon.derg.nz to list of supporters 2025-11-13 19:32:43 +00:00
l10n daemon script
51be282824 GIT_SILENT Sync po/docbooks with svn 2025-11-13 01:42:02 +00:00
Tobias Fella
c539dfc352 Fix crash when poll answer has fewer selections than possible
BUG: 511909
2025-11-12 12:29:53 +00:00
Carl Schwan
fe734206df Add Thibault Molleman to the donnors 2025-11-12 11:14:38 +01:00
l10n daemon script
79a49165f1 GIT_SILENT Sync po/docbooks with svn 2025-11-11 01:51:20 +00:00
Tobias Fella
ada5eef088 Remove outdated flatpak font config 2025-11-10 17:27:25 +00:00
Tobias Fella
9cc5778126 Remove secret backup feature flag
There isn't really a point in hiding this behind a flag and it just makes the process even more confusing
2025-11-10 18:04:50 +01:00
l10n daemon script
b129805f94 GIT_SILENT Sync po/docbooks with svn 2025-11-10 01:44:58 +00:00
Joshua Goins
887865c0aa Improve invited room counting
I didn't realize when redoing the tooltip for DMs that directChatInvites
was a boolean, not an integer type. Now it's changed to an integer type,
which fixes the DM invite count.

NeoChat apparently didn't count normal room invites until now either, so
now Home is highlighted in that case. Now it should be harder to miss
these kinds of invites.
2025-11-09 12:03:17 -05:00
Joshua Goins
1336194cf4 Don't send an erroneous SetTypingJob on start-up
You can see this while starting NeoChat, as it always fails because
localMember() isn't valid at this point. This is called during the
ChatBar setup as we're setting up the text, which also happens during
room switch.
2025-11-09 10:55:05 -05:00
Joshua Goins
5a7ae3563e Fix alignment on the room invitation page
I forgot to horizontally center the room alias label, so it was usually
stuck to the left-side. I also don't know why I put the room alias on
top and the room name on the bottom, so those are swapped now too.
2025-11-09 10:54:54 -05:00
Joshua Goins
7c56e24fdc Manually update badge count in initActiveConnection
Without doing this, it's possible to start NeoChat and have no badge
count despite having notable notifications. The reason for this is if
badgeNotificationCount was updated and changed before this function
call, and a call to refreshBadgeNotificationCount doesn't emit the
changed signal.

This is easy to work around by calling updateBadgeNotificationCount
ourselves.
2025-11-09 10:19:55 -05:00
Joshua Goins
e6b9abeca3 Account for pending invites in the badge notification count 2025-11-09 10:18:28 -05:00
l10n daemon script
1b45784a88 GIT_SILENT Sync po/docbooks with svn 2025-11-09 01:48:27 +00:00
Albert Astals Cid
a12b0d5282 GIT_SILENT Upgrade release service version to 26.03.70. 2025-11-06 18:29:06 +01:00
Paul Brown
a6d75f2ff5 Adding Joshua Strobl to list of supporters 2025-11-06 17:01:19 +00:00
l10n daemon script
b8f7f33b9a GIT_SILENT Sync po/docbooks with svn 2025-11-06 16:14:20 +00:00
157 changed files with 55096 additions and 28498 deletions

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.9",
"runtime-version": "6.10",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -31,19 +31,6 @@
"/share/ndk-modules"
],
"modules": [
{
"name": "kirigamiaddons",
"config-opts": [
"-DBUILD_TESTING=OFF"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/libraries/kirigami-addons.git"
}
]
},
{
"name": "opencv",
"config-opts": [
@@ -78,6 +65,7 @@
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DOLM_TESTS=OFF"
],
"sources": [
@@ -184,8 +172,8 @@
"sources": [
{
"type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz",
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518",
"url": "https://download.kde.org/stable/release-service/25.08.3/src/kunifiedpush-25.08.3.tar.xz",
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a",
"x-checker-data": {
"type": "anitya",
"project-id": 8763,

View File

@@ -10,11 +10,11 @@ Dependencies:
'frameworks/ki18n': '@latest-kf6'
'frameworks/kconfig': '@latest-kf6'
'frameworks/syntax-highlighting': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'frameworks/kitemmodels': '@latest-kf6'
'frameworks/kquickcharts': '@latest-kf6'
'frameworks/knotifications': '@latest-kf6'
'frameworks/kcolorscheme': '@latest-kf6'
'frameworks/kiconthemes': '@latest-kf6'
'libraries/kquickimageeditor': '@latest-kf6'
'frameworks/sonnet': '@latest-kf6'
'frameworks/prison': '@latest-kf6'
@@ -29,7 +29,6 @@ Dependencies:
'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
'frameworks/kcrash': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@latest-kf6'

View File

@@ -7,8 +7,8 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MAJOR "26")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
@@ -46,10 +46,6 @@ if (NOT ANDROID)
include(KDEClangFormat)
endif()
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
@@ -69,7 +65,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif ()
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels IconThemes ColorScheme)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme IconThemes)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -92,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 Crash)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME

View File

@@ -63,7 +63,7 @@ void ActionsTest::testActions_data()
QTest::addColumn<std::optional<QString>>("resultText");
QTest::addColumn<std::optional<Quotient::RoomMessageEvent::MsgType>>("type");
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\_(ツ)_/¯ Hello"_s)
QTest::newRow("shrug") << u"/shrug Hello"_s << std::make_optional(u"¯\\\\\\_(ツ)\\_/¯ Hello"_s)
<< std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("lenny") << u"/lenny Hello"_s << std::make_optional(u"( ͡° ͜ʖ ͡°) Hello"_s) << std::make_optional(Quotient::RoomMessageEvent::MsgType::Text);
QTest::newRow("tableflip") << u"/tableflip Hello"_s << std::make_optional(u"(╯°□°)╯︵ ┻━┻ Hello"_s)

View File

@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0);
}
void TimelineMessageModelTest::cleanup()

View File

@@ -1,14 +0,0 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the
# KDE Flatpak SDK.
install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
)

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -320,7 +320,6 @@
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
<value key="KDE::supporters">Anonymous donor, Akseli</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>
@@ -488,6 +487,8 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="25.12.1" date="2026-01-08"/>
<release version="25.12.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/>
<release version="25.08.2" date="2025-10-09"/>
<release version="25.08.1" date="2025-09-11"/>

View File

@@ -108,6 +108,7 @@ Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[ko]=Matrix에서 대화하기
Comment[lt]=Pokalbiai per Matrix
Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie

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

7216
po/ga/neochat.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" ?>
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
<!ENTITY % Brazilian-Portuguese "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Manual do Usuário do NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>NeoChat man page.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>01/11/2022</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Cliente para interação com o protocolo de mensagens Matrix.</refpurpose>
</refnamediv>
<!-- body begins here -->
<refsynopsisdiv id='synopsis'>
<cmdsynopsis
><command
>neochat</command
> <arg choice="opt"
><replaceable
>URI</replaceable
></arg
> </cmdsynopsis>
</refsynopsisdiv>
<refsect1 id="description">
<title
>Descrição</title>
<para
>O <command
>neochat</command
> é um aplicativo de bate-papo para o protocolo Matrix. Ele funciona tanto em computadores quanto em dispositivos móveis. </para>
</refsect1>
<refsect1 id="options"
><title
>Opções</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>O URI da matriz para um usuário ou uma sala. Por exemplo, matrix:u/usuário:exemplo.org e matrix:r/root:exemplo.org. Isso fará com que o NeoChat tente abrir a sala ou conversa especificada. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Relatar bugs</title>
<para
>Você pode reportar erros e solicitar novas funcionalidades em <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General"
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&amp;component=General</ulink
></para>
</refsect1>
<refsect1>
<title
>Veja também</title>
<simplelist>
<member
>Lista de perguntas frequentes sobre o Matrix <ulink url="https://matrix.org/faq/"
>https://matrix.org/faq/</ulink
> </member>
<member
>kf5options(7)</member>
<member
>qt5options(7)</member>
</simplelist>
</refsect1>
<refsect1 id="copyright"
><title
>Direitos autorais</title>
<para
>Direitos autorais &copy; 2020-2022 Tobias Fella </para>
<para
>Direitos autorais &copy; 2020-2022 Carl Schwan </para>
<para
>Licença: GNU General Public Versão 3 ou posterior <ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
>&gt;</para>
</refsect1>
</refentry>

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

@@ -22,7 +22,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>carl@carlschwan.eu</email
></author>
<date
>2022-11-01</date>
>20221101</date>
<releaseinfo
>22.09</releaseinfo>
<productname
@@ -111,9 +111,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
><title
>Telif Hakkı</title>
<para
>Telif hakkı &copy; 2020-2022 Tobias Fella </para>
>Telif hakkı &copy; 20202022 Tobias Fella </para>
<para
>Telif hakkı &copy; 2020-2022 Carl Schwan </para>
>Telif hakkı &copy; 20202022 Carl Schwan </para>
<para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink

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

@@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
add_library(neochat STATIC
qt_add_library(neochat STATIC
controller.cpp
controller.h
roommanager.cpp
@@ -35,6 +35,10 @@ add_library(neochat STATIC
models/commonroomsmodel.h
texttospeechhelper.h
texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -104,6 +108,8 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/NewPollDialog.qml
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES
QtCore
QtQuick
@@ -140,10 +146,17 @@ if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
add_executable(neochat-app
qt_add_executable(neochat-app
main.cpp
)
if(ANDROID)
set_target_properties(neochat-app PROPERTIES
OUTPUT_NAME "neochat-app"
PREFIX "lib"
)
endif()
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
@@ -153,6 +166,7 @@ target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
neochat
KF6::IconThemes
)
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -183,7 +197,7 @@ else()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PRIVATE neochatplugin Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PUBLIC
LibNeoChat
Timeline
@@ -203,10 +217,10 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::IconThemes
KF6::ItemModels
KF6::I18nQml
KirigamiApp
KirigamiAddonsComponents
QuotientQt6
Login
Rooms
@@ -214,10 +228,6 @@ target_link_libraries(neochat PUBLIC
Spaces
)
if (TARGET KF6::Crash)
target_link_libraries(neochat PUBLIC KF6::Crash)
endif()
kconfig_target_kcfg_file(neochat FILE neochatconfig.kcfg CLASS_NAME NeoChatConfig MUTATORS GENERATE_PROPERTIES DEFAULT_VALUE_GETTERS PARENT_IN_CONSTRUCTOR SINGLETON GENERATE_MOC QML_REGISTRATION)
if(NEOCHAT_FLATPAK)
@@ -357,7 +367,8 @@ endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
# krunner plugin must be the same as the app id for flatpak to export it
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins RENAME org.kde.neochat.desktop)
endif()
if (APPLE)

View File

@@ -246,7 +246,10 @@ void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatC
if (newConnection) {
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
// Refresh and update manually, in case we init too late for the badge count to actually change.
newConnection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(newConnection->badgeNotificationCount());
}
Q_EMIT activeConnectionChanged(newConnection);
}

View File

@@ -103,6 +103,10 @@ int main(int argc, char *argv[])
{
QNetworkProxyFactory::setUseSystemConfiguration(true);
// We currently need to do this ourselves,
// KirigamiApp currently called this after constructing the app which breaks icons on Windows.
KIconTheme::initTheme();
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
@@ -162,12 +166,6 @@ int main(int argc, char *argv[])
Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy(u"/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"_s, u"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"_s);
#endif
ColorSchemer colorScheme;
QCommandLineParser parser;

View File

@@ -5,6 +5,7 @@
#include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication>
#include <Quotient/room.h>
using namespace Quotient;
@@ -39,8 +40,22 @@ void CommonRoomsModel::setUserId(const QString &userId)
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{
Q_UNUSED(index)
Q_UNUSED(roleName)
auto roomId = m_commonRooms[index.row()];
auto room = connection()->room(roomId);
if (!room) {
return {};
}
switch (roleName) {
case Qt::DisplayRole:
case RoomNameRole:
return room->displayName();
case RoomAvatarRole:
return room->avatarUrl();
case RoomIdRole:
return roomId;
}
return {};
}
@@ -50,6 +65,15 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const
return m_commonRooms.size();
}
QHash<int, QByteArray> CommonRoomsModel::roleNames() const
{
return {
{RoomIdRole, "roomId"},
{RoomNameRole, "roomName"},
{RoomAvatarRole, "roomAvatar"},
};
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {

View File

@@ -24,7 +24,9 @@ class CommonRoomsModel : public QAbstractListModel
public:
enum Roles {
RoomIdRole = Qt::DisplayRole,
RoomIdRole = Qt::UserRole,
RoomNameRole,
RoomAvatarRole,
};
Q_ENUM(Roles)
@@ -39,6 +41,8 @@ public:
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "models/limitermodel.h"
LimiterModel::LimiterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged);
}
int LimiterModel::maximumCount() const
{
return m_maximumCount;
}
void LimiterModel::setMaximumCount(int maximumCount)
{
if (m_maximumCount != maximumCount) {
m_maximumCount = maximumCount;
Q_EMIT maximumCountChanged();
}
}
int LimiterModel::extraCount() const
{
if (sourceModel()) {
return std::max(sourceModel()->rowCount() - maximumCount(), 0);
}
return 0;
}
bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent)
return source_row < maximumCount();
}
#include "moc_limitermodel.cpp"

View File

@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class LimiterModel
*
* @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount.
*
* Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch.
*/
class LimiterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged)
Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged)
public:
explicit LimiterModel(QObject *parent = nullptr);
[[nodiscard]] int maximumCount() const;
void setMaximumCount(int maximumCount);
[[nodiscard]] int extraCount() const;
Q_SIGNALS:
void maximumCountChanged();
void extraCountChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
int m_maximumCount = 0;
};

View File

@@ -259,7 +259,7 @@ Comment[sa]=कक्षस्य नूतनं निमन्त्रणम
Comment[sl]=Tam je novo povabilo v sobo
Comment[sv]=Det finns en ny inbjudan till ett rum
Comment[ta]=ஓர் அரங்கிற்கான புதிய அழைப்பிதழ் உள்ளது
Comment[tr]=Bir odaya yeni bir davetiye var
Comment[tr]=Bir odaya yeni bir davet var
Comment[uk]=У кімнаті нове запрошення
Comment[zh_CN]=有新的聊天室邀请
Comment[zh_TW]=有新的加入聊天室邀請
@@ -287,6 +287,7 @@ Name[ia]=Comparti
Name[it]=Condivisione
Name[ka]=გაზიარება
Name[ko]=공유
Name[lt]=Bendrinti
Name[lv]=Kopīgot
Name[nl]=Gedeelde
Name[nn]=Del
@@ -322,6 +323,7 @@ Comment[ia]=Le exito de compartir un pecietta de contento
Comment[it]=Il risultato della condivisione di un contenuto
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
Comment[ko]=콘텐츠 공유 결과
Comment[lt]=Turinio dalies bendrinimo rezultatas
Comment[lv]=Satura kopīgošanas rezultāts
Comment[nl]=Het resultaat van het delen van een stukje inhoud
Comment[nn]=Resultatet av deling av innhald

View File

@@ -211,10 +211,6 @@
<label>Enable threads</label>
<default>false</default>
</entry>
<entry name="SecretBackup" type="bool">
<label>Enable secret backup</label>
<default>false</default>
</entry>
<entry name="Phone3PId" type="bool">
<label>Enable add phone numbers as 3PIDs</label>
<default>false</default>

View File

@@ -11,6 +11,7 @@
#include <KNotification>
#include <KNotificationPermission>
#include <KNotificationReplyAction>
#include <KirigamiAddons/Components/NameUtils>
#include <QPainter>
#include <Quotient/accountregistry.h>
@@ -144,16 +145,9 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
}
QImage avatar_image;
if (!sender.avatarUrl().isEmpty()) {
avatar_image = room->member(sender.id()).avatar(128, 128, {});
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
sender.displayName(),
room->member(sender.id()),
body,
avatar_image,
notification["event"_L1].toObject()["event_id"_L1].toString(),
true,
pair.first);
@@ -195,9 +189,8 @@ bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> co
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &sender,
const RoomMember &member,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp)
@@ -216,17 +209,17 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
}
});
notification->setTitle(room->displayName());
QString entry;
if (sender == room->displayName()) {
notification->setTitle(sender);
if (room->isDirectChat()) {
entry = text.toHtmlEscaped();
} else {
notification->setTitle(room->displayName());
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
}
notification->setText(entry);
notification->setPixmap(createNotificationImage(icon, room));
notification->setPixmap(createNotificationImage(member, room));
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
@@ -253,7 +246,9 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
notification->setReplyAction(std::move(replyAction));
}
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
if (Controller::instance().accounts()->rowCount() > 1) {
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent();
}
@@ -292,18 +287,10 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
}
const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image;
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else {
qWarning() << "using this room's avatar";
avatar_image = room->avatar(128);
}
KNotification *notification = new KNotification(u"invite"_s);
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
notification->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
notification->setPixmap(createNotificationImage(sender, room));
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) {
@@ -347,7 +334,9 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
m_invitations.remove(room->id());
});
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
if (Controller::instance().accounts()->rowCount() > 1) {
notification->setHint(u"x-kde-origin-name"_s, room->localMember().id());
}
notification->sendEvent();
}
@@ -407,41 +396,125 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
}
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room)
{
QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {});
bool senderIconIsPlaceholder = false;
if (senderIcon.isNull()) {
senderIcon = createPlaceholderImage(member.displayName());
senderIconIsPlaceholder = true;
}
QImage icon;
if (room->isDirectChat()) {
icon = senderIcon;
} else {
QImage roomIcon = room->avatar(avatarDimension, avatarDimension);
bool roomIconIsPlaceholder = false;
if (roomIcon.isNull()) {
roomIcon = createPlaceholderImage(room->displayName());
roomIconIsPlaceholder = true;
}
icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder);
}
return QPixmap::fromImage(icon);
}
QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon,
const bool senderIconIsPlaceholder,
const QImage &roomIcon,
const bool roomIconIsPlaceholder)
{
// Handle avatars that are lopsided in one dimension
const int biggestDimension = std::max(icon.width(), icon.height());
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
roundedImage.fill(Qt::transparent);
QPainter painter(&roundedImage);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setPen(Qt::NoPen);
// Fill background for transparent avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (senderIconIsPlaceholder) {
painter.drawImage(imageRect, senderIcon);
} else {
// Fill background for transparent non-placeholder avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room != nullptr) {
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
}
return QPixmap::fromImage(roundedImage);
const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0};
if (roomIconIsPlaceholder) {
// Ditto for room icons, but we also want to "carve out" the transparent area for readability
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.setBrush(Qt::transparent);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawImage(lowerQuarter, roomIcon);
} else {
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
return roundedImage;
}
QImage NotificationsManager::createPlaceholderImage(const QString &name)
{
const QColor color = NameUtils().colorsFromString(name);
QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
// Draw background
QColor backgroundColor = color;
backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons.
painter.setBrush(backgroundColor);
painter.setPen(Qt::transparent);
painter.drawRoundedRect(image.rect(), image.width(), image.height());
constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions.
// Draw border
painter.setBrush(Qt::transparent);
painter.setPen(QPen(color, borderWidth));
painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)),
image.width(),
image.height());
const QString initials = NameUtils().initialsFromString(name);
QTextOption option;
option.setAlignment(Qt::AlignCenter);
// Calculation similar to the one found in Kirigami Add-ons.
constexpr int largeSpacing = 8; // Same as what's defined in kirigami.
constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2));
QFont font;
font.setPixelSize((avatarDimension - padding) / 2);
painter.setBrush(color);
painter.setPen(color);
painter.setFont(font);
painter.drawText(image.rect(), initials, option);
return image;
}
#include "moc_notificationsmanager.cpp"

View File

@@ -13,6 +13,11 @@
#include <Quotient/csapi/notifications.h>
#include <Quotient/jobs/basejob.h>
namespace Quotient
{
class RoomMember;
}
class NeoChatConnection;
class KNotification;
class NeoChatRoom;
@@ -67,15 +72,24 @@ private:
QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection);
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
/**
* @return A combined image of the sender and room's avatar.
*/
static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room);
/**
* @return The sender and room icon combined together into one image. Used internally by createNotificationImage.
*/
static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder);
/**
* @return A placeholder avatar image, similar to the one found in Kirigami Add-ons.
*/
static QImage createPlaceholderImage(const QString &name);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
void postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp);
void
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
void doPostInviteNotification(QPointer<NeoChatRoom> room);
@@ -84,6 +98,8 @@ private:
bool permissionAsked = false;
static constexpr int avatarDimension = 128;
private Q_SLOTS:
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
};

View File

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

View File

@@ -45,14 +45,12 @@ Labs.MenuBar {
}
Labs.MenuItem {
icon.name: "compass-symbolic"
text: i18nc("@action:inmenu", "Explore Rooms")
text: i18nc("@action:inmenu Explore public rooms and spaces", "Explore")
enabled: root.connection
onTriggered: {
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
});
}, {});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});

View File

@@ -27,16 +27,17 @@ ColumnLayout {
Layout.fillHeight: true
}
KirigamiComponents.Avatar {
KirigamiComponents.AvatarButton {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
name: root.invitingMember.displayName
source: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl
color: root.invitingMember.color
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
}
Loader {
@@ -54,6 +55,12 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
}
Kirigami.Heading {
text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
}
Kirigami.SelectableLabel {
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
@@ -61,12 +68,7 @@ ColumnLayout {
visible: root.currentRoom && root.currentRoom.canonicalAlias
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
color: Kirigami.Theme.disabledTextColor
}
Kirigami.Heading {
text: root.currentRoom.displayName
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
@@ -137,8 +139,24 @@ ColumnLayout {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
id: viewProfileDelegate
icon.name: "user-properties-symbolic"
text: i18nc("@action:button View this user's profile", "View %1's Profile", root.invitingMember.displayName)
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
}
FormCard.FormDelegateSeparator {
above: viewProfileDelegate
below: ignoreUserDelegate
}
FormCard.FormButtonDelegate {
id: ignoreUserDelegate
icon.name: "list-remove-symbolic"
text: i18nc("@action:button Block the user", "Block %1", root.invitingMember.displayName)
text: i18nc("@action:button Ignore the user", "Ignore %1 and Reject Invite", root.invitingMember.displayName)
onClicked: {
root.currentRoom.forget()

View File

@@ -23,72 +23,63 @@ Kirigami.Page {
name: "cancelled"
when: root.session.state === KeyVerificationSession.CANCELED
PropertyChanges {
target: stateLoader
sourceComponent: verificationCanceled
stateLoader.sourceComponent: verificationCanceled
}
},
State {
name: "waitingForVerification"
when: root.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
PropertyChanges {
target: stateLoader
sourceComponent: emojiSas
stateLoader.sourceComponent: emojiSas
}
},
State {
name: "waitingForReady"
when: root.session.state === KeyVerificationSession.WAITINGFORREADY
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
},
State {
name: "incoming"
when: root.session.state === KeyVerificationSession.INCOMING
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
},
State {
name: "waitingForKey"
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
},
State {
name: "waitingForAccept"
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
},
State {
name: "waitingForMac"
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
},
State {
name: "ready"
when: root.session.state === KeyVerificationSession.READY
PropertyChanges {
target: stateLoader
sourceComponent: chooseVerificationComponent
stateLoader.sourceComponent: chooseVerificationComponent
}
},
State {
name: "done"
when: root.session.state === KeyVerificationSession.DONE
PropertyChanges {
target: stateLoader
sourceComponent: message
stateLoader.sourceComponent: message
}
}
]

View File

@@ -23,13 +23,45 @@ Components.AlbumMaximizeComponent {
*/
required property NeoChatRoom currentRoom
readonly property string currentEventId: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.EventIdRole)
readonly property string currentEventId: {
const idx = (content as ListView).currentIndex;
readonly property var currentAuthor: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.AuthorRole)
if (idx === -1) {
return ""
}
readonly property var currentTime: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.TimeRole)
return model.data(model.index(idx, 0), TimelineMessageModel.EventIdRole)
}
readonly property var currentProgressInfo: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
readonly property var currentAuthor: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
return model.data(model.index(idx, 0), TimelineMessageModel.AuthorRole)
}
readonly property var currentTime: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
model.data(model.index(idx, 0), TimelineMessageModel.TimeRole)
}
readonly property var currentProgressInfo: {
const idx = (content as ListView).currentIndex;
if (idx === -1) {
return {}
}
model.data(model.index(idx, 0), TimelineMessageModel.ProgressInfoRole)
}
actions: [
ShareAction {
@@ -122,7 +154,10 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus()
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
onItemRightClicked: {
const event = root.currentRoom.findEvent(root.currentEventId);
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
}
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;

View File

@@ -30,15 +30,13 @@ Kirigami.SearchDialog {
emptyText: i18nc("Placeholder message", "No room found")
Kirigami.Action {
id: exploreRoomAction
text: i18nc("@action:button", "Explore rooms")
text: i18nc("@action:button Explore public rooms and spaces", "Explore")
icon.name: "compass"
onTriggered: {
root.close()
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
connection: root.connection
}, {
title: i18nc("@title", "Explore Rooms")
});
}, {});
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
});

View File

@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
Kirigami.Page {
id: root
@@ -13,6 +14,8 @@ Kirigami.Page {
required property string placeholder
required property string actionText
required property string icon
required property bool reporting
required property NeoChatConnection connection
signal accepted(reason: string)
@@ -21,6 +24,15 @@ Kirigami.Page {
topPadding: 0
bottomPadding: 0
header: Kirigami.InlineMessage {
showCloseButton: false
visible: root.reporting
type: Kirigami.MessageType.Information
position: Kirigami.InlineMessage.Position.Header
text: xi18n("This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
}
QQC2.TextArea {
id: reason
placeholderText: root.placeholder
@@ -31,7 +43,7 @@ Kirigami.Page {
Keys.onReturnPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
root.accepted(reason.text);
root.closeDialog();
root.Kirigami.PageStack.closeDialog();
}
}
@@ -52,14 +64,14 @@ Kirigami.Page {
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.accepted(reason.text);
root.closeDialog();
root.Kirigami.PageStack.closeDialog();
}
}
QQC2.Button {
icon.name: "dialog-cancel-symbolic"
text: i18nc("@action", "Cancel")
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.RejectRole
onClicked: root.closeDialog()
onClicked: root.Kirigami.PageStack.closeDialog()
}
}
}

View File

@@ -75,6 +75,12 @@ Kirigami.Page {
focus: true
padding: 0
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
actions: [
Kirigami.Action {
id: jitsiMeetingAction
@@ -113,7 +119,7 @@ Kirigami.Page {
}
},
Kirigami.Action {
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow)?.wideMode
icon.name: "view-right-new"
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
}
@@ -227,6 +233,8 @@ Kirigami.Page {
// Used to keep track of messages so we can hide the right one at the right time
property string messageId
Layout.fillWidth: true
showCloseButton: true
visible: false
position: Kirigami.InlineMessage.Position.Header
@@ -253,7 +261,6 @@ Kirigami.Page {
id: timelineView
messageFilterModel: root.messageFilterModel
compactLayout: NeoChatConfig.compactLayout
fileDropEnabled: !Controller.isFlatpak
markReadCondition: NeoChatConfig.markReadCondition
}
}
@@ -288,7 +295,7 @@ Kirigami.Page {
footer: Loader {
id: chatBarLoader
height: active ? (item as ChatBar).implicitHeight : 0
height: active ? (item as ChatBar)?.implicitHeight : 0
active: timelineViewLoader.active && !root.currentRoom.readOnly
sourceComponent: ChatBar {
id: chatBar
@@ -348,14 +355,17 @@ Kirigami.Page {
});
}
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(root, {
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
(delegateContextMenu.createObject(parent, {
room: room,
author: author,
eventId: eventId,
plainText: plainText,
mimeType: mimeType,
progressInfo: progressInfo,
messageComponentType: messageComponentType,
selectedText,
hoveredLink,
}) as DelegateContextMenu).popup();
}

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat
Kirigami.Dialog {
id: root
property var model
standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
maximumHeight: Kirigami.Units.gridUnit * 24
title: i18nc("@title:menu Seen by/read marker dialog", "Seen By")
contentItem: ColumnLayout {
spacing: 0
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
id: listView
model: root.model
spacing: Kirigami.Units.smallSpacing
onCountChanged: {
if (listView.count === 0) {
root.close();
}
}
delegate: Delegates.RoundedItemDelegate {
id: userDelegate
required property string displayName
required property url avatarUrl
required property color memberColor
required property string userId
implicitHeight: Kirigami.Units.gridUnit * 2
text: displayName
highlighted: false
onClicked: {
root.close();
RoomManager.resolveResource(userDelegate.userId);
}
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
KirigamiComponents.Avatar {
implicitWidth: height
sourceSize {
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
}
source: userDelegate.avatarUrl
name: userDelegate.displayName
color: userDelegate.memberColor
Layout.fillHeight: true
}
QQC2.Label {
text: userDelegate.displayName
textFormat: Text.PlainText
elide: Text.ElideRight
clip: true // Intentional to limit insane Unicode in display names
Layout.fillWidth: true
}
}
}
}
}
}
}

View File

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

View File

@@ -11,7 +11,9 @@ import org.kde.neochat
FormCard.FormCardPage {
id: root
title: i18nc("@title:window", "Load your encrypted messages")
property bool processing: false
title: i18nc("@title:window", "Manage Secret Backup")
topPadding: Kirigami.Units.gridUnit
leftPadding: 0
@@ -25,75 +27,42 @@ FormCard.FormCardPage {
position: Kirigami.InlineMessage.Position.Header
}
property SSSSHandler ssssHandler: SSSSHandler {
id: ssssHandler
Connections {
target: Controller.activeConnection
function onKeyBackupError(): void {
securityKeyField.clear()
root.processing = false
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
banner.visible = true
}
property bool processing: false
connection: Controller.activeConnection
onKeyBackupUnlocked: {
ssssHandler.processing = false
function onKeyBackupUnlocked(): void {
root.processing = false
banner.text = i18nc("@info:status", "Encryption keys restored.")
banner.type = Kirigami.MessageType.Positive
banner.visible = true
}
onError: error => {
if (error !== SSSSHandler.WrongKeyError) {
banner.text = error
banner.visible = true
return;
}
passwordField.clear()
ssssHandler.processing = false
banner.text = i18nc("@info:status", "The security phrase was not correct.")
banner.visible = true
}
}
FormCard.FormHeader {
title: i18nc("@title", "Unlock using Passphrase")
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have a backup passphrase for this account, enter it below.")
}
FormCard.FormTextFieldDelegate {
id: passwordField
label: i18nc("@label:textbox", "Backup Password:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
id: unlockButton
text: i18nc("@action:button", "Unlock")
icon.name: "unlock"
enabled: passwordField.text.length > 0 && !ssssHandler.processing
onClicked: {
ssssHandler.processing = true
banner.visible = false
ssssHandler.unlockSSSSWithPassphrase(passwordField.text)
}
}
}
FormCard.FormHeader {
title: i18nc("@title", "Unlock using Security Key")
}
FormCard.FormCard {
FormCard.FormTextDelegate {
description: i18nc("@info", "If you have a security key for this account, enter it below or upload it as a file.")
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
}
FormCard.FormTextFieldDelegate {
id: securityKeyField
label: i18nc("@label:textbox", "Security Key:")
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
echoMode: TextInput.Password
}
FormCard.FormButtonDelegate {
id: uploadSecurityKeyButton
text: i18nc("@action:button", "Upload from File")
icon.name: "cloud-upload"
enabled: !ssssHandler.processing
enabled: !root.processing
onClicked: {
ssssHandler.processing = true
root.processing = true
openFileDialog.open()
}
}
@@ -101,10 +70,10 @@ FormCard.FormCardPage {
id: unlockSecurityKeyButton
text: i18nc("@action:button", "Unlock")
icon.name: "unlock"
enabled: securityKeyField.text.length > 0 && !ssssHandler.processing
enabled: securityKeyField.text.length > 0 && !root.processing
onClicked: {
ssssHandler.processing = true
ssssHandler.unlockSSSSFromSecurityKey(securityKeyField.text)
root.processing = true
Controller.activeConnection.unlockSSSS(securityKeyField.text)
}
}
}
@@ -120,10 +89,10 @@ FormCard.FormCardPage {
id: unlockCrossSigningButton
icon.name: "emblem-shared-symbolic"
text: i18nc("@action:button", "Request from other Devices")
enabled: !ssssHandler.processing
enabled: !root.processing
onClicked: {
ssssHandler.processing = true
ssssHandler.unlockSSSSFromCrossSigning()
root.processing = true
Controller.activeConnection.unlockSSSS("")
}
}
}

View File

@@ -20,37 +20,62 @@ Kirigami.Dialog {
// Make sure that code is prepared to deal with this property being null
property NeoChatRoom room
property var user
// Used for the "View Main Profile" feature so you can toggle back to the room profile.
property NeoChatRoom oldRoom
property NeoChatConnection connection
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
property CommonRoomsModel model: CommonRoomsModel {
connection: root.connection
userId: root.user.id
}
property LimiterModel limiterModel: LimiterModel {
maximumCount: 5
sourceModel: root.model
}
readonly property bool isSelf: root.user.id === root.connection.localUserId
readonly property bool hasMutualRooms: root.model.count > 0
readonly property bool isRoomProfile: root.room
readonly property string shareUrl: "https://matrix.to/#/" + root.user.id
leftPadding: Kirigami.Units.largeSpacing * 2
rightPadding: Kirigami.Units.largeSpacing * 2
topPadding: Kirigami.Units.largeSpacing * 2
bottomPadding: Kirigami.Units.largeSpacing * 2
standardButtons: Kirigami.Dialog.NoButton
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
title: i18nc("@title:menu Account details dialog", "Account Details")
header: null
contentItem: ColumnLayout {
spacing: 0
RowLayout {
id: detailRow
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Layout.fillWidth: true
KirigamiComponents.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
source: root.room ? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
source: {
if (root.room) {
return root.room.member(root.user.id).avatarUrl;
} else if(root.user.avatarUrl.toString() !== '') {
return root.connection.makeMediaUrl(root.user.avatarUrl);
}
return "";
}
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
}
@@ -75,223 +100,363 @@ Kirigami.Dialog {
id: idLabel
textFormat: TextEdit.PlainText
text: idLabelTextMetrics.elidedText
color: Kirigami.Theme.disabledTextColor
TextMetrics {
id: idLabelTextMetrics
text: root.user.id
elide: Qt.ElideRight
elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
}
}
QQC2.Label {
property CommonRoomsModel model: CommonRoomsModel {
connection: root.connection
userId: root.user.id
}
text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count)
color: Kirigami.Theme.disabledTextColor
visible: model.count > 0
Kirigami.ActionToolBar {
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
}
QQC2.AbstractButton {
id: qrButton
Layout.minimumHeight: avatar.height * 0.75
Layout.maximumHeight: avatar.height * 1.5
Layout.maximumWidth: avatar.height * 1.5
}
contentItem: Barcode {
id: barcode
barcodeType: Barcode.QRCode
content: "https://matrix.to/#/" + root.user.id
}
Kirigami.Heading {
text: i18nc("@title Moderation actions for this user", "Moderation")
level: 2
visible: root.isRoomProfile && moderationToolbar.actions.filter(function (it) { return it.visible; }).length > 0
onClicked: {
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: barcode.content,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
Layout.topMargin: Kirigami.Units.largeSpacing
}
Kirigami.ActionToolBar {
id: moderationToolbar
flat: false
visible: root.isRoomProfile
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
text: i18nc("@action:button Kick the user from the room", "Kick…")
icon.name: "im-kick-user"
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
}
return false;
}
text: i18nc("@action:button Ban this user from the room", "Ban…")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
},
Kirigami.Action {
visible: {
if (root.room) {
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
}
return false;
}
text: i18nc("@action:button Unban the user from this room", "Unban")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
root.room.unban(root.user.id);
root.close();
}
},
Kirigami.Action {
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete",
reporting: false,
connection: root.connection,
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
}
}
]
}
Kirigami.Heading {
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
level: 2
visible: root.isRoomProfile
Layout.topMargin: Kirigami.Units.largeSpacing
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: root.isRoomProfile
Layout.topMargin: Kirigami.Units.smallSpacing
QQC2.Label {
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
}
QQC2.Button {
visible: {
if (root.room) {
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
}
return false;
}
text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level")
icon.name: "document-edit-symbolic"
display: QQC2.AbstractButton.IconOnly
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: barcode.content
QQC2.ToolTip.text: text
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
Kirigami.Chip {
visible: root.room
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
closable: false
checkable: false
onClicked: {
(powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
}) as PowerLevelDialog).open();
root.close();
}
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
Kirigami.Separator {
Layout.fillWidth: true
}
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId && !!root.user
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
icon.name: "im-invisible-user"
onClicked: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Kick this user")
icon.name: "im-kick-user"
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Kick User"),
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
icon: "im-kick-user"
}, {
title: i18nc("@title:dialog", "Kick User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.kickMember(root.user.id, reason);
});
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("invite") && !root.room.containsUser(root.user.id)
enabled: root.room && !root.room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Invite this user")
icon.name: "list-add-user"
onClicked: {
root.room.inviteToRoom(root.user.id);
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
text: i18nc("@action:button", "Ban this user")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Ban User"),
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
icon: "im-ban-user"
}, {
title: i18nc("@title:dialog", "Ban User"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.ban(root.user.id, reason);
});
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
text: i18nc("@action:button", "Unban this user")
icon.name: "im-irc"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
root.room.unban(root.user.id);
root.close();
}
}
FormCard.FormButtonDelegate {
visible: root.room && root.room.canSendState("m.room.power_levels")
text: i18nc("@action:button", "Set user power level")
icon.name: "visibility"
onClicked: {
(powerLevelDialog.createObject(this, {
room: root.room,
userId: root.user.id,
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
}) as PowerLevelDialog).open();
root.close();
}
Component {
id: powerLevelDialog
PowerLevelDialog {
Component {
id: powerLevelDialog
PowerLevelDialog {
id: powerLevelDialog
}
}
}
}
FormCard.FormButtonDelegate {
visible: root.room && (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
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
text: i18nc("@action:button", "Remove recent messages by this user")
icon.name: "delete"
icon.color: Kirigami.Theme.negativeTextColor
onClicked: {
let dialog = ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Remove Messages"),
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
icon: "delete"
}, {
title: i18nc("@title", "Remove Messages"),
width: Kirigami.Units.gridUnit * 25
});
dialog.accepted.connect(reason => {
root.room.deleteMessagesByUser(root.user.id, reason);
});
root.close();
Layout.topMargin: Kirigami.Units.largeSpacing
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
visible: !root.isSelf && root.hasMutualRooms
Layout.topMargin: Kirigami.Units.smallSpacing
Repeater {
model: root.limiterModel
delegate: KirigamiComponents.AvatarButton {
required property string roomName
required property string roomAvatar
required property string roomId
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
name: roomName
source: roomAvatar
onClicked: {
root.close();
RoomManager.resolveResource(roomId);
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.Label {
text: i18ncp("@info:label And '%1' more rooms you have in common with this user, but are not shown", "and 1 more…", "and %1 more…", root.limiterModel.extraCount)
visible: root.limiterModel.extraCount > 0
color: Kirigami.Theme.disabledTextColor
}
}
FormCard.FormButtonDelegate {
visible: root.user.id !== root.connection.localUserId
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
icon.name: "document-send"
onClicked: {
root.connection.requestDirectChat(root.user.id);
root.close();
}
Kirigami.Heading {
text: i18nc("@title Private note for this user", "Private Note")
level: 4
Layout.topMargin: Kirigami.Units.largeSpacing
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button %1 is the name of the user.", "Search room for %1's messages", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName))
icon.name: "search-symbolic"
onClicked: {
((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
}
QQC2.TextArea {
id: noteText
FormCard.FormButtonDelegate {
text: i18n("Copy link")
icon.name: "username-copy"
onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id)
text: root.connection.noteForUser(root.user.id)
textFormat: TextEdit.PlainText
wrapMode: TextEdit.Wrap
placeholderText: i18nc("@info:placeholder", "Only visible to you")
onTextEdited: editTimer.restart()
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
// Prevent unnecessary edits by waiting 1 second
Timer {
id: editTimer
interval: 1000
onTriggered: root.connection.setNoteForUser(root.user.id, noteText.text)
}
}
}
}

View File

@@ -29,6 +29,28 @@
#include <KIO/OpenUrlJob>
#endif
/**
* @brief Stops RoomManager from updating the last room and space config.
*/
class LastRoomBlocker
{
public:
explicit LastRoomBlocker(RoomManager *manager)
: m_manager(manager)
{
Q_ASSERT(manager);
m_manager->m_dontUpdateLastRoom = true;
}
~LastRoomBlocker()
{
m_manager->m_dontUpdateLastRoom = false;
}
private:
RoomManager *m_manager;
};
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openStateConfig())
@@ -282,26 +304,22 @@ void RoomManager::viewEventSource(const QString &eventId)
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
{
if (eventId.isEmpty()) {
qWarning() << "Tried to open event menu with empty event id";
if (!event) {
qWarning() << "Tried to open event menu with empty event";
return;
}
const auto it = room->findInTimeline(eventId);
if (it == room->historyEdge()) {
// This is probably a pending event
return;
}
const auto &event = **it;
Q_EMIT showDelegateMenu(eventId,
room->qmlSafeMember(event.senderId()),
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
room->fileTransferInfo(eventId),
Q_EMIT showDelegateMenu(parent,
room,
event->id(),
room->qmlSafeMember(event->senderId()),
MessageComponentType::typeForEvent(*event),
EventHandler::plainBody(room, event),
EventHandler::richBody(room, event),
EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
room->fileTransferInfo(event->id()),
selectedText,
hoveredLink);
}
@@ -324,17 +342,6 @@ void RoomManager::loadInitialRoom()
resolveResource(m_arg);
}
if (m_isMobile) {
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
// We can't have empty keys in KConfig, so we stored it as "Home"
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, false);
// We don't want to open a room on startup on mobile
return;
}
if (m_currentRoom) {
// we opened a room with the arg parsing already
return;
@@ -347,16 +354,14 @@ void RoomManager::loadInitialRoom()
void RoomManager::openRoomForActiveConnection()
{
if (!m_connection) {
setCurrentRoom({});
setCurrentSpace({}, false);
return;
}
Q_ASSERT(m_connection);
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
if (lastSpace == u"Home"_s) {
lastSpace.clear();
}
setCurrentSpace(lastSpace, true);
// We don't want to open a room on startup on mobile
setCurrentSpace(lastSpace, !m_isMobile);
}
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
@@ -513,7 +518,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
Q_EMIT connectionChanged();
}
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
{
m_currentSpaceId = spaceId;
@@ -533,25 +538,65 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
}
if (!setRoom) {
return;
}
// If we requested to change to the last opened room, do so:
if (goToLastUsedRoom) {
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
LastRoomBlocker blocker(this);
// We intentionally don't want to open the last room on mobile
if (m_isMobile) {
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home":
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
}
// We can't have empty keys in KConfig, so it's stored as "Home"
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
resolveResource(lastRoom, "no_join"_L1);
return;
// If no last room was opened, go to the space home:
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
// Fallback to no room opened:
setCurrentRoom({});
}
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
resolveResource(spaceId, "no_join"_L1);
return;
}
QString RoomManager::findSpaceIdForCurrentRoom() const
{
if (!m_currentRoom) {
return m_currentSpaceId;
}
setCurrentRoom({});
if (m_currentRoom->isDirectChat()) {
const auto roomsInSpace = SpaceHierarchyCache::instance().getRoomListForSpace(m_currentSpaceId, false);
if (roomsInSpace.contains(m_currentRoom->id())) {
return m_currentSpaceId;
}
return "DM"_L1;
}
const auto &parentSpaces = SpaceHierarchyCache::instance().parentSpaces(m_currentRoom->id());
if (parentSpaces.contains(m_currentSpaceId)) {
return m_currentSpaceId;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome()) {
return {};
}
if (const auto &parent = m_connection->room(m_currentRoom->canonicalParent())) {
for (const auto &parentParent : SpaceHierarchyCache::instance().parentSpaces(parent->id())) {
if (SpaceHierarchyCache::instance().parentSpaces(parentParent).isEmpty()) {
return parentParent;
}
}
return parent->id();
}
for (const auto &space : parentSpaces) {
if (SpaceHierarchyCache::instance().parentSpaces(space).isEmpty()) {
return space;
}
}
if (m_currentRoom->isSpace()) {
return m_currentSpaceId;
}
return {};
}
void RoomManager::setCurrentRoom(const QString &roomId)
@@ -571,57 +616,25 @@ void RoomManager::setCurrentRoom(const QString &roomId)
}
Q_EMIT currentRoomChanged();
if (m_connection) {
if (!m_dontUpdateLastRoom) {
if (roomId.isEmpty()) {
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
} else {
// We can't have empty keys in KConfig, so name it "Home"
if (m_currentSpaceId.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(m_currentSpaceId, roomId);
}
}
}
if (roomId.isEmpty()) {
return;
}
if (m_currentRoom->isSpace()) {
return;
}
if (m_currentRoom->isDirectChat()) {
const auto roomsInSpace = SpaceHierarchyCache::instance().getRoomListForSpace(m_currentSpaceId, false);
if (!roomsInSpace.contains(m_currentRoom->id()) && m_currentSpaceId != "DM"_L1) {
setCurrentSpace("DM"_L1, false);
}
return;
}
const auto &parentSpaces = SpaceHierarchyCache::instance().parentSpaces(roomId);
if (parentSpaces.contains(m_currentSpaceId)) {
return;
}
static auto config = NeoChatConfig::self();
if (config->allRoomsInHome()) {
setCurrentSpace({}, false);
return;
}
if (const auto &parent = m_connection->room(m_currentRoom->canonicalParent())) {
for (const auto &parentParent : SpaceHierarchyCache::instance().parentSpaces(parent->id())) {
if (SpaceHierarchyCache::instance().parentSpaces(parentParent).isEmpty()) {
setCurrentSpace(parentParent, false);
return;
}
}
setCurrentSpace(parent->id(), false);
return;
}
for (const auto &space : parentSpaces) {
if (SpaceHierarchyCache::instance().parentSpaces(space).isEmpty()) {
setCurrentSpace(space, false);
return;
}
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
// We can't have empty keys in KConfig, so name it "Home"
if (spaceIdForRoom.isEmpty()) {
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
} else {
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
}
if (m_currentSpaceId != spaceIdForRoom) {
setCurrentSpace(spaceIdForRoom, false);
}
}
setCurrentSpace({}, false);
}
void RoomManager::clearCurrentRoom()

View File

@@ -233,7 +233,8 @@ public:
/**
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
Q_INVOKABLE void
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
@@ -306,7 +307,9 @@ Q_SIGNALS:
/**
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(const QString &eventId,
void showDelegateMenu(QObject *parent,
NeoChatRoom *room,
const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
@@ -337,6 +340,11 @@ Q_SIGNALS:
void currentSpaceChanged();
protected:
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
friend class LastRoomBlocker;
private:
bool m_isMobile = false;
@@ -373,8 +381,22 @@ private:
void setCurrentRoom(const QString &roomId);
// Space ID, "DM", or empty string
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
/**
* @brief Find the most appropriate space for the currently selected room
*
* Should be used to figure out what space to switch to after a room change.
*
* @return The Space ID that the currently set room should be displayed as part of. (or "DM" for DM and "" for Home)
*/
QString findSpaceIdForCurrentRoom() const;
/**
* @brief Sets the current space.
*
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
*/
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
/**
* @brief Resolve a user URI.

View File

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

View File

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

View File

@@ -286,6 +286,8 @@ QQC2.Control {
quickFormatBar.selectionStart = selectionStart;
quickFormatBar.selectionEnd = selectionEnd;
quickFormatBar.open();
} else if (quickFormatBar.visible) {
quickFormatBar.close();
}
}

View File

@@ -65,7 +65,7 @@ QQC2.Popup {
padding: 2
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window.width)
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, QQC2.ApplicationWindow.window?.width)
contentItem: EmojiPicker {
id: emojiPicker
height: 400

View File

@@ -148,7 +148,7 @@ ColumnLayout {
id: quickReactions
Layout.fillWidth: true
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
delegate: EmojiDelegate {
required property string modelData

View File

@@ -21,12 +21,6 @@ FormCard.FormCard {
onToggled: NeoChatConfig.threads = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
checked: NeoChatConfig.secretBackup
onToggled: NeoChatConfig.secretBackup = checked
}
FormCard.FormCheckDelegate {
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
checked: NeoChatConfig.phone3PId

View File

@@ -33,6 +33,8 @@ target_sources(LibNeoChat PRIVATE
events/imagepackevent.cpp
events/pollevent.cpp
jobs/neochatgetcommonroomsjob.cpp
jobs/neochatreportroomjob.cpp
jobs/neochatreportuserjob.cpp
models/actionsmodel.cpp
models/completionmodel.cpp
models/completionproxymodel.cpp

View File

@@ -3,6 +3,10 @@
#include "chatbarcache.h"
#include <QMimeData>
#include <KUrlMimeData>
#include <Quotient/roommember.h>
#include "eventhandler.h"
@@ -146,7 +150,12 @@ Quotient::RoomMember ChatBarCache::relationAuthor() const
if (m_relationId.isEmpty()) {
return room->member(QString());
}
return room->member((*room->findInTimeline(m_relationId))->senderId());
const auto [event, _] = room->getEvent(m_relationId);
if (event != nullptr) {
return room->member(event->senderId());
}
qWarning() << "Failed to find relation" << m_relationId << "in timeline?";
return room->member(QString());
}
bool ChatBarCache::relationAuthorIsPresent() const
@@ -169,8 +178,8 @@ QString ChatBarCache::relationMessage() const
return {};
}
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
return EventHandler::markdownBody(&**event);
if (auto [event, _] = room->getEvent(m_relationId); event != nullptr) {
return EventHandler::markdownBody(event);
}
return {};
}
@@ -276,11 +285,6 @@ void ChatBarCache::postMessage()
return;
}
const auto replyIt = room->findInTimeline(replyId());
if (replyIt == room->historyEdge()) {
isReply = false;
}
auto content = std::make_unique<Quotient::EventContent::TextContent>(sendText, u"text/html"_s);
room->post<Quotient::RoomMessageEvent>(text(), *std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result), std::move(content), relatesTo);
@@ -295,4 +299,17 @@ void ChatBarCache::clearCache()
clearRelations();
}
void ChatBarCache::drop(QList<QUrl> u, const QString &transferPortal)
{
QMimeData mimeData;
mimeData.setUrls(u);
if (!transferPortal.isEmpty()) {
mimeData.setData(u"application/vnd.portal.filetransfer"_s, transferPortal.toLatin1());
}
auto urls = KUrlMimeData::urlsFromMimeData(&mimeData);
if (urls.size() > 0) {
setAttachmentPath(urls[0].toString());
}
}
#include "moc_chatbarcache.cpp"

View File

@@ -198,6 +198,8 @@ public:
*/
Q_INVOKABLE void postMessage();
Q_INVOKABLE void drop(QList<QUrl> urls, const QString &transferPortal);
Q_SIGNALS:
void textChanged();
void relationIdChanged(const QString &oldEventId, const QString &newEventId);

View File

@@ -12,6 +12,10 @@ QString PowerLevel::nameForLevel(Level level)
return i18n("Moderator");
case PowerLevel::Admin:
return i18n("Admin");
case PowerLevel::Owner:
return i18nc("The person that owns a room", "Owner");
case PowerLevel::Creator:
return i18nc("The person that created a room", "Creator");
case PowerLevel::Mute:
return i18n("Mute");
case PowerLevel::Custom:
@@ -30,6 +34,8 @@ int PowerLevel::valueForLevel(Level level)
return 50;
case PowerLevel::Admin:
return 100;
case PowerLevel::Owner:
return 150;
case PowerLevel::Mute:
return -1;
default:
@@ -46,8 +52,12 @@ PowerLevel::Level PowerLevel::levelForValue(int value)
return PowerLevel::Moderator;
case 100:
return PowerLevel::Admin;
case 150:
return PowerLevel::Owner;
case -1:
return PowerLevel::Mute;
case std::numeric_limits<int>::max():
return PowerLevel::Creator;
default:
return PowerLevel::Custom;
}

View File

@@ -31,10 +31,12 @@ public:
enum Level {
Member, /**< A basic member. */
Moderator, /**< A moderator with enhanced powers. */
Admin, /**< The highest power level in the room. */
Admin, /**< Power level 100. */
Owner, /**< Power level 150. */
Mute, /**< The level to remove posting privileges. */
NUMLevels,
Custom, /**< A non-standard value. Intentionally after NUMLevels so it doesn't appear in the model. */
Creator, /**< The user creating the (co-)creating the room. */
};
Q_ENUM(Level);

View File

@@ -389,9 +389,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
return i18n("left the room");
}
if (const auto &reason = e.contentJson()["reason"_L1].toString().toHtmlEscaped(); !reason.isEmpty()) {
return i18n("has put %1 out of the room: %2", subjectName, reason);
return i18n("has removed %1 from the room: %2", subjectName, reason);
}
return i18n("has put %1 out of the room", subjectName);
return i18n("has removed %1 from the room", subjectName);
case Membership::Ban:
if (e.senderId() != e.userId()) {
if (e.reason().isEmpty()) {
@@ -697,6 +697,9 @@ QString EventHandler::subtitleText(const NeoChatRoom *room, const Quotient::Room
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
return {};
}
if (room->isDirectChat()) {
return plainBody(room, event, true);
}
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? u" "_s : u": "_s) + plainBody(room, event, true);
}

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatreportroomjob.h"
using namespace Quotient;
NeochatReportRoomJob::NeochatReportRoomJob(const QString &userId, const QString &reason)
: BaseJob(HttpVerb::Post, u"ReportRoomJob"_s, makePath(" /_matrix/client/v3/", userId, "/report"))
{
QJsonObject _dataJson;
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
setRequestData({_dataJson});
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
// TODO: Remove once libQuotient updates to Matrix API v1.14
class NeochatReportRoomJob : public Quotient::BaseJob
{
public:
explicit NeochatReportRoomJob(const QString &roomId, const QString &reason);
};

View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "neochatreportuserjob.h"
using namespace Quotient;
NeochatReportUserJob::NeochatReportUserJob(const QString &userId, const QString &reason)
: BaseJob(HttpVerb::Post, u"ReportUserJob"_s, makePath("/_matrix/client/v3/users/", userId, "/report"))
{
QJsonObject _dataJson;
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
setRequestData({_dataJson});
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Quotient/jobs/basejob.h>
// TODO: Remove once libQuotient updates to Matrix API v1.14
class NeochatReportUserJob : public Quotient::BaseJob
{
public:
explicit NeochatReportUserJob(const QString &userId, const QString &reason);
};

View File

@@ -59,7 +59,7 @@ QList<ActionsModel::Action> actions{
Action{
u"shrug"_s,
[](const QString &message, NeoChatRoom *, ChatBarCache *) {
return u"¯\\\\_(ツ)_/¯ %1"_s.arg(message);
return u"¯\\\\\\_(ツ)\\_/¯ %1"_s.arg(message);
},
Quotient::RoomMessageEvent::MsgType::Text,
kli18n("<message>"),

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