Compare commits

..

211 Commits
1.0 ... v1.1.1

Author SHA1 Message Date
Carl Schwan
cc372f67eb Update version to 1.1.1 2021-02-23 14:39:22 +01:00
Tobias Fella
aaa042a2f0 Fix include
(cherry picked from commit 37a681596b)
2021-02-23 11:36:29 +00:00
Tobias Fella
6fdc8a9298 Fix another include
(cherry picked from commit b67a35bfe3)
2021-02-23 11:36:09 +00:00
Carl Schwan
1526c81a2f Add 1.1 release information 2021-02-22 21:01:57 +01:00
Carl Schwan
03d13bb543 Bump version 2021-02-22 20:58:10 +01:00
Carl Schwan
d8916aa133 Update AppStream 2021-02-22 20:56:55 +01:00
Carl Schwan
bb3b3bc088 Input field fixes
* Message with multiple mentions are not broken in IRC. Fix #267
* Editing a message won't remove mentions anymore
2021-02-22 19:09:56 +00:00
l10n daemon script
1234132a05 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"
2021-02-22 10:25:39 +01:00
l10n daemon script
52e5ec50c6 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"
2021-02-20 12:01:33 +01:00
l10n daemon script
16f661f6ae 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"
2021-02-19 17:00:04 +01:00
l10n daemon script
df1835de2d 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"
2021-02-18 11:51:49 +01:00
l10n daemon script
1432ba2368 GIT_SILENT made messages (after extraction) 2021-02-18 10:23:48 +01:00
Tobias Fella
edcbc9ab01 Port away from QQC1
The only usage was a dialog that was never called


(cherry picked from commit f71bbe20dc)
2021-02-17 21:34:30 +00:00
Carl Schwan
1afbd42523 Revert "Revert "Improve sending message with mentions""
This reverts commit 0f043e36c4.
2021-02-17 21:58:57 +01:00
Tobias Fella
7ff54f62f3 Backport stickerevents 2021-02-17 21:00:11 +00:00
Tobias Fella
cc2b183fc5 Revert "Switch to newDisplayName() and newAvatarUrl()"
This reverts commit a1b66f3aa6.
2021-02-17 21:00:11 +00:00
Tobias Fella
2558e1f6b5 DOn't try to compile without keywords 2021-02-17 21:00:11 +00:00
Tobias Fella
a7e61f0e20 Revert "Fix build failure"
This reverts commit ab8dabc280.
2021-02-17 21:00:11 +00:00
Tobias Fella
345eb0c229 Revert "Bump dependencies to libQuotient 0.7 (master)"
This reverts commit d646962ea1.
2021-02-17 21:00:11 +00:00
Tobias Fella
91ef8806f2 Show contextdrawer only in RoomPage 2021-02-17 20:16:38 +00:00
Tobias Fella
e546c12b45 Don't underline links 2021-02-17 20:16:19 +00:00
Carl Schwan
43f81fcead Use version less Qt target 2021-02-15 19:46:07 +01:00
l10n daemon script
bfbca5b1c2 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"
2021-02-15 07:09:27 +01:00
Anjani Kumar
babbc039ab Focus inputField after cancelling edit/reply 2021-02-14 15:32:02 +05:30
Yuri Chornoivan
d3b8c9b98e Fix minor typo: sytem -> system 2021-02-14 08:53:48 +02:00
Tobias Fella
0ca2eb4008 Add option to disable system tray integration
Implements #59
2021-02-13 19:18:28 +00:00
Anjani Kumar
3979cf59ce Clears inputField when edit is cancelled. 2021-02-13 19:17:06 +00:00
Tobias Fella
f4ab281789 Revert "Fix broken i18ncp call"
This reverts commit 7d100b2a0f
2021-02-12 15:21:20 +00:00
Tobias Fella
7d100b2a0f Fix broken i18ncp call 2021-02-11 23:18:14 +01:00
Tobias Fella
9432e28685 Fix opening a second invitation 2021-02-10 23:13:16 +01:00
Arnav Rawat
b84375749b Close menu after selecting a reaction
Fixes #256
2021-02-10 10:33:35 +00:00
l10n daemon script
07dffa7e73 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"
2021-02-10 08:16:48 +01:00
l10n daemon script
7816d220ea GIT_SILENT made messages (after extraction) 2021-02-10 03:39:08 +01:00
Tobias Fella
28dfc4b6d7 Update gitignore 2021-02-08 18:09:01 +01:00
Tobias Fella
d78196d7c7 Mention the nightly build in README 2021-02-08 15:04:33 +00:00
Tobias Fella
21c4f8b636 Fix typo 2021-02-08 15:00:55 +00:00
Yuri Chornoivan
4c7be7426e Fix minor typos 2021-02-08 14:53:32 +02:00
l10n daemon script
bd4dfb037a 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"
2021-02-08 06:52:38 +01:00
l10n daemon script
a7720409ca GIT_SILENT made messages (after extraction) 2021-02-08 02:34:53 +01:00
Tobias Fella
b689e55068 Refactor and cleanup dead qml 2021-02-07 22:34:07 +01:00
Tobias Fella
464c48540e Improve first-run UX
- Replace LoginPage with step-by-step approach to support different login flows
- Implement login using SSO
2021-02-07 21:23:31 +00:00
l10n daemon script
e7bada4cde 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"
2021-02-07 06:47:50 +01:00
l10n daemon script
9cd441dc1d GIT_SILENT made messages (after extraction) 2021-02-07 02:52:49 +01:00
Carl Schwan
ff6bff208a Remove room description from room header
After using it for some time, I don't think it is really usefull but it
makes the UI more visually heavy, it doesn't show it completely and also
has very bad contrast.

Fix #197
2021-02-07 00:29:08 +00:00
Tobias Fella
7ae222d427 Fix typo 2021-02-06 20:50:37 +01:00
Carl Schwan
82945ab153 Make right clicking on message works again
This is now using TapHandler that can be used in a Layout without
warning about undefined behaviors.
2021-02-06 00:44:07 +00:00
Nicolas Fella
1411d28b81 Fix crash when logging out and back in
we get the platformtheme attached property from a random user object,
but that user is destroyed when logging out. Instead use the controller
as parent since that survives the logout
2021-02-06 00:36:51 +00:00
Carl Schwan
6dcfad1f8d Don't show login page when starting NeoChat
This was caused by us calling initiated to early. Only do it when no
accounts exists or that at least one account fails to login.

Fix #248
2021-02-06 00:16:30 +00:00
Carl Schwan
10054bf879 Fix RoomDrawer is visible when no room is open
Fix #246
2021-02-06 00:03:10 +01:00
Carl Schwan
66b06aac6e Use correct username for typing users
Fix #257
2021-02-05 22:52:32 +00:00
Carl Schwan
c17392bd9d Add minimul width and height to modal window
Fix #253
2021-02-05 23:40:32 +01:00
Tobias Fella
1cb6b3bbd6 Consistently use pragma once in all headers 2021-02-04 23:14:54 +01:00
Tobias Fella
546d17b1a2 Correctly open all kinds of matrix.to links in TextDelegate and MessageDelegateContextMenu 2021-02-04 20:23:16 +00:00
Tobias Fella
72907a1f18 Refactor and fix invitations
-Move invitation handling into RoomPage and delete InvitationPage
-Open the new room after accepting the invitation
2021-02-04 20:22:53 +00:00
Bart Ribbers
465334e23f Improve the look of reactions
- Always show the reaction counts. Element does this too and it makes sure
the look is consistent, no matter how many reactions there are.
- Show a slight border around the background to make the transition to
non-reaction less "grainy"
2021-02-04 18:28:53 +01:00
Nicolas Fella
66bcc2105a Only keep one Kirigami theme instance for all users
Fetching the Kirigami Theme via attached properties is expensive. The
theme is also going to be the same for all users so it's enough to only
do it once.
2021-02-03 21:57:54 +00:00
Nicolas Fella
f217bbd3c4 Don't fetch same modeldata twice 2021-02-02 21:51:27 +00:00
Nicolas Fella
70691fb295 Fix hiding replaced events
https://invent.kde.org/network/neochat/-/commit/5993c1f6 accidentally
switched from SpecialMarksRole to MessageRole which is not only slower
but also completely wrong
2021-02-02 21:51:27 +00:00
Nicolas Fella
7aedfd0e17 Move message filtering to C++
The filter callback is called very often (O(messages)). The current
filter model shows some significant overhead in QML internals. Moving
that to C++ makes it quite a bit faster.
2021-02-02 21:51:27 +00:00
Nicolas Fella
92e00587f7 Use QSystemTrayIcon instead of KStatusNotifierItem
KSNI doesn't support Windows and macOS and we don't need any of the features it provides over QSystemTrayIcon

Also remove some dead code
2021-02-02 21:51:05 +00:00
Arnav Rawat
ab4db4dd3d remove accidental qdebug 2021-02-01 13:18:55 +00:00
Arnav Rawat
b4e996aecd Adds ability to specify server with /join
Fixes bug #232
2021-02-01 13:18:55 +00:00
Nicolas Fella
5c8b9c0803 Don't use KDBusService on macOS 2021-01-31 22:06:44 +01:00
Nicolas Fella
c1d5883af9 Add missing semicolon 2021-01-27 20:09:45 +01:00
Tobias Fella
dae7ee2f67 Readd icon for gitlab 2021-01-27 18:54:13 +01:00
Nicolas Fella
e6f2b5ea7f Don't use KDBusAddons on Windows
It's only used for KDBusService, which likely doesn't work properly on Windows
2021-01-27 15:36:55 +01:00
Nicolas Fella
9603811a6d Add app icon for Windows 2021-01-27 14:51:10 +01:00
l10n daemon script
d9cf7ce552 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"
2021-01-27 08:48:35 +01:00
Tobias Fella
4894470e7d Fix crash on name change events 2021-01-27 01:23:49 +01:00
Nicolas Fella
8e6d1a8ea2 Fix warning 2021-01-26 17:35:02 +01:00
Nicolas Fella
c2b388d553 Use breeze QStyle on Windows
On Windows we want to use qqc2-desktop-style together with the Breeze QStyle instead of the default QStyle
2021-01-26 16:08:47 +01:00
Arnav Rawat
f67f319854 Fix sending attachments/files
This commit lets attachments be sent by themselves and prevents
a crash when a text message is sent with an attachment
2021-01-24 20:38:40 +00:00
Yuri Chornoivan
fde637b1df Add i18n() 2021-01-23 21:49:38 +02:00
Tobias Fella
75d3b346ac Actually save the settings 2021-01-23 16:39:34 +00:00
l10n daemon script
80b6d80c7d 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"
2021-01-23 06:40:44 +01:00
Carl Schwan
0f043e36c4 Revert "Improve sending message with mentions"
This reverts commit b9d34487a4
2021-01-22 14:45:40 +00:00
Carl Schwan
b9d34487a4 Improve sending message with mentions
* Fix bug with reply having broken mentions (@$1:$2)
* Fix mentions disapearing from edited messages
* Fix formatting disapearing from edited messages
2021-01-21 22:56:19 +01:00
Tobias Fella
157f7cd870 Add logo for invent 2021-01-21 15:27:10 +01:00
l10n daemon script
cec47b40cc 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"
2021-01-21 06:57:29 +01:00
Tobias Fella
f7cbb876f0 Make room topic in RoomDrawer readOnly 2021-01-20 15:17:10 +01:00
Tobias Fella
6f7f0e025d Fix showing user's displayName instead of mxid in roomlist delegate subtitles 2021-01-18 22:27:14 +01:00
l10n daemon script
30eeb538e0 GIT_SILENT made messages (after extraction) 2021-01-18 02:44:33 +01:00
Tobias Fella
fe1e3ee374 Remove markdown links from 'body' of messages
- Markdown is not in the matrix spec
- Clients use the 'body' text for things like notifications, which render these links as plain text
2021-01-17 01:54:41 +01:00
Tobias Fella
a653be8be8 Load serverAddress using QUrl::fromUserInput()
Fixes login when 'https://' is not added to the server url
2021-01-17 01:32:03 +01:00
Yaroslav Sidlovsky
6893cb361e Fix displaying user names with bold text
Property "font.bold" is ignored for QQC2.Label (see: https://phabricator.kde.org/D14495)
2021-01-16 15:16:22 +03:00
l10n daemon script
038441b854 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"
2021-01-16 06:41:58 +01:00
l10n daemon script
b7da732a15 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"
2021-01-15 06:38:23 +01:00
Carl Schwan
7762f5f5ae Don't load events if not needed 2021-01-14 21:11:05 +00:00
Carl Schwan
1abc28ad7f Make sure we load events when opening a room 2021-01-14 20:53:11 +00:00
Carl Schwan
c24c25eb38 Be less noisy 2021-01-14 20:32:15 +01:00
Nate Graham
bd11f543f5 Regularize context menu
"Open in new Window" goes on top, as it does in most other context menus
with similar items

"Leave room" goes on the bottom with a separator above it, because it's
a mildly destructive action.
2021-01-14 08:16:55 -07:00
l10n daemon script
085ebaa451 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"
2021-01-14 07:00:09 +01:00
Christopher Hock
e5771e2733 Change Comment in line 25 of neochat.notifyrc 2021-01-14 00:49:39 +01:00
Carl Schwan
3ebda274ef Fix broken name 2021-01-13 23:15:48 +00:00
Christopher Hock
3ac85bacad Change color of role description to light grey. 2021-01-13 21:45:49 +00:00
Carl Schwan
eff8c08ccf Add launcher badge to NeoChat showing the unread count 2021-01-13 20:14:51 +00:00
Carl Schwan
a9c2e3ec49 Fix appstream file
(cherry picked from commit f25bc6bac6)
2021-01-13 10:09:27 +01:00
Carl Schwan
92488343cc Update appdata 2021-01-12 23:01:56 +01:00
l10n daemon script
59df28822c 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"
2021-01-12 06:44:34 +01:00
l10n daemon script
8b0a14a2cf GIT_SILENT made messages (after extraction) 2021-01-12 02:46:12 +01:00
Carl Schwan
d9128ca483 Fix the white bar in the room page's header 2021-01-11 22:18:45 +00:00
Carson Black
07f637c854 Improve appearance of room listing
This ports the room list delegates to Kirigami.BasicListItem leading/trailing for a more consistent appearance with other applications, and adjusts how their context menus look and behave
2021-01-11 21:25:29 +00:00
Carl Schwan
a3e1e1d0a4 Fix autocompletion
Now it will save a map from display name to id and use that to generate
clean matrix.to links. This also make sure the colors used for the
preview are correct by using NeoChatUser and fix the bug with the regex
by simply removing the regex.

Fix #234
2021-01-11 02:19:55 +01:00
Yaroslav Sidlovsky
ed26e87c96 Display table borders 2021-01-09 15:02:24 +01:00
Carl Schwan
f4784bb0a1 Allow opening window in a secondary window 2021-01-09 13:32:16 +00:00
Yuri Chornoivan
a82b9dc14e Fix minor typos 2021-01-09 09:19:11 +02:00
Carl Schwan
2cb38ad1ea Add filter search field in room drawer
Fix #195
2021-01-09 01:02:19 +01:00
Carl Schwan
4be3eac7af Fix avatar loading in multiple places and prefers name instead of
display name for avatar fallback.

This also fixes a bug where users didn't get their avatar loaded in the
room list.

Fix #209
2021-01-09 00:37:13 +01:00
Carl Schwan
de23eef519 Fix PgUp/PgDn keys in message view switch rooms
Now use Ctr+PgUp/PgDn keys instead

Fix #213
2021-01-09 00:15:02 +01:00
Carl Schwan
cd1bec9977 Introduce the ActionsHandler 2021-01-08 23:12:09 +00:00
Adam Szopa
8e846f73d7 Reference the stable release 2021-01-08 22:28:53 +00:00
Nate Graham
af7003e680 Disable "Send message" button when there's no message to send 2021-01-08 14:42:21 -07:00
Nate Graham
cb57a1ec06 Fix case of anchors being set on an item in a Layout 2021-01-08 14:09:04 -07:00
Carl Schwan
2e0096380f Fix NeoChat not syncing
This problem was caused because addConnection was starting the sync
proccess unfortunally because the user wasn't connected this aborted
almost immediately and then the sync proccess wouldn't run at all.

Now start the sync proccess after making sure we are connected.

Fix #228
2021-01-08 21:42:07 +01:00
Carl Schwan
ca5f95b298 Handle non-consistent configuration 2021-01-08 21:26:56 +01:00
Carl Schwan
f6ac8ccb45 Improve timeline state event text representation
Change are inspired by Quatermion model

* Fix invite events not getting displayed correctly
* Add some options to control what get displayed (for the moment without
a GUI
* Show reason for a state event if it exists
2021-01-08 17:18:48 +00:00
Carl Schwan
249054b57f Fix initial loading of room 2021-01-08 17:17:52 +00:00
Nate Graham
ab8dabc280 Fix build failure
isJobRunning() -> isJobPending()
2021-01-08 07:35:29 -07:00
Jonathan Riddell
43c7e00ec5 add a matrix channel for this app which is to connect to matrix 2021-01-05 17:08:06 +00:00
Adriaan de Groot
085bd4a2cf CMake: systematically use the feature-summary
There's not much point in having a feature summary that will
trip over just-a-few of the required packages, while also
using REQUIRED in find_package() calls -- then you have to
re-run CMake for all the REQUIRED ones you're missing,
and then one more time for the packages that are required
in the feature summary.

Use the feature summary (e.g. TYPE REQUIRED) consistently.
Then you can run CMake once and learn about all the missing
dependencies in one go.
2021-01-05 16:43:36 +01:00
Noah Davis
6cc29f0254 Add LicenseRef-KDE-Accepted-LGPL license file 2021-01-04 14:02:02 -05:00
Noah Davis
338553de16 [ChatTextInput] Fix isImage (no such property) and rgba (Should be Qt.rgba) 2021-01-04 13:58:34 -05:00
Noah Davis
9a17c07fdd [ChatTextInput] support more image formats and use icons for non-image attached files 2021-01-04 13:58:34 -05:00
Noah Davis
50d8bd5b7e Add FileType singleton
This singleton is used to get the mimetype info for files as well as supported formats for Images and AnimatedImages
2021-01-04 13:58:34 -05:00
Mathew Broady
f232c40955 Remove unused "parent" parameter warning for DevicesModel::rowCount() 2021-01-02 15:47:01 +11:00
l10n daemon script
cbc082c1b6 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"
2021-01-01 06:26:01 +01:00
l10n daemon script
d45b0675fb GIT_SILENT made messages (after extraction) 2021-01-01 02:32:59 +01:00
Carl Schwan
c60ee602e2 Add maximum width to room heading in sidebar 2020-12-30 14:18:33 +00:00
Carl Schwan
8224d3ae9f Save and restore window size 2020-12-30 13:19:16 +00:00
Carl Schwan
4463e3e3f2 Add edited flag to edited messages
Fix #206
2020-12-30 13:17:59 +00:00
Tobias Fella
e6b97e3350 Fix accountCount not updating correctly 2020-12-30 13:17:22 +00:00
Tobias Fella
2c1cbc91d8 Fix active connection not loading on startup 2020-12-30 13:17:22 +00:00
Tobias Fella
bd00a73aa9 Ask for consent to terms and conditions if required 2020-12-30 13:17:14 +00:00
Nicolas Fella
88cc972edc Build with QT_NO_KEYWORDS 2020-12-29 14:28:49 +00:00
Carl Schwan
5c8d916752 Add support for stickers
Fix #130
2020-12-29 14:28:32 +00:00
Carl Schwan
9ba0a755e4 Disable menu item when login in
Fix #204
2020-12-29 14:27:28 +00:00
l10n daemon script
4e765f51a7 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"
2020-12-29 06:16:30 +01:00
l10n daemon script
60e0ad9c48 GIT_SILENT made messages (after extraction) 2020-12-29 02:25:08 +01:00
Carl Schwan
5a831732c5 Fix Platform is undefined bug 2020-12-29 01:43:23 +01:00
Tobias Fella
494e6dca42 Android: Add missing icons 2020-12-28 15:40:26 +01:00
Carl Schwan
feb123c1e6 Add new shortcut 2020-12-28 12:47:59 +01:00
Carl Schwan
1f065e46cf Simplify shortcuts code in hamburger menu 2020-12-28 12:35:50 +01:00
Carl Schwan
a4cebe9b36 Add Minimum Size to Screen Geometry
Fix #184
2020-12-28 10:55:04 +01:00
Carl Schwan
a929f7bca3 Move Header Collapse Button to the Right
Fix #191
2020-12-28 10:53:08 +01:00
Carl Schwan
f00cd82676 Add a logout action from the menubar
Fix #188
2020-12-28 10:51:20 +01:00
Carl Schwan
c69d3587ba Allow editing text and also hide edits from the timeline 2020-12-28 09:37:17 +00:00
Shantanu Tushar
9d82ebb0ed Use the I-beam cursor when hovering on chat message text field 2020-12-28 10:25:17 +01:00
Tobias Fella
724f10a895 Don't load empty images from imageprovider
Previously, when there was no avatar set, the source property of Avatar was still set to 'image://mxc/',
which caused Avatar to load that from the imageprovider. The imageprovider can't provide an empty image and aborts with error
2020-12-28 01:28:13 +01:00
Tobias Fella
0fe0f45944 Fix segfault/assert when logging out of account 2020-12-28 00:07:37 +00:00
Tobias Fella
b1080df9dd Show Loading page during initial sync 2020-12-28 00:00:56 +00:00
Eamonn Rea
066ab1e6c6 Fix cursorShape not updating for messages 2020-12-27 23:51:06 +00:00
Nicolas Fella
a52dbb0042 Remove modules from Qt includes 2020-12-28 00:36:17 +01:00
Nicolas Fella
6a1fd3ff31 Don't call stopSync when destroying controller
Connection does that internally already
2020-12-28 00:31:10 +01:00
Carl Schwan
9c97983794 Use Noto Color Emoji instead of Emoji One 2020-12-27 23:08:10 +00:00
Tobias Fella
3858956e82 Fix login for homeservers without well-known 2020-12-27 23:36:35 +01:00
Antonio Rojas
93e0a2b2f6 Add missing cmake check for kitemmodels
Otherwise packagers have no way to know that it is a runtime dependency
2020-12-27 13:07:12 +01:00
Carl Schwan
dce3b796c2 Don't translate something we shouldn't 2020-12-26 15:58:57 +00:00
l10n daemon script
a5cf0af004 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"
2020-12-26 06:13:48 +01:00
l10n daemon script
e55f0bd84b GIT_SILENT made messages (after extraction) 2020-12-26 02:28:29 +01:00
Tobias Fella
c449a8fafe Apply clang-format 2020-12-25 22:23:35 +01:00
Tobias Fella
6351454759 CMake Cleanup 2020-12-25 17:59:50 +01:00
Tobias Fella
8aec6b67cb Fix image saving 2020-12-24 13:29:35 +01:00
Eamonn Rea
c515f1bdbd Fix typo
Nightly was incorrectly spelled as nigthly
2020-12-24 11:20:39 +01:00
l10n daemon script
43b094d446 GIT_SILENT made messages (after extraction) 2020-12-24 02:28:21 +01:00
Tobias Fella
1a28e52d79 Set a default name when saving files
Fixes #173
2020-12-24 01:54:19 +01:00
Nicolas Fella
57e05e2114 Default to org.kde.desktop QQC2 style
plasma-integration does that for us, but that obviously doesn't work for non-Plasma desktops.
2020-12-24 00:15:37 +01:00
Carl Schwan
b4e528b047 Remove old code 2020-12-23 23:11:23 +00:00
Carl Schwan
59f9c36854 Dismiss reply when clicking on Esc
Fix #175
2020-12-23 18:01:09 +01:00
Carl Schwan
3fe10bfc3c Update appstream information 2020-12-23 12:21:12 +01:00
Devin Lin
8f348eb4fd Cap height of send message box, and make it scrollable 2020-12-23 09:07:37 +00:00
Devin Lin
93f35faf95 Fix room header text alignment and add support for two line room descriptions 2020-12-23 08:53:09 +00:00
Devin Lin
87a7a34d80 Show feedback on avatar hover 2020-12-23 08:51:07 +00:00
Mathew Broady
a1b66f3aa6 Switch to newDisplayName() and newAvatarUrl()
Removes build warnings that these are deprecated.
See libQuotient commit f4db6988bf2fd71f74ac851557d82c6f65cc89b1
for more details.
2020-12-23 12:30:11 +11:00
Carl Schwan
218f897229 Update version 2020-12-22 22:13:20 +00:00
Nicolas Fella
ef8c21213a Fix icon in notifyrc 2020-12-22 20:53:49 +01:00
Carl Schwan
dbc82b113b Fix not eliding text in USerDetailDialog
Fix: #169
2020-12-22 15:23:12 +01:00
Mathew Broady
f65b494422 Use room avatar if message sender does not have an avatar 2020-12-22 09:36:58 +11:00
Mathew Broady
74c6cc928b Use user icon instead of room icon for notifications 2020-12-22 09:36:21 +11:00
Tobias Fella
b3899f1e69 Port away from implicitly defined onFoo properties in Connections 2020-12-21 16:37:22 +01:00
Mathew Broady
44da1ca1bf Use consistent capitalisation for postNotification's roomName 2020-12-21 11:08:33 +00:00
Mathew Broady
6a4b1983a1 Remove unused eventId() argument to postNotification() 2020-12-21 11:08:33 +00:00
Carl Schwan
6482f08eba Switch back to plain text editing
See https://bugreports.qt.io/browse/QTBUG-89630
2020-12-21 10:23:14 +01:00
Carl Schwan
f61eff2937 Use TextArea instead of simple field for room topic 2020-12-20 20:25:53 +01:00
Tobias Fella
af65884094 CMake cleanup
KQuickImageEditor is required, no need to check if it's found
2020-12-20 18:09:30 +01:00
Tobias Fella
449adf993c Allow opening links in the MessageDelegateContextMenu
Fixes #167
2020-12-20 18:05:05 +01:00
Jan Blackquill
9189a8ca30 Add symbolic icon 2020-12-19 20:53:32 -05:00
Carl Schwan
9c75deee8c Fix current page not getting updated after switching a page
This was caused by myself not updating the index after updating the
content.
2020-12-19 23:00:25 +01:00
Carl Schwan
3fcb40f9dd Fix invite page closing the wrong page
This fix #150
2020-12-19 11:47:05 +01:00
Carl Schwan
6e659c853b Add special font configuration for flatpak 2020-12-17 13:35:40 +01:00
Carl Schwan
eb95813f67 Make kquickimageeditor a required dependency 2020-12-17 13:18:43 +01:00
Carl Schwan
00e6584f84 Last icon fix 2020-12-17 10:28:54 +01:00
Carl Schwan
13bcb5c0ff fix icon 2020-12-17 10:16:50 +01:00
Carl Schwan
8312fdd08d Rename icon and set icon name explicitely
Fix #140
2020-12-17 10:03:21 +01:00
Carl Schwan
bd41dcc986 Don't recreate RoomPage each time and add a small loading indicator 2020-12-17 08:59:11 +00:00
Carl Schwan
2b84c5dd02 Improve autocompletion 2020-12-17 08:57:50 +00:00
Mathew Broady
79dab63993 Remove forgotten NeoChat.Effect imports
Fixes the "Start Chat" and "Explore Rooms" pages
2020-12-17 17:20:29 +11:00
Tobias Fella
8e2cdc8f08 Implement a device management page 2020-12-16 23:37:49 +00:00
Tobias Fella
d6e56174b5 Merge branch '1.0' 2020-12-17 00:35:34 +01:00
Carl Schwan
4e57e47def Merge branch '1.0' 2020-12-16 22:20:47 +01:00
Carl Schwan
5d80fdfcb6 Make the RightClick button works correctly 2020-12-16 16:28:42 +00:00
l10n daemon script
2056d75ee7 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"
2020-12-16 06:41:07 +01:00
l10n daemon script
83098d11b9 GIT_SILENT made messages (after extraction) 2020-12-16 02:33:11 +01:00
Tobias Fella
8435243c7a Improve android style 2020-12-15 18:13:25 +01:00
Carl Schwan
3eb53c2456 Merge branch '1.0' 2020-12-15 17:58:24 +01:00
l10n daemon script
54b07737c0 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"
2020-12-14 06:06:35 +01:00
l10n daemon script
7ae45d37a8 GIT_SILENT made messages (after extraction) 2020-12-14 02:28:56 +01:00
l10n daemon script
1ec62870f8 GIT_SILENT made messages (after extraction) 2020-12-13 02:31:16 +01:00
Tobias Fella
72fd647b7b Add 'quit' action to global menu
Implements #134
2020-12-12 23:38:13 +01:00
Carl Schwan
d646962ea1 Bump dependencies to libQuotient 0.7 (master) 2020-12-12 15:39:29 +01:00
Carl Schwan
a18ecdddb2 Fix CMake required Qt version 2020-12-12 15:06:10 +01:00
Tobias Fella
3c5ee404c3 Get rid of Neochat.Effect 2020-12-12 00:09:10 +00:00
92 changed files with 4122 additions and 2327 deletions

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ build
.DS_Store
.kdev4/
neochat.kdev4
compile_commands.json
.cache/

BIN
128-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.1)
project(Neochat)
set(KF5_MIN_VERSION "5.76.0")
set(KF5_MIN_VERSION "5.77.0")
set(QT_MIN_VERSION "5.15.0")
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
@@ -19,13 +19,16 @@ include(ECMQMLModules)
include(KDEClangFormat)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMAddAppIcon)
include(cmake/Flatpak.cmake)
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
# Fix a crash due to problems with quotient's event system. Can probably be removed once the reworked event system is in
cmake_policy(SET CMP0063 OLD)
ecm_setup_version(1.0.1
ecm_setup_version(1.1.1
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
@@ -57,11 +60,10 @@ else()
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
find_package(KF5DBusAddons ${KF5_MIN_VERSION})
set_package_properties(KF5DBusAddons PROPERTIES
TYPE REQUIRED
PURPOSE "DBus convenience functions"
)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} REQUIRED)
endif()
find_package(Quotient 0.6)
@@ -94,9 +96,12 @@ set_package_properties(KQuickImageEditor PROPERTIES
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@2/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/16x16@3/apps RENAME org.kde.neochat.svg)
install(FILES org.kde.neochat-symbolic.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/symbolic/apps)
install(FILES neochat.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
# add_definitions(-DQT_NO_KEYWORDS) Need to fix libQuotient first
add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)

View File

@@ -0,0 +1,12 @@
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the license or (at your option) any later version
that is accepted by the membership of KDE e.V. (or its successor
approved by the membership of KDE e.V.), which shall act as a
proxy as defined in Section 6 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

View File

@@ -6,7 +6,10 @@ KConfig and KI18n.
## Get it
There is no stable release for now, but a Flatpak version is available for the nightly
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions.
Along with the stable release, a Flatpak version is available for the nightly
version:
```
@@ -15,9 +18,11 @@ flatpak remote-add --if-not-exists kdeapps --from https://distribute.kde.org/kde
flatpak install kdeapps org.kde.neochat
```
A nigthly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
A nightly build is also available for Android in the [KDE nightly F-Droid repo](https://community.kde.org/Android/FDroid)
and can also directly be downloaded from the [binary factory](https://binary-factory.kde.org/view/Android/job/Neochat_android/).
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
![Timeline](https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png)
## Features

View File

@@ -1,8 +1,3 @@
if(NOT NEOCHAT_FLATPAK)
# Only include this if we build a Flatpak
return()
endif()
include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the

View File

@@ -4,19 +4,19 @@
<alias>
<family>serif</family>
<prefer>
<family>Emoji One</family>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Emoji One</family>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Emoji One</family>
<family>Noto Color Emoji</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -22,8 +22,10 @@ ToolBar {
property alias isReply: replyItem.visible
property bool isReaction: false
property var replyUser
property string replyEventID
property string replyContent
property string replyEventID: ""
property string replyContent: ""
property string editEventId
property alias isAutoCompleting: autoCompleteListView.visible
property var autoCompleteModel
@@ -31,7 +33,10 @@ ToolBar {
property int autoCompleteEndPosition
property bool hasAttachment: false
property url attachmentPath
property url attachmentPath: ""
property var attachmentMimetype: FileType.mimeTypeForUrl(attachmentPath)
property bool hasImageAttachment: hasAttachment && attachmentMimetype.valid
&& FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
position: ToolBar.Footer
@@ -192,13 +197,12 @@ ToolBar {
Image {
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
source: attachmentPath
visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
visible: hasImageAttachment
fillMode: Image.PreserveAspectFit
Layout.preferredWidth: paintedWidth
RowLayout {
anchors.right: parent.right
Button {
visible: isImage
icon.name: "document-edit"
// HACK: Use a component because an url doesn't work
@@ -233,7 +237,7 @@ ToolBar {
}
}
Rectangle {
color: rgba(255, 255, 255, 40)
color: Qt.rgba(255, 255, 255, 40)
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
@@ -248,7 +252,7 @@ ToolBar {
}
RowLayout {
visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg'))
visible: hasAttachment && !hasImageAttachment
ToolButton {
icon.name: "dialog-cancel"
onClicked: {
@@ -257,12 +261,33 @@ ToolBar {
}
}
Kirigami.Icon {
id: mimetypeIcon
implicitHeight: Kirigami.Units.fontMetrics.roundedIconSize(horizontalFileLabel.implicitHeight)
implicitWidth: implicitHeight
source: attachmentMimetype.iconName
}
Label {
id: horizontalFileLabel
Layout.alignment: Qt.AlignVCenter
text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : ""
}
}
RowLayout {
visible: editEventId.length > 0
ToolButton {
icon.name: "dialog-cancel"
onClicked: clearEditReply();
}
Label {
Layout.alignment: Qt.AlignVCenter
text: i18n("Edit Message")
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.preferredHeight: 1
@@ -281,187 +306,191 @@ ToolBar {
icon.name: "dialog-cancel"
onClicked: clearReply()
onClicked: clearEditReply()
}
TextArea {
id: inputField
property real progress: 0
property bool autoAppeared: false
ChatDocumentHandler {
id: documentHandler
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
room: currentRoom ?? null
}
ScrollView {
Layout.fillWidth: true
Layout.maximumHeight: inputField.lineHeight * 8
TextArea {
id: inputField
property real progress: 0
property bool autoAppeared: false
wrapMode: Text.Wrap
placeholderText: i18n("Write your message...")
topPadding: 0
bottomPadding: 0
leftPadding: Kirigami.Units.smallSpacing
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
// store each user we autoComplete here, this will be helpful later to generate
// the matrix.to links.
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
property var userAutocompleted: ({})
text: currentRoom != null ? currentRoom.cachedInput : ""
background: Item {}
Rectangle {
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
height: parent.height
opacity: 0.2
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
ChatDocumentHandler {
id: documentHandler
document: inputField.textDocument
cursorPosition: inputField.cursorPosition
selectionStart: inputField.selectionStart
selectionEnd: inputField.selectionEnd
room: currentRoom ?? null
}
}
Timer {
id: repeatTimer
property int lineHeight: contentHeight / lineCount
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
wrapMode: Text.Wrap
placeholderText: i18n("Write your message...")
topPadding: 0
bottomPadding: 0
leftPadding: Kirigami.Units.smallSpacing
selectByMouse: true
verticalAlignment: TextEdit.AlignVCenter
text: currentRoom != null ? currentRoom.cachedInput : ""
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
Rectangle {
width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0
height: parent.height
opacity: 0.2
}
Timer {
id: timeoutTimer
repeat: false
interval: 2000
onTriggered: {
repeatTimer.stop()
currentRoom.sendTypingNotification(false)
}
}
Timer {
id: repeatTimer
repeat: true
interval: 5000
triggeredOnStart: true
onTriggered: currentRoom.sendTypingNotification(true)
}
Keys.onReturnPressed: {
if (isAutoCompleting) {
inputField.autoComplete();
isAutoCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n")
} else {
postMessage()
text = ""
clearEditReply()
closeAll()
}
}
Keys.onEscapePressed: {
clearEditReply();
closeAll();
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
root.pasteImage();
}
}
Keys.onBacktabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomUp();
return;
}
if (isAutoCompleting) {
autoCompleteListView.decrementCurrentIndex();
}
}
Keys.onTabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomDown();
return;
}
if (!isAutoCompleting) {
return;
}
// TODO detect moved cursor
// ignore first time tab was clicked so that user can select
// first emoji/user
if (autoAppeared === false) {
autoCompleteListView.incrementCurrentIndex()
} else {
autoAppeared = false;
}
Keys.onReturnPressed: {
if (isAutoCompleting) {
inputField.autoComplete();
isAutoCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
insert(cursorPosition, "\n")
} else {
postMessage()
text = ""
clearReply()
closeAll()
}
}
Keys.onEscapePressed: {
clearReply();
closeAll();
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
root.pasteImage();
}
}
Keys.onBacktabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomUp();
return;
}
if (isAutoCompleting) {
autoCompleteListView.decrementCurrentIndex();
}
}
Keys.onTabPressed: {
if (event.modifiers & Qt.ControlModifier) {
switchRoomDown();
return;
}
if (!isAutoCompleting) {
return;
}
// TODO detect moved cursor
// ignore first time tab was clicked so that user can select
// first emoji/user
if (autoAppeared === false) {
autoCompleteListView.incrementCurrentIndex()
} else {
onTextChanged: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;
const autocompletionInfo = documentHandler.getAutocompletionInfo();
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.None) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.User) {
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
} else {
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
}
if (autoCompleteModel.length === 0) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
isAutoCompleting = true
autoAppeared = true;
autoCompleteEndPosition = cursorPosition
}
inputField.autoComplete();
}
onTextChanged: {
timeoutTimer.restart()
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;
const autocompletionInfo = documentHandler.getAutocompletionInfo();
if (autocompletionInfo.type === ChatDocumentHandler.Ignore) {
return;
}
if (autocompletionInfo.type === ChatDocumentHandler.None) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
function postMessage() {
roomManager.actionsHandler.postMessage(inputField.text.trim(), attachmentPath,
replyEventID, editEventId, inputField.userAutocompleted);
clearAttachment();
currentRoom.markAllMessagesAsRead();
clear();
text = Qt.binding(function() {
return currentRoom != null ? currentRoom.cachedInput : "";
});
}
if (autocompletionInfo.type === ChatDocumentHandler.User) {
autoCompleteModel = currentRoom.getUsers(autocompletionInfo.keyword);
} else {
autoCompleteModel = emojiModel.filterModel(autocompletionInfo.keyword);
}
function autoComplete() {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
if (!autoCompleteListView.currentItem.isEmoji) {
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
}
if (autoCompleteModel.length === 0) {
isAutoCompleting = false;
autoCompleteListView.currentIndex = 0;
return;
}
isAutoCompleting = true
autoAppeared = true;
autoCompleteEndPosition = cursorPosition
}
// store each user we autoComplete here, this will be helpful later to generate
// the matrix.to links.
// This use an hack to define: https://doc.qt.io/qt-5/qml-var.html#property-value-initialization-semantics
property var userAutocompleted: ({})
function postMessage() {
// Qt wraps lines so we need to use a small hack
// to remove the wrapped lines but not break the empty
// lines.
documentHandler.postMessage(inputField.text.trim(), attachmentPath, replyEventID,
inputField.userAutocompleted);
clearAttachment();
currentRoom.markAllMessagesAsRead();
clear();
text = Qt.binding(function() {
return currentRoom != null ? currentRoom.cachedInput : "";
});
}
function autoComplete() {
documentHandler.replaceAutoComplete(autoCompleteListView.currentItem.displayText)
if (!autoCompleteListView.currentItem.isEmoji) {
inputField.userAutocompleted[autoCompleteListView.currentItem.displayText] = autoCompleteListView.currentItem.userId;
}
}
}
@@ -519,10 +548,12 @@ ToolBar {
icon.name: "document-send"
icon.color: "transparent"
enabled: inputField.length > 0 || hasAttachment
onClicked: {
inputField.postMessage()
inputField.text = ""
root.clearReply()
root.clearEditReply()
root.closeAll()
}
@@ -551,21 +582,46 @@ ToolBar {
}
function clear() {
inputField.clear()
inputField.clear();
inputField.userAutocompleted = {};
}
function clearReply() {
isReply = false
function clearEditReply() {
isReply = false;
replyUser = null;
replyContent = "";
replyEventID = ""
clear();
root.replyContent = "";
root.replyEventID = "";
root.editEventId = "";
focus();
}
function focus() {
inputField.forceActiveFocus()
}
function edit(editContent, editFormatedContent, editEventId) {
console.log("Editing ", editContent, "html:", editFormatedContent)
// Set the input field in edit mode
inputField.text = editContent;
root.editEventId = editEventId
// clean autocompletion list
inputField.userAutocompleted = {};
// Fill autocompletion list with values extracted from message.
// We can't just iterate on every user in the list and try to
// find matching display name since some users have display name
// matching frequent words and this will marks too many words as
// mentions.
const regex = /<a href=\"https:\/\/matrix.to\/#\/(@[a-zA-Z09]*:[a-zA-Z09.]*)\">([^<]*)<\/a>/g;
let match;
while ((match = regex.exec(editFormatedContent.toString())) !== null) {
inputField.userAutocompleted[match[2]] = match[1];
}
}
function closeAll() {
replyItem.visible = false
autoCompleteListView.visible = false

View File

@@ -0,0 +1,64 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
LoginStep {
id: root
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText
property bool loading: false
title: i18n("@title", "Select a Homeserver")
action: Kirigami.Action {
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput
onTriggered: {
// TODO
console.log("register todo")
}
}
onHomeserverChanged: {
LoginHelper.testHomeserver("@user:" + homeserver)
}
Kirigami.FormLayout {
Component.onCompleted: Controller.testHomeserver(homeserver)
QQC2.ComboBox {
id: serverCombo
Kirigami.FormData.label: i18n("Homeserver:")
model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")]
}
QQC2.TextField {
id: customHomeserver
Kirigami.FormData.label: i18n("Url:")
visible: serverCombo.currentIndex === 3
onTextChanged: {
Controller.testHomeserver(text)
}
validator: RegularExpressionValidator {
regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
}
}
QQC2.Button {
id: continueButton
text: i18nc("@action:button", "Continue")
action: root.action
}
}
}

View File

@@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
QQC2.BusyIndicator {
property var showContinueButton: false
property var showBackButton: false
property string title: i18n("Loading")
anchors.centerIn: parent
}

View File

@@ -0,0 +1,68 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: login
showContinueButton: true
showBackButton: false
title: i18nc("@title", "Login")
message: i18n("Enter your Matrix ID")
Component.onCompleted: {
LoginHelper.matrixId = ""
}
Kirigami.FormLayout {
QQC2.TextField {
id: matrixIdField
Kirigami.FormData.label: i18n("Matrix ID:")
placeholderText: "@user:matrix.org"
onTextChanged: {
if(acceptableInput) {
LoginHelper.matrixId = text
}
}
Component.onCompleted: {
matrixIdField.forceActiveFocus()
}
Keys.onReturnPressed: {
login.action.trigger()
}
validator: RegularExpressionValidator {
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
}
}
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
onTriggered: {
if (LoginHelper.supportsSso && LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml");
} else if (LoginHelper.supportsPassword) {
processed("qrc:/imports/NeoChat/Component/Login/Password.qml");
} else {
processed("qrc:/imports/NeoChat/Component/Login/Sso.qml");
}
}
enabled: LoginHelper.homeserverReachable
}
}

View File

@@ -0,0 +1,35 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginMethod
title: i18n("Login Methods")
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with password")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login with single sign-on")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml")
}
}

View File

@@ -0,0 +1,35 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
LoginStep {
id: loginRegister
Layout.alignment: Qt.AlignHCenter
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Login")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml")
}
Controls.Button {
Layout.alignment: Qt.AlignHCenter
text: i18n("Register")
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml")
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
/// Step for the login/registration flow
ColumnLayout {
property string title: i18n("Welcome")
property string message: i18n("Welcome")
property bool showContinueButton: false
property bool showBackButton: false
property bool acceptable: false
property string previousUrl: ""
/// Process this module, this is called by the continue button.
/// Should call \sa processed when it finish successfully.
property Action action: null
/// Called when switching to the next step.
signal processed(url nextUrl)
signal showMessage(string message)
}

View File

@@ -0,0 +1,56 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
LoginStep {
id: password
title: i18nc("@title", "Password")
message: i18n("Enter your password")
showContinueButton: true
showBackButton: true
previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml"
action: Kirigami.Action {
text: i18nc("@action:button", "Login")
enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn
onTriggered: {
LoginHelper.login();
}
}
Connections {
target: LoginHelper
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
}
Kirigami.FormLayout {
Kirigami.PasswordField {
id: passwordField
onTextChanged: LoginHelper.password = text
enabled: !LoginHelper.isLoggingIn
Component.onCompleted: {
passwordField.forceActiveFocus()
}
Keys.onReturnPressed: {
password.action.trigger()
}
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.15
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
LoginStep {
id: root
title: i18nc("@title", "Login")
message: i18n("Login with single sign-on")
Kirigami.FormLayout {
Connections {
target: LoginHelper
onSsoUrlChanged: {
Qt.openUrlExternally(LoginHelper.ssoUrl)
}
onConnected: processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")
}
QQC2.Button {
text: i18n("Login")
onClicked: {
LoginHelper.loginWithSso()
root.showMessage(i18n("Complete the authentication steps in your browser"))
}
Component.onCompleted: forceActiveFocus()
Keys.onReturnPressed: clicked()
}
}
}

View File

@@ -0,0 +1,7 @@
module NeoChat.Component.Login
Login 1.0 Login.qml
Password 1.0 Password.qml
LoginRegister 1.0 LoginRegister.qml
Loading 1.0 Loading.qml
LoginMethod 1.0 LoginMethod.qml
LoginStep 1.0 LoginStep.qml

View File

@@ -107,6 +107,7 @@ RowLayout {
function saveFileAs() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
function downloadAndOpen()

View File

@@ -17,6 +17,8 @@ import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Image {
id: img
readonly property bool isAnimated: contentType === "image/gif"
property bool openOnFinished: false
@@ -26,8 +28,7 @@ Image {
// readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
readonly property var info: content.info
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
id: img
property bool readonly: false
source: "image://mxc/" + mediaId
@@ -36,34 +37,12 @@ Image {
fillMode: Image.PreserveAspectFit
Control {
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
ToolTip.text: display
ToolTip.visible: hoverHandler.hovered
horizontalPadding: 8
verticalPadding: 4
contentItem: RowLayout {
Label {
text: Qt.formatTime(time)
color: "white"
font.pixelSize: 12
}
Label {
text: author.displayName
color: "white"
font.pixelSize: 12
}
}
background: Rectangle {
radius: 2
color: "black"
opacity: 0.3
}
HoverHandler {
id: hoverHandler
enabled: img.readonly
}
Rectangle {
@@ -87,6 +66,8 @@ Image {
MouseArea {
id: messageMouseArea
enabled: !img.readonly
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton

View File

@@ -51,7 +51,7 @@ RowLayout {
Layout.alignment: Qt.AlignTop
visible: showAuthor && Config.showAvatarInTimeline
name: author.name
name: author.name ?? author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
@@ -95,7 +95,7 @@ RowLayout {
visible: showAuthor && !isEmote
text: author.displayName
font.bold: true
font.weight: Font.Bold
color: author.color
wrapMode: Text.Wrap
}
@@ -112,7 +112,9 @@ RowLayout {
}
Connections {
target: replyLoader.item
onClicked: replyClicked(reply.eventId)
function onClicked() {
replyClicked(reply.eventId)
}
}
}
RowLayout {
@@ -132,6 +134,13 @@ RowLayout {
onReact: currentRoom.toggleReaction(eventId, emoji)
}
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message")
icon.name: "document-edit"
onClicked: chatTextInput.edit(message, model.formattedBody, eventId)
}
QQC2.Button {
QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered

View File

@@ -22,7 +22,7 @@ Flow {
contentItem: Label {
horizontalAlignment: Text.AlignHCenter
text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "")
text: modelData.reaction + " " + modelData.count
}
padding: Kirigami.Units.smallSpacing
@@ -31,6 +31,8 @@ Flow {
radius: height / 2
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: checked ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
border.color: checked ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor
border.width: 1
}
checkable: true
@@ -45,7 +47,7 @@ Flow {
for (var i = 0; i < modelData.authors.length; i++) {
if (i === modelData.authors.length - 1 && i !== 0) {
text += i18nc("Seperate the usernames of users", " and ")
text += i18nc("Separate the usernames of users", " and ")
} else if (i !== 0) {
text += ", "
}

View File

@@ -15,8 +15,28 @@ TextEdit {
property bool isEmote: false
text: "<style>pre {white-space: pre-wrap} a{color: " + Kirigami.Theme.linkColor + ";} .user-pill{}</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
text: "<style>
table {
width:100%;
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
table th,
table td {
border: 1px solid black;
padding: 3px;
}
pre {
white-space: pre-wrap
}
a{
color: " + Kirigami.Theme.linkColor + ";
text-decoration: none;
}
.user-pill{}
</style>" + (isEmote ? "* <a href='https://matrix.to/#/" + author.id + "' style='color: " + author.color + "'>" + author.displayName + "</a> " : "") + display + (isEdited ? (" <span style=\"color: " + Kirigami.Theme.disabledTextColor + "\">" + i18n("(edited)") + "</span>") : "")
color: Kirigami.Theme.textColor
font.pointSize: isEmoji.test(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
@@ -26,15 +46,7 @@ TextEdit {
textFormat: Text.RichText
onLinkActivated: {
if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (!result || result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
applicationWindow().handleLink(link, currentRoom)
}
MouseArea {

View File

@@ -93,7 +93,7 @@ Video {
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white"
text: "Video"
text: i18n("Video")
font.pixelSize: 16
padding: 8

View File

@@ -25,7 +25,7 @@ Kirigami.OverlaySheet {
TextField {
id: roomNameField
Kirigami.FormData.label: i18n("Room Name")
onAccepted: roomTopixField.forceActiveFocus();
onAccepted: roomTopicField.forceActiveFocus();
}
TextField {
@@ -36,11 +36,11 @@ Kirigami.OverlaySheet {
Button {
id: okButton
text: i18nc("@action:button", "Ok")
onClicked: {
Controller.createRoom(Controller.activeConnection, roomNameField.text, roomTopicField.text)
roomManager.actionsHandler.createRoom(roomNameField.text, roomTopicField.text);
root.close();
// TODO investigate how to join the new room automatically
root.destroy();
}
}

View File

@@ -24,7 +24,7 @@ QQC2.Popup {
implicitHeight: Kirigami.Units.gridUnit * 20
contentItem: EmojiPicker {
onChosen: react(emoji);
onChosen: react(emoji)
emojiModel: EmojiModel {}
}
}

View File

@@ -9,6 +9,8 @@ import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.13 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Setting 1.0
@@ -109,7 +111,10 @@ Kirigami.OverlaySheet {
action: Kirigami.Action {
text: i18n("Kick this user")
icon.name: "im-kick-user"
onTriggered: room.kickMember(user.id)
onTriggered: {
room.kickMember(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
@@ -119,7 +124,20 @@ Kirigami.OverlaySheet {
text: i18n("Ban this user")
icon.name: "im-ban-user"
icon.color: Kirigami.Theme.negativeTextColor
onTriggered: room.banMember(user.id)
onTriggered: {
room.banMember(user.id)
root.close()
}
}
}
Kirigami.BasicListItem {
action: Kirigami.Action {
text: i18n("Open a private chat")
icon.name: "document-send"
onTriggered: {
Controller.activeConnection.requestDirectChat(user)
root.close()
}
}
}
Component {

View File

@@ -6,6 +6,7 @@
*/
import QtQuick 2.12
import QtQuick.Controls 2.12
import NeoChat.Page 1.0
/**
* Context menu when clicking on a room in the room list
@@ -15,23 +16,24 @@ Menu {
property var room
MenuItem {
text: i18n("Favourite")
checkable: true
checked: room.isFavourite
text: i18n("Open in new window")
onTriggered: roomManager.openWindow(room);
}
MenuSeparator {}
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
MenuItem {
text: i18n("Deprioritize")
checkable: true
checked: room.isLowPriority
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
MenuSeparator {}
MenuItem {
text: i18n("Mark as Read")

View File

@@ -57,15 +57,7 @@ Kirigami.OverlaySheet {
wrapMode: Text.WordWrap
onLinkActivated: {
if (link.startsWith("https://matrix.to/")) {
var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)")
if (!result || result.length < 3) return
if (result[1] != currentRoom.id) return
if (!result[2]) return
goToEvent(result[2])
} else {
Qt.openUrlExternally(link)
}
applicationWindow().handleLink(link, currentRoom)
}
}
}

View File

@@ -77,7 +77,7 @@ Kirigami.ScrollablePage {
actions.main: Kirigami.Action {
text: i18n("Add an account")
iconName: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/LoginPage.qml")
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
}
Kirigami.OverlaySheet {

View File

@@ -0,0 +1,103 @@
/**
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
*
* SPDX-LicenseIdentifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
Kirigami.ScrollablePage {
title: i18n("Devices")
ListView {
model: DevicesModel {
id: devices
}
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
Kirigami.BasicListItem {
anchors.top: parent.top
anchors.bottom: parent.bottom
text: model.displayName
subtitle: model.id
icon: "network-connect"
}
actions: [
Kirigami.Action {
text: i18n("Edit device name")
iconName: "document-edit"
onTriggered: {
renameSheet.index = model.index
renameSheet.name = model.displayName
renameSheet.open()
}
},
Kirigami.Action {
text: i18n("Logout device")
iconName: "edit-delete-remove"
onTriggered: {
passwordSheet.index = index
passwordSheet.open()
}
}
]
}
}
Kirigami.OverlaySheet {
id: passwordSheet
property var index
header: Kirigami.Heading {
text: i18n("Remove device")
}
Kirigami.FormLayout {
Controls.TextField {
id: passwordField
Kirigami.FormData.label: i18n("Password:")
echoMode: TextInput.Password
}
Controls.Button {
text: i18n("Confirm")
onClicked: {
devices.logout(passwordSheet.index, passwordField.text)
passwordField.text = ""
passwordSheet.close()
}
}
}
}
Kirigami.OverlaySheet {
id: renameSheet
property int index
property string name
header: Kirigami.Heading {
text: i18n("Edit device")
}
Kirigami.FormLayout {
Controls.TextField {
id: nameField
Kirigami.FormData.label: i18n("Name:")
text: renameSheet.name
}
Controls.Button {
text: i18n("Save")
onClicked: {
devices.setName(renameSheet.index, nameField.text)
renameSheet.close()
}
}
}
}
}

View File

@@ -8,7 +8,6 @@ import QtQuick 2.10
import QtQuick.Controls 2.1 as QQC2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Dialogs 1.2
import org.kde.kquickimageeditor 1.0 as KQuickImageEditor
import QtGraphicalEffects 1.12
import Qt.labs.platform 1.0 as Platform
@@ -81,21 +80,6 @@ Kirigami.Page {
onActivated: saveAsAction.trigger();
} anchors.fill: parent
FileDialog {
id: fileDialog
title: i18n("Save As")
folder: shortcuts.home
selectMultiple: false
selectExisting: false
onAccepted: {
fileDialog.close()
}
onRejected: {
fileDialog.close()
}
Component.onCompleted: visible = false
}
KQuickImageEditor.ImageDocument {
id: imageDoc
path: rootEditorView.imagePath

View File

@@ -1,50 +0,0 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12
import org.kde.kirigami 2.14 as Kirigami
import QtQuick.Layouts 1.12
Kirigami.Page {
id: root
property var room
title: i18n("Invitation Received - %1", room.displayName)
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout {
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Cancel")
onClicked: roomManager.getBack();
}
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: {
room.forget()
roomManager.getBack();
}
}
Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
room.acceptInvitation();
roomManager.enterRoom(room);
}
}
}
}
}

View File

@@ -15,15 +15,13 @@ Kirigami.ScrollablePage {
property var room
parent: applicationWindow().overlay
title: i18n("Invite a User")
actions {
main: Kirigami.Action {
icon.name: "dialog-close"
text: i18nc("@action", "Cancel")
onTriggered: applicationWindow().pageStack.pop()
onTriggered: applicationWindow().pageStack.layers.pop()
}
}
header: RowLayout {
@@ -125,7 +123,7 @@ Kirigami.ScrollablePage {
onClicked: {
room.inviteToRoom(userID);
applicationWindow().pageStack.pop();
applicationWindow().pageStack.layers.pop();
}
}
}

View File

@@ -22,8 +22,6 @@ Kirigami.ScrollablePage {
property alias keyword: identifierField.text
property string server
signal joinRoom(string room)
title: i18n("Explore Rooms")
header: Control {
@@ -51,9 +49,9 @@ Kirigami.ScrollablePage {
onClicked: {
if (!identifierField.isJoined) {
Controller.joinRoom(connection, identifierField.text);
roomManager.actionsHandler.joinRoom(identifierField.text);
// When joining the room, the room will be opened
}
roomManager.enterRoom(connection.room(identifierField.room));
applicationWindow().pageStack.layers.pop();
}
}
@@ -104,12 +102,12 @@ Kirigami.ScrollablePage {
width: publicRoomsListView.width
onClicked: {
if (!isJoined) {
Controller.joinRoom(connection, roomID)
roomManager.actionsHandler.joinRoom(connection, roomID)
justJoined = true;
} else {
roomManager.enterRoom(connection.room(roomID))
applicationWindow().pageStack.layers.pop();
}
applicationWindow().pageStack.layers.pop();
}
contentItem: RowLayout {
Kirigami.Avatar {

View File

@@ -1,97 +0,0 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Layouts 1.12
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import org.kde.kirigami 2.12 as Kirigami
Kirigami.ScrollablePage {
id: root
title: i18n("Login")
header: QQC2.Control {
padding: Kirigami.Units.smallSpacing
contentItem: Kirigami.InlineMessage {
id: inlineMessage
visible: false
showCloseButton: true
}
}
Kirigami.FormLayout {
id: formLayout
QQC2.TextField {
id: serverField
Kirigami.FormData.label: i18n("Server Address")
text: "https://matrix.org"
onAccepted: usernameField.forceActiveFocus()
}
QQC2.TextField {
id: usernameField
Kirigami.FormData.label: i18n("Username")
onAccepted: passwordField.forceActiveFocus()
}
Kirigami.PasswordField {
id: passwordField
Kirigami.FormData.label: i18n("Password")
onAccepted: accessTokenField.forceActiveFocus()
}
QQC2.TextField {
id: accessTokenField
Kirigami.FormData.label: i18n("Access Token (Optional)")
onAccepted: deviceNameField.forceActiveFocus()
}
QQC2.TextField {
id: deviceNameField
Kirigami.FormData.label: i18n("Device Name (Optional)")
onAccepted: doLogin()
}
RowLayout {
QQC2.Button {
visible: Controller.accountCount > 0
text: i18n("Cancel")
onClicked: {
pageStack.layers.clear();
}
}
QQC2.Button {
text: i18n("Login")
onClicked: doLogin()
}
}
Connections {
target: Controller
onErrorOccured: {
inlineMessage.type = Kirigami.MessageType.Error;
if (detail && detail.length !== 0) {
inlineMessage.text = i18n("%1: %2", error, detail);
} else {
inlineMessage.text = error;
}
inlineMessage.visible = true;
}
}
}
function doLogin() {
inlineMessage.text = i18n("Loading, this might take up to 10 seconds.");
inlineMessage.type = Kirigami.MessageType.Information
inlineMessage.visible = true;
if (accessTokenField.text.length > 0) {
Controller.loginWithAccessToken(serverField.text.trim(), usernameField.text.trim(), accessTokenField.text, deviceNameField.text.trim());
} else {
Controller.loginWithCredentials(serverField.text.trim(), usernameField.text.trim(), passwordField.text, deviceNameField.text.trim());
}
}
}

View File

@@ -18,13 +18,9 @@ import NeoChat.Menu 1.0
Kirigami.ScrollablePage {
id: page
property var roomListModel
property var enteredRoom
required property var activeConnection
signal enterRoom(var room)
signal leaveRoom(var room)
function goToNextRoom() {
do {
listView.incrementCurrentIndex();
@@ -65,7 +61,10 @@ Kirigami.ScrollablePage {
}
model: SortFilterRoomListModel {
id: sortFilterRoomListModel
sourceModel: roomListModel
sourceModel: RoomListModel {
id: roomListModel
connection: page.activeConnection
}
roomSortOrder: Config.mergeRoomList ? SortFilterRoomListModel.LastActivity : SortFilterRoomListModel.Categories
}
@@ -77,100 +76,50 @@ Kirigami.ScrollablePage {
}
contentItem: RowLayout {
implicitHeight: categoryName.implicitHeight
Kirigami.Icon {
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
}
Kirigami.Heading {
id: categoryName
level: 3
text: roomListModel.categoryName(section)
Layout.fillWidth: true
}
Kirigami.Icon {
source: roomListModel.categoryVisible(section) ? "go-up" : "go-down"
implicitHeight: Kirigami.Units.iconSizes.small
implicitWidth: Kirigami.Units.iconSizes.small
}
}
}
delegate: Kirigami.AbstractListItem {
delegate: Kirigami.BasicListItem {
id: roomListItem
property bool itemVisible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
visible: itemVisible
highlighted: roomManager.currentRoom && roomManager.currentRoom.displayName === displayName
visible: model.categoryVisible || sortFilterRoomListModel.filterText.length > 0 || Config.mergeRoomList
focus: true
icon: undefined
action: Kirigami.Action {
id: enterRoomAction
onTriggered: {
if (category === RoomType.Invited) {
roomManager.openInvitation(currentRoom);
} else {
var roomItem = roomManager.enterRoom(currentRoom)
roomListItem.KeyNavigation.right = roomItem
roomItem.focus = true;
}
var roomItem = roomManager.enterRoom(currentRoom)
roomListItem.KeyNavigation.right = roomItem
roomItem.focus = true;
}
}
contentItem: RowLayout {
id: roomLayout
spacing: Kirigami.Units.largeSpacing
width: listView.width
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
label: name ?? ""
subtitle: {
let txt = (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
if (txt.length) {
return txt
}
return " "
}
TapHandler {
onTapped: enterRoomAction.trigger()
onLongPressed: roomListContextMenu.createObject(roomLayout, {"room": currentRoom}).popup()
}
leading: Kirigami.Avatar {
source: avatar ? "image://mxc/" + avatar : ""
name: model.name || i18n("No Name")
implicitWidth: height
}
Kirigami.Avatar {
id: roomAvatar
property int size: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing
Layout.minimumHeight: size
Layout.maximumHeight: size
Layout.minimumWidth: size
Layout.maximumWidth: size
source: avatar ? ("image://mxc/" + avatar) : ""
name: model.name || i18n("No Name")
}
ColumnLayout {
id: roomitemcolumn
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
Layout.maximumHeight: Kirigami.Units.gridUnit * 2
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignHCenter
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: displayName ?? ""
elide: Text.ElideRight
font.bold: unreadCount >= 0 || highlightCount > 0 || notificationCount > 0
wrapMode: Text.NoWrap
}
QQC2.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ")
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.7)
}
}
trailing: RowLayout {
QQC2.Label {
text: notificationCount
visible: notificationCount > 0
@@ -184,6 +133,26 @@ Kirigami.ScrollablePage {
radius: height / 2
}
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered || Kirigami.Settings.isMobile
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
id: optionAction
icon.name: "configure"
onTriggered: {
const menu = roomListContextMenu.createObject(page, {"room": currentRoom})
configButton.visible = true
configButton.down = true
menu.closed.connect(function() {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.popup()
}
}
}
}
}
Component {

View File

@@ -23,11 +23,22 @@ import NeoChat.Menu.Timeline 1.0
Kirigami.ScrollablePage {
id: page
property var currentRoom
required property var currentRoom
title: currentRoom.displayName
signal switchRoomUp()
signal switchRoomDown()
Connections {
target: Controller.activeConnection
function onJoinedRoom(room) {
if(room.id === invitation.id) {
roomManager.enterRoom(room);
}
}
}
Connections {
target: roomManager.actionsHandler
onShowMessage: {
@@ -49,37 +60,46 @@ Kirigami.ScrollablePage {
}
}
title: currentRoom.displayName
Kirigami.PlaceholderMessage {
id: invitation
titleDelegate: Component {
property var id
visible: currentRoom && currentRoom.isInvite
anchors.centerIn: parent
text: i18n("Accept this invitation?")
RowLayout {
visible: !Kirigami.Settings.isMobile
Layout.fillWidth: true
Layout.maximumWidth: implicitWidth + 1 // The +1 is to make sure we do not trigger eliding at max width
Layout.minimumWidth: 0
spacing: Kirigami.Units.gridUnit * 0.8
Kirigami.Heading {
id: titleLabel
level: 2
Layout.alignment: Qt.AlignVCenter
text: page.title
opacity: page.isCurrentPage ? 1 : 0.4
maximumLineCount: 1
elide: Text.ElideRight
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Reject")
onClicked: {
page.currentRoom.forget()
roomManager.getBack();
}
}
QQC2.Label {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
anchors.baseline: lineCount < 2 ? titleLabel.baseline : undefined // necessary, since there is no way to do this with Layout.alignment
text: currentRoom.topic
maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9
QQC2.Button {
Layout.alignment : Qt.AlignHCenter
text: i18n("Accept")
onClicked: {
currentRoom.acceptInvitation();
invitation.id = currentRoom.id
currentRoom = null
}
}
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
QQC2.BusyIndicator {
running: true
}
}
focus: true
Keys.onTabPressed: {
@@ -110,6 +130,8 @@ Kirigami.ScrollablePage {
ListView {
id: messageListView
visible: !invitation.visible
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded
readonly property bool isLoaded: page.width * page.height > 10
@@ -146,14 +168,6 @@ Kirigami.ScrollablePage {
room: currentRoom
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
visible: messageListView.count === 0 && !currentRoom.allHistoryLoaded
QQC2.BusyIndicator {
running: true
}
}
QQC2.Popup {
anchors.centerIn: parent
@@ -212,25 +226,10 @@ Kirigami.ScrollablePage {
}
KSortFilterProxyModel {
MessageFilterModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin
function dontFilterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other";
}
function filterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state";
}
}
// populate: Transition {
@@ -323,24 +322,23 @@ Kirigami.ScrollablePage {
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
MouseArea {
acceptedButtons: (Kirigami.Settings.isMobile ? Qt.LeftButton : 0) | Qt.RightButton
anchors.fill: parent
onClicked: {
if (mouse.button == Qt.RightButton) {
openMessageContext(author, display, eventId, toolTip);
}
}
onPressAndHold: openMessageContext(author, display, eventId, toolTip);
},
TextDelegate {
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, display, eventId, toolTip)
}
TapHandler {
acceptedButtons: Qt.LeftButton
//enabled: Kirigami.Settings.isMobile
onLongPressed: openMessageContext(author, display, eventId, toolTip)
}
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.bottomMargin: Kirigami.Units.largeSpacing * 2
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
}
@@ -384,7 +382,35 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: 8
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
}
}
}
DelegateChoice {
roleValue: "sticker"
delegate: TimelineContainer {
width: messageListView.width
innerObject: MessageDelegate {
Layout.fillWidth: true
onReplyClicked: goToEvent(eventID)
onReplyToMessageClicked: replyToMessage(replyUser, replyContent, eventId);
innerObject: [
ImageDelegate {
readonly: true
Layout.maximumWidth: parent.width / 2
Layout.minimumWidth: 320
Layout.preferredHeight: info.h / info.w * width
},
ReactionDelegate {
Layout.fillWidth: true
Layout.topMargin: 0
Layout.maximumHeight: 320
Layout.bottomMargin: Kirigami.Units.largeSpacing
}
]
}
@@ -465,6 +491,7 @@ Kirigami.ScrollablePage {
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
@@ -563,6 +590,8 @@ Kirigami.ScrollablePage {
footer: ChatTextInput {
id: chatTextInput
visible: !invitation.visible && !(messageListView.count === 0 && !currentRoom.allHistoryLoaded)
Layout.fillWidth: true
}

View File

@@ -0,0 +1,21 @@
/**
* SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 2.12 as QQC2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.12
import org.kde.kirigami 2.14 as Kirigami
Kirigami.ApplicationWindow {
id: window
required property var currentRoom
minimumWidth: Kirigami.Units.gridUnit * 10
minimumHeight: Kirigami.Units.gridUnit * 15
pageStack.initialPage: RoomPage {
visible: true
currentRoom: window.currentRoom
}
}

View File

@@ -16,6 +16,15 @@ Kirigami.ScrollablePage {
title: i18n("Settings")
Kirigami.FormLayout {
QQC2.CheckBox {
Kirigami.FormData.label: i18nc("General settings:")
text: i18n("Close to system tray")
checked: Config.systemTray
onToggled: {
Config.systemTray = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.

View File

@@ -42,7 +42,10 @@ Kirigami.ScrollablePage {
text: i18n("Chat")
highlighted: true
onClicked: Controller.createDirectChat(connection, identifierField.text)
onClicked: {
connection.requestDirectChat(identifierField.text);
applicationWindow().pageStack.layers.pop();
}
}
}
}
@@ -103,23 +106,27 @@ Kirigami.ScrollablePage {
}
Button {
id: joinChatButton
Layout.alignment: Qt.AlignRight
visible: directChats != null
visible: directChats && directChats.length > 0
icon.name: "document-send"
onClicked: {
roomListForm.joinRoom(connection.room(directChats[0]))
root.close()
connection.requestDirectChat(userID);
applicationWindow().pageStack.layers.pop();
}
}
Button {
Layout.alignment: Qt.AlignRight
icon.name: "irc-join-channel"
// We wants to make sure an user can't start more than one
// chat with someone.
visible: !joinChatButton.visible
onClicked: {
Controller.createDirectChat(connection, userID)
root.close()
connection.requestDirectChat(userID);
applicationWindow().pageStack.layers.pop();
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as Controls
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component.Login 1.0
Kirigami.ScrollablePage {
id: welcomePage
property alias currentStep: module.item
title: module.item.title ?? i18n("Welcome")
header: Controls.Control {
contentItem: Kirigami.InlineMessage {
id: headerMessage
type: Kirigami.MessageType.Error
showCloseButton: true
visible: false
}
}
Component.onCompleted: LoginHelper.init()
Connections {
target: LoginHelper
onErrorOccured: {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Error;
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 16
}
Controls.Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 25
text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix")
}
Loader {
id: module
Layout.alignment: Qt.AlignHCenter
source: "qrc:/imports/NeoChat/Component/Login/Login.qml"
onSourceChanged: {
headerMessage.visible = false
headerMessage.text = ""
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Controls.Button {
text: i18nc("@action:button", "Back")
enabled: welcomePage.currentStep.previousUrl !== ""
visible: welcomePage.currentStep.showBackButton
Layout.alignment: Qt.AlignHCenter
onClicked: {
module.source = welcomePage.currentStep.previousUrl
}
}
Controls.Button {
id: continueButton
enabled: welcomePage.currentStep.acceptable
visible: welcomePage.currentStep.showContinueButton
action: welcomePage.currentStep.action
}
}
Connections {
target: currentStep
function onProcessed(nextUrl) {
module.source = nextUrl;
}
function onShowMessage(message) {
headerMessage.text = message;
headerMessage.visible = true;
headerMessage.type = Kirigami.MessageType.Information;
}
}
}
}

View File

@@ -1,8 +1,8 @@
module NeoChat.Page
LoadingPage 1.0 LoadingPage.qml
LoginPage 1.0 LoginPage.qml
RoomListPage 1.0 RoomListPage.qml
RoomPage 1.0 RoomPage.qml
RoomWindow 1.0 RoomWindow.qml
JoinRoomPage 1.0 JoinRoomPage.qml
InviteUserPage 1.0 InviteUserPage.qml
SettingsPage 1.0 SettingsPage.qml

View File

@@ -48,7 +48,7 @@ Kirigami.OverlayDrawer {
icon.name: "list-add-user"
text: i18n("Invite")
onClicked: {
applicationWindow().pageStack.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
applicationWindow().pageStack.layers.push("qrc:/imports/NeoChat/Page/InviteUserPage.qml", {"room": room})
roomDrawer.close();
}
}
@@ -84,12 +84,6 @@ Kirigami.OverlayDrawer {
}
}
Component {
id: fullScreenImage
FullScreenImage {}
}
Control {
Layout.fillWidth: true
bottomPadding: Kirigami.Units.largeSpacing
@@ -120,6 +114,7 @@ Kirigami.OverlayDrawer {
spacing: 0
Kirigami.Heading {
Layout.maximumWidth: Kirigami.Units.gridUnit * 9
Layout.fillWidth: true
level: 1
font.bold: true
@@ -144,6 +139,7 @@ Kirigami.OverlayDrawer {
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
readOnly: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
@@ -171,6 +167,16 @@ Kirigami.OverlayDrawer {
headerPositioning: ListView.OverlayHeader
boundsBehavior: Flickable.DragOverBounds
header: Pane {
padding: Kirigami.Units.smallSpacing
implicitWidth: parent.width
z: 2
contentItem: Kirigami.SearchField {
id: userListSearchField
onTextChanged: sortedMessageEventModel.filterString = text;
}
}
model: KSortFilterProxyModel {
id: sortedMessageEventModel
@@ -179,11 +185,13 @@ Kirigami.OverlayDrawer {
}
sortRole: "perm"
filterRole: "name"
}
delegate: Kirigami.AbstractListItem {
width: userListView.width
implicitHeight: Kirigami.Units.gridUnit * 2
z: 1
contentItem: RowLayout {
Kirigami.Avatar {
@@ -221,7 +229,7 @@ Kirigami.OverlayDrawer {
}
return ""
}
color: perm == UserType.Muted ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.textColor
color: Kirigami.Theme.disabledTextColor
font.pixelSize: 12
textFormat: Text.PlainText
wrapMode: Text.NoWrap
@@ -229,7 +237,7 @@ Kirigami.OverlayDrawer {
}
action: Kirigami.Action {
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open()
onTriggered: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user, "displayName": name, "avatarMediaId": avatar}).open()
}
}
}

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -24,28 +24,21 @@ Name[uk]=Neochat
Name[x-test]=xxNeochatxx
Name[zh_CN]=Neochat
DesktopEntry=org.kde.neochat
Comment=IM client for the Matrix protocol
Comment[ca]=Client de MI per al protocol Matrix
Comment[ca@valencia]=Client de MI per al protocol Matrix
Comment[de]=IM-Programm für das Matrix-Protokoll
Comment[en_GB]=IM client for the Matrix protocol
Comment[es]=Cliente de MI para el protocolo Matrix
Comment[eu]=Matrix protokolorako bat-bateko mezularitza bezeroa
Comment[fi]=Pikaviestiasiakas Matrix-yhteyskäytännölle
Comment[fr]=Client « IM » pour le protocole « Matrix »
Comment[hu]=Azonnali üzenetküldő kliens a Matrix protokollhoz
Comment[it]=Client di messaggistica istantanea per il protocollo Matrix
Comment[nl]=IM-client voor het Matrix-protocol
Comment[nn]=Lynmeldings­klient for Matrix-protokollen
Comment[pl]=Komunikator internetowy dla protokołu Matrix
Comment[pt]=Cliente de MI para o protocolo Matrix
Comment[pt_BR]=Cliente de mensageiro instantâneo para o protocolo Matrix
Comment[sk]=IM klient pre protokol Matrix
Comment[sl]=Odjemalec neposrednega sporočanja po protokolu Matrix
Comment[sv]=Direktmeddelandeklient för protokollet Matrix
Comment[uk]=Клієнт служби миттєвого обміну повідомленнями для протоколу Matrix
Comment[x-test]=xxIM client for the Matrix protocolxx
Comment[zh_CN]=为 Matrix 协议打造的 IM 客户端
Comment=A client for matrix, the decentralized communication protocol
Comment[ca]=Un client per al Matrix, el protocol de comunicacions descentralitzat
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
Comment[nl]=Een client voor matrix, het gedecentraliseerde communicatieprotocol
Comment[nn]=Klient for Matrix, den desentraliserte lynmeldings­protokollen.
Comment[pl]=Program do obsługi matriksa, rozproszonego protokołu porozumiewania się
Comment[pt_BR]=Um cliente para o Matrix, o protocolo de comunicação decentralizado
Comment[sv]=En klient för matrix, det decentraliserade kommunikationsprotokollet
Comment[uk]=Клієнт matrix, децентралізованого протоколу обміну даними
Comment[x-test]=xxA client for matrix, the decentralized communication protocolxx
[Event/message]
Name=New message

View File

@@ -0,0 +1,13 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V12H5.68102L2 15.0675V12H1V1ZM2 11H3V12.9325L5.31897 11H14V2H2V11Z"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="4" width="9" height="1"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="6" width="7" height="1"/>
<rect class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" x="3" y="8" width="5" height="1"/>
<path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M12 12.2929L10.8536 11.1465L10.1465 11.8536L13 14.7071V11.5H12V12.2929Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1007 B

View File

@@ -102,18 +102,30 @@
<developer_name xml:lang="x-test">xxThe KDE Communityxx</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<value key="KDE::matrix">#neochat:kde.org</value>
<screenshots>
<screenshot type="default">
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
</screenshot>
<screenshot>
<image>https://www.plasma-mobile.org/img/post-2020-10/post-2020-10-neochat-timeline.png</image>
<image>https://cdn.kde.org/screenshots/neochat/application-mobile.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release urgency="critical" version="1.1.1" date="2021-02-23"></release>
<release version="1.1.0" date="2021-02-22">
<description>
<p>Probably the highlight of this release is the completely new login page. It detects the server configuration based on your Matrix Id. This allows you to login to servers requiring Single Sign On (SSO) (like the Mozilla or the incoming Fedora Matrix instance).</p>
<p>Servers that require agreeing to the TOS before usage are correctly detected now and redirect to their TOS webpage, allowing the user to agree to them instead of silently failing to load the account.</p>
<p>It is now possible to open a room into a new window. This allows you to view and interact with multiple rooms at the same time.</p>
<p>We added a few commands to NeoChat (/shrug, /lenny, /join, /ignore, ...).</p>
<p>We improved the Plasma integration a bit. Now the number of unread messages is displayed in the Plasma Taskbar.</p>
</description>
<url>https://carlschwan.eu/2020/02/22/neochat-1.1/</url>
</release>
<release version="1.0.1" date="2021-01-13">
<description>
<p>This version fixes several bugs.</p>

View File

@@ -6,6 +6,7 @@
*/
import QtQuick 2.14
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.14
import org.kde.kirigami 2.12 as Kirigami
@@ -19,11 +20,17 @@ import NeoChat.Page 1.0
Kirigami.ApplicationWindow {
id: root
property var currentRoom: null
property int columnWidth: Kirigami.Units.gridUnit * 13
minimumWidth: Kirigami.Units.gridUnit * 15
minimumHeight: Kirigami.Units.gridUnit * 20
wideScreen: width > columnWidth * 5
onClosing: Controller.saveWindowGeometry(root)
pageStack.initialPage: LoadingPage {}
Connections {
target: root.quitAction
function onTriggered() {
@@ -31,15 +38,38 @@ Kirigami.ApplicationWindow {
}
}
// This timer allows to batch update the window size change to reduce
// the io load and also work around the fact that x/y/width/height are
// changed when loading the page and overwrite the saved geometry from
// the previous session.
Timer {
id: saveWindowGeometryTimer
interval: 1000
onTriggered: Controller.saveWindowGeometry(root)
}
onWidthChanged: saveWindowGeometryTimer.restart()
onHeightChanged: saveWindowGeometryTimer.restart()
onXChanged: saveWindowGeometryTimer.restart()
onYChanged: saveWindowGeometryTimer.restart()
/**
* Manage opening and close rooms
* TODO this should probably be moved to C++
*/
QtObject {
id: roomManager
property var actionsHandler: ActionsHandler {
room: roomManager.currentRoom
connection: Controller.activeConnection
onRoomJoined: {
roomManager.enterRoom(Controller.activeConnection.room(roomName))
}
}
property var currentRoom: null
property alias pageStack: root.pageStack
property bool invitationOpen: false
property var roomList: null
property Item roomItem: null
@@ -48,11 +78,20 @@ Kirigami.ApplicationWindow {
signal leaveRoom(string room);
signal openRoom(string room);
function roomByAliasOrId(aliasOrId) {
return Controller.activeConnection.room(aliasOrId)
}
function openRoomAndEvent(room, event) {
enterRoom(room)
roomItem.goToEvent(event)
}
function loadInitialRoom() {
if (Config.openRoom) {
const room = Controller.activeConnection.room(Config.openRoom);
currentRoom = room;
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
connectRoomToSignal(roomItem);
} else {
// TODO create welcome page
@@ -60,12 +99,11 @@ Kirigami.ApplicationWindow {
}
function enterRoom(room) {
let item = null;
if (currentRoom != null || invitationOpen) {
if (currentRoom != null) {
roomItem.currentRoom = room;
pageStack.currentIndex = pageStack.depth - 1;
} else {
roomItem = pageStack.push(roomPage, { 'currentRoom': room, });
roomItem = pageStack.push("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': room, });
}
currentRoom = room;
Config.openRoom = room.id;
@@ -74,17 +112,14 @@ Kirigami.ApplicationWindow {
return roomItem;
}
function openInvitation(room) {
if (currentRoom != null) {
currentRoom = null;
pageStack.removePage(pageStack.lastItem);
}
invitationOpen = true;
pageStack.push("qrc:/imports/NeoChat/Page/InvitationPage.qml", {"room": room});
function getBack() {
pageStack.replace("qrc:/imports/NeoChat/Page/RoomPage.qml", { 'currentRoom': currentRoom, });
}
function getBack() {
pageStack.replace(roomPage, { 'currentRoom': currentRoom, });
function openWindow(room) {
const secondayWindow = roomWindow.createObject(applicationWindow(), {currentRoom: room});
secondayWindow.width = root.width - roomList.width;
secondayWindow.show();
}
function connectRoomToSignal(item) {
@@ -119,7 +154,7 @@ Kirigami.ApplicationWindow {
id: contextDrawer
contentItem.implicitWidth: columnWidth
edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.LeftEdge : Qt.RightEdge
modal: !root.wideScreen
modal: !root.wideScreen || !enabled
onEnabledChanged: drawerOpen = enabled && !modal
onModalChanged: drawerOpen = !modal
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
@@ -172,7 +207,7 @@ Kirigami.ApplicationWindow {
icon.name: "settings-configure"
onTriggered: pushReplaceLayer("qrc:/imports/NeoChat/Page/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
shortcut: Controller.preferencesShortcuts[0]
shortcut: StandardKey.Preferences
},
Kirigami.Action {
text: i18n("About Neochat")
@@ -202,45 +237,60 @@ Kirigami.ApplicationWindow {
}
}
pageStack.initialPage: LoadingPage {}
Component {
id: roomListComponent
RoomListPage {
id: roomList
roomListModel: spectralRoomListModel
activeConnection: Controller.activeConnection
}
}
Connections {
target: LoginHelper
function onInitialSyncFinished() {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
Connections {
target: Controller
onInitiated: {
function onInitiated() {
if (roomManager.hasOpenRoom) {
return;
}
if (Controller.accountCount === 0) {
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {});
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {});
} else {
roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection});
roomManager.loadInitialRoom();
}
}
onConnectionAdded: {
if (Controller.accountCount === 1) {
function onBusyChanged() {
if(!Controller.busy && roomManager.roomList === null) {
roomManager.roomList = pageStack.replace(roomListComponent);
}
}
onConnectionDropped: {
function onRoomJoined(roomId) {
const room = Controller.activeConnection.room(roomId);
return roomManager.enterRoom(room);
}
function onConnectionDropped() {
if (Controller.accountCount === 0) {
pageStack.clear();
pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml");
pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml");
}
}
onGlobalErrorOccured: showPassiveNotification(error + ": " + detail)
function onGlobalErrorOccured(error, detail) {
showPassiveNotification(error + ": " + detail)
}
onShowWindow: root.showWindow()
function onShowWindow() {
root.showWindow()
}
function onOpenRoom(room) {
roomManager.enterRoom(room)
@@ -252,6 +302,13 @@ Kirigami.ApplicationWindow {
}
}
Connections {
target: Controller.activeConnection
onDirectChatAvailable: {
roomManager.enterRoom(Controller.activeConnection.room(directChat.id));
}
}
Kirigami.OverlaySheet {
id: consentSheet
@@ -274,21 +331,47 @@ Kirigami.ApplicationWindow {
}
}
RoomListModel {
id: spectralRoomListModel
connection: Controller.activeConnection
}
Component {
id: roomPage
RoomPage {}
}
Component {
id: createRoomDialog
CreateRoomDialog {}
}
Component {
id: roomWindow
RoomWindow {}
}
function handleLink(link, currentRoom) {
if (link.startsWith("https://matrix.to/")) {
var content = link.replace("https://matrix.to/#/", "").replace(/\?.*/, "")
if(content.match("^[#!]")) {
if(content.includes("/")) {
var result = content.match("([!#].*:.*)/(\\$.*)")
if(!result) {
return
}
if(result[1] == currentRoom.id) {
roomManager.roomItem.goToEvent(result[2])
} else {
roomManager.openRoomAndEvent(roomManager.roomByAliasOrId(result[1]), result[2])
}
} else {
roomManager.enterRoom(roomManager.roomByAliasOrId(content))
}
} else if(content.match("^@")) {
let dialog = userDialog.createObject(root.overlay, {room: currentRoom, user: currentRoom.user(content)})
dialog.open()
console.log(dialog.user)
}
} else {
Qt.openUrlExternally(link)
}
}
Component {
id: userDialog
UserDetailDialog {
}
}
}

4
qtquickcontrols2.conf Normal file
View File

@@ -0,0 +1,4 @@
[Material]
Primary=Blue
Accent=Blue
Theme=System

15
res.qrc
View File

@@ -2,17 +2,18 @@
<qresource prefix="/">
<file>qml/main.qml</file>
<file>imports/NeoChat/Page/qmldir</file>
<file>imports/NeoChat/Page/LoginPage.qml</file>
<file>imports/NeoChat/Page/LoadingPage.qml</file>
<file>imports/NeoChat/Page/RoomListPage.qml</file>
<file>imports/NeoChat/Page/RoomPage.qml</file>
<file>imports/NeoChat/Page/RoomWindow.qml</file>
<file>imports/NeoChat/Page/AccountsPage.qml</file>
<file>imports/NeoChat/Page/JoinRoomPage.qml</file>
<file>imports/NeoChat/Page/InviteUserPage.qml</file>
<file>imports/NeoChat/Page/SettingsPage.qml</file>
<file>imports/NeoChat/Page/InvitationPage.qml</file>
<file>imports/NeoChat/Page/StartChatPage.qml</file>
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
<file>imports/NeoChat/Page/DevicesPage.qml</file>
<file>imports/NeoChat/Page/WelcomePage.qml</file>
<file>imports/NeoChat/Component/qmldir</file>
<file>imports/NeoChat/Component/ChatTextInput.qml</file>
<file>imports/NeoChat/Component/AutoMouseArea.qml</file>
@@ -31,6 +32,15 @@
<file>imports/NeoChat/Component/Timeline/AudioDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file>
<file>imports/NeoChat/Component/Login/qmldir</file>
<file>imports/NeoChat/Component/Login/LoginStep.qml</file>
<file>imports/NeoChat/Component/Login/Login.qml</file>
<file>imports/NeoChat/Component/Login/Password.qml</file>
<file>imports/NeoChat/Component/Login/LoginRegister.qml</file>
<file>imports/NeoChat/Component/Login/Loading.qml</file>
<file>imports/NeoChat/Component/Login/Homeserver.qml</file>
<file>imports/NeoChat/Component/Login/LoginMethod.qml</file>
<file>imports/NeoChat/Component/Login/Sso.qml</file>
<file>imports/NeoChat/Setting/Setting.qml</file>
<file>imports/NeoChat/Setting/qmldir</file>
<file>imports/NeoChat/Setting/Palette.qml</file>
@@ -49,5 +59,6 @@
<file>imports/NeoChat/Menu/Timeline/FileDelegateContextMenu.qml</file>
<file>imports/NeoChat/Menu/Timeline/MessageSourceSheet.qml</file>
<file>imports/NeoChat/Menu/RoomListContextMenu.qml</file>
<file>qtquickcontrols2.conf</file>
</qresource>
</RCC>

View File

@@ -1,10 +1,12 @@
add_executable(neochat
accountlistmodel.cpp
controller.cpp
actionshandler.cpp
emojimodel.cpp
clipboard.cpp
matriximageprovider.cpp
messageeventmodel.cpp
messagefiltermodel.cpp
roomlistmodel.cpp
neochatroom.cpp
neochatuser.cpp
@@ -16,25 +18,29 @@ add_executable(neochat
notificationsmanager.cpp
sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
devicesmodel.cpp
filetypesingleton.cpp
login.cpp
stickerevent.cpp
../res.qrc
)
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID)
target_sources(neochat PRIVATE trayicon.cpp)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PRIVATE Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark)
target_link_libraries(neochat PRIVATE Qt::Quick Qt::Qml Qt::Gui Qt::Network Qt::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PRIVATE NEOCHAT_FLATPAK)
endif()
if (KQuickImageEditor_FOUND)
target_compile_definitions(neochat PRIVATE HAS_KQUICKIMAGEEDITOR)
endif()
if(ANDROID)
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
@@ -68,9 +74,17 @@ if(ANDROID)
"search"
"mail-replied-symbolic"
"edit-copy"
"gtk-quit"
"compass"
"network-connect"
)
else()
target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::DBusAddons ${QTKEYCHAIN_LIBRARIES})
target_link_libraries(neochat PRIVATE Qt5::Widgets ${QTKEYCHAIN_LIBRARIES})
endif()
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PRIVATE KF5::DBusAddons)
target_compile_definitions(neochat PRIVATE -DHAVE_KDBUSADDONS)
endif()
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef ACCOUNTLISTMODEL_H
#define ACCOUNTLISTMODEL_H
#pragma once
#include "controller.h"
@@ -30,5 +30,3 @@ public:
private:
QVector<Connection *> m_connections;
};
#endif // ACCOUNTLISTMODEL_H

332
src/actionshandler.cpp Normal file
View File

@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPl-3.0-or-later
#include "actionshandler.h"
#include "csapi/joining.h"
#include <KLocalizedString>
#include <QStringBuilder>
#include <QDebug>
ActionsHandler::ActionsHandler(QObject *parent)
: QObject(parent)
{
}
ActionsHandler::~ActionsHandler()
{};
NeoChatRoom *ActionsHandler::room() const
{
return m_room;
}
void ActionsHandler::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
Connection *ActionsHandler::connection() const
{
return m_connection;
}
void ActionsHandler::setConnection(Connection *connection)
{
if (m_connection == connection) {
return;
}
if (m_connection != nullptr) {
disconnect(m_connection, &Connection::directChatAvailable, nullptr, nullptr);
}
m_connection = connection;
if (m_connection != nullptr) {
connect(m_connection, &Connection::directChatAvailable,
this, [this](Quotient::Room *room) {
room->setDisplayed(true);
Q_EMIT roomJoined(room->id());
});
}
Q_EMIT connectionChanged();
}
QVariantList ActionsHandler::commands() const
{
QVariantList commands;
// Messages commands
commands.append({
QStringLiteral("prefix"), QStringLiteral("/shrug "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Prepends ¯\\_(ツ)_/¯ to a plain-text message")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/lenny "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/plain "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends a message as plain text, without interpreting it as markdown")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/html "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends a message as html, without interpreting it as markdown")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/rainbow "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends the given message coloured as a rainbow")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/rainbowme "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Sends the given emote coloured as a rainbow")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/me "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<message>"),
QStringLiteral("help"), i18n("Displays action")
});
// Actions commands
commands.append({
QStringLiteral("prefix"), QStringLiteral("/join "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<room-address>"),
QStringLiteral("help"), i18n("Joins room with given address")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/part "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "[<room-address>]"),
QStringLiteral("help"), i18n("Leave room")
});
commands.append({
QStringLiteral("prefix"), QStringLiteral("/invite "),
QStringLiteral("parameter"), i18nc("@label Parameter of a command", "<user-id>"),
QStringLiteral("help"), i18n("Invites user with given id to current room")
});
// TODO more see elements /help action
return commands;
}
void ActionsHandler::joinRoom(const QString &alias)
{
if (!alias.contains(":")) {
Q_EMIT showMessage(MessageType::Error, i18n("The room id you are trying to join is not valid"));
return;
}
const auto knownServer = alias.mid(alias.indexOf(":") + 1);
auto joinRoomJob = m_connection->joinRoom(alias, QStringList{knownServer});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::failure, [=] {
Q_EMIT showMessage(MessageType::Error, i18n("Server error when joining the room \"%1\": %2",
joinRoomJob->errorString()));
});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::success, [this, joinRoomJob] {
Q_EMIT roomJoined(joinRoomJob->roomId());
});
}
void ActionsHandler::createRoom(const QString &name, const QString &topic)
{
auto createRoomJob = m_connection->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT showMessage(MessageType::Error, i18n("Room creation failed: \"%1\"", createRoomJob->errorString()));
});
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::success, [=] {
Q_EMIT roomJoined(createRoomJob->roomId());
});
}
void ActionsHandler::postMessage(const QString &text,
const QString &attachementPath, const QString &replyEventId, const QString &editEventId,
const QVariantMap &usernames)
{
QString rawText = text;
QString cleanedText = text;
for (auto it = usernames.constBegin(); it != usernames.constEnd(); it++) {
cleanedText = cleanedText.replace(it.key(),
"[" + it.key() + "](https://matrix.to/#/" + it.value().toString() + ")");
}
if (attachementPath.length() > 0) {
m_room->uploadFile(attachementPath, cleanedText);
}
if (cleanedText.length() == 0) {
return;
}
auto messageEventType = RoomMessageEvent::MsgType::Text;
// Message commands
static const QString shrugPrefix = QStringLiteral("/shrug ");
static const QString lennyPrefix = QStringLiteral("/lenny ");
static const QString plainPrefix = QStringLiteral("/plain "); // TODO
static const QString htmlPrefix = QStringLiteral("/html "); // TODO
static const QString rainbowPrefix = QStringLiteral("/rainbow ");
static const QString rainbowmePrefix = QStringLiteral("/rainbowme ");
static const QString mePrefix = QStringLiteral("/me ");
static const QString noticePrefix = QStringLiteral("/notice ");
// Actions commands
static const QString ddgPrefix = QStringLiteral("/ddg "); // TODO
static const QString nickPrefix = QStringLiteral("/nick "); // TODO
static const QString meroomnickPrefix = QStringLiteral("/myroomnick "); // TODO
static const QString roomavatarPrefix = QStringLiteral("/roomavatar "); // TODO
static const QString myroomavatarPrefix = QStringLiteral("/myroomavatar "); // TODO
static const QString myavatarPrefix = QStringLiteral("/myavatar "); // TODO
static const QString invitePrefix = QStringLiteral("/invite ");
static const QString joinPrefix = QStringLiteral("/join ");
static const QString partPrefix = QStringLiteral("/part");
static const QString ignorePrefix = QStringLiteral("/ignore ");
static const QString unignorePrefix = QStringLiteral("/unignore ");
static const QString queryPrefix = QStringLiteral("/query "); // TODO
static const QString msgPrefix = QStringLiteral("/msg "); // TODO
// Admin commands
static QStringList rainbowColors{"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500",
"#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b",
"#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff",
"#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff",
"#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
if (cleanedText.indexOf(shrugPrefix) == 0) {
cleanedText = QStringLiteral("¯\\\\_(ツ)\\_/¯") % cleanedText.remove(0, shrugPrefix.length());
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
return;
}
if (cleanedText.indexOf(lennyPrefix) == 0) {
cleanedText = QStringLiteral("( ͡° ͜ʖ ͡°)") % cleanedText.remove(0, lennyPrefix.length());
m_room->postHtmlMessage(cleanedText, cleanedText, messageEventType, replyEventId, editEventId);
return;
}
if (cleanedText.indexOf(rainbowPrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
QString rainbowText;
for (int i = 0; i < cleanedText.length(); i++) {
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
}
m_room->postHtmlMessage(cleanedText, rainbowText, RoomMessageEvent::MsgType::Notice, replyEventId, editEventId);
return;
}
if (cleanedText.indexOf(rainbowmePrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowmePrefix.length());
QString rainbowText;
for (int i = 0; i < cleanedText.length(); i++) {
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
}
m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId, editEventId);
return;
}
if (rawText.indexOf(joinPrefix) == 0) {
rawText = rawText.remove(0, joinPrefix.length());
const QStringList splittedText = rawText.split(" ");
if (text.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
if (splittedText.count() > 1) {
joinRoom(splittedText[0] + ":" + splittedText[1]);
return;
}
else {
joinRoom(splittedText[0] + ":matrix.org");
}
return;
}
if (rawText.indexOf(invitePrefix) == 0) {
rawText = rawText.remove(0, invitePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
m_room->inviteToRoom(splittedText[0]);
return;
}
if (rawText.indexOf(partPrefix) == 0) {
rawText = rawText.remove(0, partPrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0 || splittedText[0].isEmpty()) {
// leave current room
m_connection->leaveRoom(m_room);
return;
}
m_connection->leaveRoom(m_connection->room(splittedText[0]));
return;
}
if (rawText.indexOf(ignorePrefix) == 0) {
rawText = rawText.remove(0, ignorePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
if (m_connection->users().contains(splittedText[0])) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
const auto *user = m_connection->users()[splittedText[0]];
m_connection->addToIgnoredUsers(user);
return;
}
if (rawText.indexOf(unignorePrefix) == 0) {
rawText = rawText.remove(0, unignorePrefix.length());
const QStringList splittedText = rawText.split(" ");
if (splittedText.count() == 0) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
if (m_connection->users().contains(splittedText[0])) {
Q_EMIT showMessage(MessageType::Error, i18n("Invalid command"));
return;
}
const auto *user = m_connection->users()[splittedText[0]];
m_connection->removeFromIgnoredUsers(user);
return;
}
if (cleanedText.indexOf(mePrefix) == 0) {
cleanedText = cleanedText.remove(0, mePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Emote;
} else if (cleanedText.indexOf(noticePrefix) == 0) {
cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice;
}
m_room->postMessage(rawText, cleanedText, messageEventType, replyEventId, editEventId);
}

80
src/actionshandler.h Normal file
View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
//
// SPDX-License-Identifier: GPl-3.0-or-later
#pragma once
#include <QObject>
#include "connection.h"
#include "neochatroom.h"
using namespace Quotient;
/// \brief Handles user interactions with NeoChat (joining room, creating room,
/// sending message). Account management is handled by Controller.
class ActionsHandler : public QObject
{
Q_OBJECT
/// \brief List of command definition. Useful for building an autocompletion
/// engine or an help dialog.
Q_PROPERTY(QVariantList commands READ commands CONSTANT)
/// \brief The connection that will handle sending the message.
Q_PROPERTY(Connection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
/// \brief The connection that will handle sending the message.
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
public:
enum MessageType {
Info,
Error,
};
Q_ENUM(MessageType);
explicit ActionsHandler(QObject *parent = nullptr);
~ActionsHandler();
QVariantList commands() const;
[[nodiscard]] Connection *connection() const;
void setConnection(Connection *connection);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
/// \brief Show error or information message.
///
/// These messages will be displayed in the room view header.
void showMessage(MessageType messageType, QString message);
/// \brief Emitted when an action made the user join a room.
///
/// Either when a new room was created, a direct chat was started
/// or a group chat was joined. The UI will react to this signal
/// and switch to the newly joined room.
void roomJoined(QString roomName);
void roomChanged();
void connectionChanged();
public Q_SLOTS:
/// \brief Create new room for a group chat.
void createRoom(const QString &name, const QString &topic);
/// \brief Join a room.
void joinRoom(const QString &alias);
/// \brief Post a message.
///
/// This also interprets commands if any.
void postMessage(const QString &text, const QString &attachementPath,
const QString &replyEventId, const QString &editEventId, const QVariantMap &usernames);
private:
Connection *m_connection = nullptr;
NeoChatRoom *m_room = nullptr;
};

View File

@@ -126,7 +126,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
if (cursor.block().text() == m_lastState) {
// ignore change, it was caused by autocompletion
return QVariantMap {
return QVariantMap{
{"type", AutoCompletionType::Ignore},
};
}
@@ -141,7 +141,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
QString autoCompletePrefix = textBeforeCursor.section(" ", -1);
if (autoCompletePrefix.isEmpty()) {
return QVariantMap {
return QVariantMap{
{"type", AutoCompletionType::None},
};
}
@@ -151,77 +151,22 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
if (autoCompletePrefix.startsWith("@")) {
autoCompletePrefix.remove(0, 1);
return QVariantMap {
return QVariantMap{
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::User},
};
}
return QVariantMap {
return QVariantMap{
{"keyword", autoCompletePrefix},
{"type", AutoCompletionType::Emoji},
};
}
return QVariantMap {
return QVariantMap{
{"type", AutoCompletionType::None},
};
}
void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath,
const QString &replyEventId, const QVariantMap usernames) const
{
if (!m_room || !m_document) {
return;
}
QString cleanedText = text;
cleanedText = cleanedText.trimmed();
for (const auto username : usernames.keys()) {
const auto replacement = usernames.value(username);
cleanedText = cleanedText.replace(username,
"[" + username + "](https://matrix.to/#/" + replacement.toString() + ")");
}
if (attachementPath.length() > 0) {
m_room->uploadFile(attachementPath, cleanedText);
}
if (cleanedText.length() == 0) {
return;
}
auto messageEventType = RoomMessageEvent::MsgType::Text;
const QString rainbowPrefix = QStringLiteral("/rainbow ");
const QString mePrefix = QStringLiteral("/me ");
const QString noticePrefix = QStringLiteral("/notice ");
if (cleanedText.indexOf(rainbowPrefix) == 0) {
cleanedText = cleanedText.remove(0, rainbowPrefix.length());
QString rainbowText;
QStringList rainbowColors {"#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff",
"#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"};
for (int i = 0; i < cleanedText.length(); i++) {
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
}
m_room->postHtmlMessage(text, rainbowText, messageEventType, replyEventId);
return;
}
if (cleanedText.indexOf(mePrefix) == 0) {
cleanedText = cleanedText.remove(0, mePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Emote;
} else if (cleanedText.indexOf(noticePrefix) == 0) {
cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice;
}
m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId);
}
void ChatDocumentHandler::replaceAutoComplete(const QString &word)
{
QTextCursor cursor = textCursor();

View File

@@ -14,6 +14,7 @@
class QTextDocument;
class QQuickTextDocument;
class NeoChatRoom;
class Controller;
class ChatDocumentHandler : public QObject
{
@@ -51,10 +52,8 @@ public:
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QVariantMap usernames) const;
/// This function will look at the current QTextCursor and determine if there
/// is the posibility to autocomplete it.
/// is the possibility to autocomplete it.
Q_INVOKABLE QVariantMap getAutocompletionInfo();
Q_INVOKABLE void replaceAutoComplete(const QString &word);
@@ -64,6 +63,7 @@ Q_SIGNALS:
void selectionStartChanged();
void selectionEndChanged();
void roomChanged();
void joinRoom(QString roomName);
private:
[[nodiscard]] QTextCursor textCursor() const;

View File

@@ -57,7 +57,7 @@ bool Clipboard::saveImage(const QUrl &localPath) const
void Clipboard::saveText(QString message)
{
QRegularExpression re("<[^>]*>");
auto *mineData = new QMimeData; // ownership is transfered to clipboard
auto *mineData = new QMimeData; // ownership is transferred to clipboard
mineData->setHtml(message);
mineData->setText(message.replace(re, ""));
m_clipboard->setMimeData(mineData);

View File

@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <QObject>

View File

@@ -8,43 +8,42 @@
#ifndef Q_OS_ANDROID
#include <qt5keychain/keychain.h>
#else
#endif
#include <KConfig>
#include <KConfigGroup>
#endif
#include <KWindowConfig>
#include <KLocalizedString>
#include <QClipboard>
#include <QDebug>
#include <QQuickWindow>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QStringBuilder>
#include <QSysInfo>
#include <QTimer>
#include <QtGui/QCloseEvent>
#include <QtGui/QDesktopServices>
#include <QtGui/QMovie>
#include <QtGui/QPixmap>
#include <QtNetwork/QAuthenticator>
#include <QtNetwork/QNetworkReply>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QMovie>
#include <QPixmap>
#include <QAuthenticator>
#include <QNetworkReply>
#include <QStringBuilder>
#include <utility>
#include "csapi/account-data.h"
#include "csapi/content-repo.h"
#include "csapi/joining.h"
#include "csapi/logout.h"
#include "csapi/profile.h"
#include "csapi/registration.h"
#include "csapi/wellknown.h"
#include "events/eventcontent.h"
#include "events/roommessageevent.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "neochatconfig.h"
#include "settings.h"
#include "utils.h"
#include <KStandardShortcut>
@@ -56,16 +55,26 @@
Controller::Controller(QObject *parent)
: QObject(parent)
{
QApplication::setQuitOnLastWindowClosed(false);
Connection::setRoomType<NeoChatRoom>();
Connection::setUserType<NeoChatUser>();
#ifndef Q_OS_ANDROID
TrayIcon *trayIcon = new TrayIcon(this);
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
trayIcon->setIconSource("org.kde.neochat");
trayIcon->setIsOnline(true);
if(NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
QApplication::setQuitOnLastWindowClosed(false);
}
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [=](){
if(NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
trayIcon->hide();
disconnect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
}
QApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
});
#endif
QTimer::singleShot(0, this, [=] {
@@ -93,51 +102,6 @@ inline QString accessTokenFileName(const AccountSettings &account)
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName;
}
void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName)
{
if (user.isEmpty() || pass.isEmpty()) {
return;
}
if (deviceName.isEmpty()) {
deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture();
}
auto conn = new Connection(this);
const QUrl serverUrl = QUrl::fromUserInput(serverAddr);
// we are using a fake mixd since resolveServer just set the homeserver url :sigh:
conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443)));
connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() {
conn->loginWithPassword(user, pass, deviceName, "");
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
AccountSettings account(conn->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(conn->homeserver());
account.setDeviceId(conn->deviceId());
account.setDeviceName(deviceName);
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
addConnection(conn);
setActiveConnection(conn);
});
connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
connect(conn, &Connection::loginError, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed"), std::move(error));
});
});
connect(conn, &Connection::resolveError, this, [=](QString error) {
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
});
}
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
{
if (user.isEmpty() || token.isEmpty()) {
@@ -260,7 +224,7 @@ void Controller::invokeLogin()
const auto accounts = SettingsGroup("Accounts").childGroups();
QString id = NeoChatConfig::self()->activeConnection();
for (const auto &accountId : accounts) {
AccountSettings account {accountId};
AccountSettings account{accountId};
if (id.isEmpty()) {
// handle case where the account config is empty
id = accountId;
@@ -285,6 +249,7 @@ void Controller::invokeLogin()
Q_EMIT errorOccured(i18n("Login Failed"), error);
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [=](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured("Network Error", error);
@@ -292,14 +257,14 @@ void Controller::invokeLogin()
connection->connectWithToken(account.userId(), accessToken, account.deviceId());
}
}
if (m_connections.isEmpty()) {
if (accounts.isEmpty()) {
Q_EMIT initiated();
}
}
QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
{
QFile accountTokenFile {accessTokenFileName(account)};
QFile accountTokenFile{accessTokenFileName(account)};
if (accountTokenFile.open(QFile::ReadOnly)) {
if (accountTokenFile.size() < 1024) {
return accountTokenFile.readAll();
@@ -337,7 +302,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken);
if (saved) {
QFile accountTokenFile {accessTokenFileName(account)};
QFile accountTokenFile{accessTokenFileName(account)};
removed = accountTokenFile.remove();
}
if (!(saved && removed)) {
@@ -360,7 +325,7 @@ QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &accoun
bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken)
{
// (Re-)Make a dedicated file for access_token.
QFile accountTokenFile {accessTokenFileName(account)};
QFile accountTokenFile{accessTokenFileName(account)};
accountTokenFile.remove(); // Just in case
auto fileDir = QFileInfo(accountTokenFile).dir();
@@ -399,34 +364,6 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true;
}
void Controller::joinRoom(Connection *c, const QString &alias)
{
if (!alias.contains(":")) {
return;
}
auto knownServer = alias.mid(alias.indexOf(":") + 1);
auto joinRoomJob = c->joinRoom(alias, QStringList {knownServer});
Quotient::JoinRoomJob::connect(joinRoomJob, &JoinRoomJob::failure, [=] {
Q_EMIT errorOccured("Join Room Failed", joinRoomJob->errorString());
});
}
void Controller::createRoom(Connection *c, const QString &name, const QString &topic)
{
auto createRoomJob = c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT errorOccured("Create Room Failed", createRoomJob->errorString());
});
}
void Controller::createDirectChat(Connection *c, const QString &userID)
{
auto createRoomJob = c->createDirectChat(userID);
Quotient::CreateRoomJob::connect(createRoomJob, &CreateRoomJob::failure, [=] {
Q_EMIT errorOccured("Create Direct Chat Failed", createRoomJob->errorString());
});
}
void Controller::playAudio(const QUrl &localFile)
{
@@ -565,7 +502,21 @@ void Controller::setActiveConnection(Connection *connection)
Q_EMIT activeConnectionChanged();
}
QList<QKeySequence> Controller::preferencesShortcuts() const
void Controller::saveWindowGeometry(QQuickWindow *window)
{
return KStandardShortcut::preferences();
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup windowGroup(&dataResource, "Window");
KWindowConfig::saveWindowPosition(window, windowGroup);
KWindowConfig::saveWindowSize(window, windowGroup);
dataResource.sync();
}
NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth)
: Quotient::BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), QStringLiteral("/_matrix/client/r0/devices/%1").arg(deviceId))
{
QJsonObject _data;
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
setRequestData(std::move(_data));
}

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef CONTROLLER_H
#define CONTROLLER_H
#pragma once
#include <QApplication>
#include <QMediaPlayer>
@@ -21,6 +21,7 @@ class QKeySequences;
#include "user.h"
class NeoChatRoom;
class QQuickWindow;
using namespace Quotient;
@@ -33,9 +34,6 @@ class Controller : public QObject
Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
Q_PROPERTY(KAboutData aboutData READ aboutData WRITE setAboutData NOTIFY aboutDataChanged)
/// Get the list of shortcuts activating the preferences page
Q_PROPERTY(QList<QKeySequence> preferencesShortcuts READ preferencesShortcuts CONSTANT)
public:
static Controller &instance();
@@ -47,15 +45,12 @@ public:
void addConnection(Connection *c);
void dropConnection(Connection *c);
Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString);
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString &currentPassword, const QString &newPassword);
[[nodiscard]] int accountCount() const;
[[nodiscard]] QList<QKeySequence> preferencesShortcuts() const;
[[nodiscard]] static bool quitOnLastWindowClosed();
void setQuitOnLastWindowClosed(bool value);
@@ -65,6 +60,9 @@ public:
void setAboutData(const KAboutData &aboutData);
[[nodiscard]] KAboutData aboutData() const;
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
enum PasswordStatus {
Success,
Wrong,
@@ -83,8 +81,6 @@ private:
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
void loadSettings();
void saveSettings() const;
@@ -95,10 +91,10 @@ private Q_SLOTS:
Q_SIGNALS:
void busyChanged();
/// Error occured because of user inputs
/// Error occurred because of user inputs
void errorOccured(QString error, QString detail);
/// Error occured because of server or bug in NeoChat
/// Error occurred because of server or bug in NeoChat
void globalErrorOccured(QString error, QString detail);
void syncDone();
void connectionAdded(Quotient::Connection *_t1);
@@ -114,15 +110,14 @@ Q_SIGNALS:
void showWindow();
void openRoom(NeoChatRoom *room);
void userConsentRequired(QUrl url);
void testConnectionResult(const QString &connection, bool usable);
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);
void joinRoom(Quotient::Connection *c, const QString &alias);
void createRoom(Quotient::Connection *c, const QString &name, const QString &topic);
void createDirectChat(Quotient::Connection *c, const QString &userID);
static void playAudio(const QUrl &localFile);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void saveWindowGeometry(QQuickWindow *);
};
// TODO libQuotient 0.7: Drop
@@ -132,4 +127,8 @@ public:
explicit NeochatChangePasswordJob(const QString &newPassword, bool logoutDevices, const Omittable<QJsonObject> &auth = none);
};
#endif // CONTROLLER_H
class NeochatDeleteDeviceJob : public BaseJob
{
public:
explicit NeochatDeleteDeviceJob(const QString &deviceId, const Omittable<QJsonObject> &auth = none);
};

88
src/devicesmodel.cpp Normal file
View File

@@ -0,0 +1,88 @@
/**
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
*
* SPDX-LicenseIdentifier: GPL-2.0-or-later
*/
#include "devicesmodel.h"
#include <csapi/device_management.h>
#include "controller.h"
DevicesModel::DevicesModel(QObject *parent)
: QAbstractListModel(parent)
{
GetDevicesJob *job = Controller::instance().activeConnection()->callApi<GetDevicesJob>();
connect(job, &BaseJob::success, this, [this, job]() {
beginResetModel();
m_devices = job->devices();
endResetModel();
});
}
QVariant DevicesModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= rowCount(QModelIndex()))
return QVariant();
switch (role) {
case Id:
return m_devices[index.row()].deviceId;
case DisplayName:
return m_devices[index.row()].displayName;
case LastIp:
return m_devices[index.row()].lastSeenIp;
case LastTimestamp:
if (m_devices[index.row()].lastSeenTs)
return *m_devices[index.row()].lastSeenTs;
}
return QVariant();
}
int DevicesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_devices.size();
}
QHash<int, QByteArray> DevicesModel::roleNames() const
{
return {{Id, "id"}, {DisplayName, "displayName"}, {LastIp, "lastIp"}, {LastTimestamp, "lastTimestamp"}};
}
void DevicesModel::logout(int index, const QString &password)
{
auto job = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId);
connect(job, &BaseJob::result, this, [this, job, password, index] {
if (job->error() != 0) {
QJsonObject replyData = job->jsonData();
QJsonObject authData;
authData["session"] = replyData["session"];
authData["password"] = password;
authData["type"] = "m.login.password";
QJsonObject identifier = {{"type", "m.id.user"}, {"user", Controller::instance().activeConnection()->user()->id()}};
authData["identifier"] = identifier;
auto *innerJob = Controller::instance().activeConnection()->callApi<NeochatDeleteDeviceJob>(m_devices[index].deviceId, authData);
connect(innerJob, &BaseJob::success, this, [this, index]() {
Q_EMIT beginRemoveRows(QModelIndex(), index, index);
m_devices.remove(index);
Q_EMIT endRemoveRows();
});
}
});
}
void DevicesModel::setName(int index, const QString &name)
{
auto job = Controller::instance().activeConnection()->callApi<UpdateDeviceJob>(m_devices[index].deviceId, name);
QString oldName = m_devices[index].displayName;
Q_EMIT beginResetModel();
m_devices[index].displayName = name;
Q_EMIT endResetModel();
connect(job, &BaseJob::failure, this, [=]() {
Q_EMIT beginResetModel();
m_devices[index].displayName = oldName;
Q_EMIT endResetModel();
});
}

39
src/devicesmodel.h Normal file
View File

@@ -0,0 +1,39 @@
/**
* SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
*
* SPDX-LicenseIdentifier: GPL-2.0-or-later
*/
#pragma once
#include <QAbstractListModel>
#include <csapi/definitions/client_device.h>
using namespace Quotient;
class DevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
Id,
DisplayName,
LastIp,
LastTimestamp,
};
Q_ENUM(Roles);
DevicesModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
Q_INVOKABLE void logout(int index, const QString &password);
Q_INVOKABLE void setName(int index, const QString &name);
private:
QVector<Quotient::Device> m_devices;
};

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef EMOJIMODEL_H
#define EMOJIMODEL_H
#pragma once
#include <QObject>
#include <QSettings>
@@ -86,5 +86,3 @@ private:
QSettings m_settings;
};
#endif // EMOJIMODEL_H

114
src/filetypesingleton.cpp Normal file
View File

@@ -0,0 +1,114 @@
/* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
*/
#include "filetypesingleton.h"
#include <QImageReader>
#include <QMovie>
static QStringList byteArrayListToStringList(const QByteArrayList &byteArrayList)
{
QStringList stringList;
for(const QByteArray &byteArray : byteArrayList) {
stringList.append(QString::fromLocal8Bit(byteArray));
}
return stringList;
}
class FileTypeSingletonPrivate
{
Q_DECLARE_PUBLIC(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingletonPrivate)
public:
FileTypeSingletonPrivate(FileTypeSingleton *qq);
FileTypeSingleton * const q_ptr;
QMimeDatabase mimetypeDatabase;
QStringList supportedImageFormats = byteArrayListToStringList(QImageReader::supportedImageFormats());
QStringList supportedAnimatedImageFormats = byteArrayListToStringList(QMovie::supportedFormats());
};
FileTypeSingletonPrivate::FileTypeSingletonPrivate(FileTypeSingleton* qq) : q_ptr(qq)
{
}
FileTypeSingleton::FileTypeSingleton(QObject* parent)
: QObject(parent)
, d_ptr(new FileTypeSingletonPrivate(this))
{
}
FileTypeSingleton::~FileTypeSingleton() noexcept
{
}
QMimeType FileTypeSingleton::mimeTypeForName(const QString& nameOrAlias) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForName(nameOrAlias);
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QString& fileName, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileName, static_cast<QMimeDatabase::MatchMode>(mode));
}
QMimeType FileTypeSingleton::mimeTypeForFile(const QFileInfo& fileInfo, MatchMode mode) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFile(fileInfo, static_cast<QMimeDatabase::MatchMode>(mode));
}
QList<QMimeType> FileTypeSingleton::mimeTypesForFileName(const QString& fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypesForFileName(fileName);
}
QMimeType FileTypeSingleton::mimeTypeForData(const QByteArray& data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(data);
}
QMimeType FileTypeSingleton::mimeTypeForData(QIODevice* device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForData(device);
}
QMimeType FileTypeSingleton::mimeTypeForUrl(const QUrl& url) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForUrl(url);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString& fileName, QIODevice* device) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, device);
}
QMimeType FileTypeSingleton::mimeTypeForFileNameAndData(const QString& fileName, const QByteArray& data) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.mimeTypeForFileNameAndData(fileName, data);
}
QString FileTypeSingleton::suffixForFileName(const QString& fileName) const
{
Q_D(const FileTypeSingleton);
return d->mimetypeDatabase.suffixForFileName(fileName);
}
QStringList FileTypeSingleton::supportedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedImageFormats;
}
QStringList FileTypeSingleton::supportedAnimatedImageFormats() const
{
Q_D(const FileTypeSingleton);
return d->supportedAnimatedImageFormats;
}

60
src/filetypesingleton.h Normal file
View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2015 Klaralvdalens Datakonsult AB
* SPDX-FileCopyrightText: 2016 The Qt Company Ltd.
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <qqml.h>
#include <QMimeDatabase>
class FileTypeSingletonPrivate;
class FileTypeSingleton : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList supportedImageFormats READ supportedImageFormats CONSTANT FINAL)
Q_PROPERTY(QStringList supportedAnimatedImageFormats READ supportedAnimatedImageFormats CONSTANT FINAL)
QML_NAMED_ELEMENT(FileType)
QML_SINGLETON
public:
explicit FileTypeSingleton(QObject *parent = nullptr);
~FileTypeSingleton();
// Most of the code in this public section was copy/pasted from qmimedatabase.h
Q_INVOKABLE QMimeType mimeTypeForName(const QString &nameOrAlias) const;
enum MatchMode {
MatchDefault,
MatchExtension,
MatchContent
};
Q_ENUM(MatchMode)
Q_INVOKABLE QMimeType mimeTypeForFile(const QString &fileName, MatchMode mode = MatchDefault) const;
Q_INVOKABLE QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode = MatchDefault) const;
Q_INVOKABLE QList<QMimeType> mimeTypesForFileName(const QString &fileName) const;
Q_INVOKABLE QMimeType mimeTypeForData(const QByteArray &data) const;
Q_INVOKABLE QMimeType mimeTypeForData(QIODevice *device) const;
Q_INVOKABLE QMimeType mimeTypeForUrl(const QUrl &url) const;
Q_INVOKABLE QMimeType mimeTypeForFileNameAndData(const QString &fileName, QIODevice *device) const;
Q_INVOKABLE QMimeType mimeTypeForFileNameAndData(const QString &fileName, const QByteArray &data) const;
Q_INVOKABLE QString suffixForFileName(const QString &fileName) const;
// These return a list of file extensions, not mimetypes
QStringList supportedImageFormats() const;
QStringList supportedAnimatedImageFormats() const;
private:
const QScopedPointer<FileTypeSingletonPrivate> d_ptr;
Q_DECLARE_PRIVATE(FileTypeSingleton)
Q_DISABLE_COPY(FileTypeSingleton)
};
QML_DECLARE_TYPE(FileTypeSingleton)

206
src/login.cpp Normal file
View File

@@ -0,0 +1,206 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "login.h"
#include "connection.h"
#include "controller.h"
#include <QUrl>
#include <KLocalizedString>
Login::Login(QObject *parent)
: QObject(parent)
{
init();
}
void Login::init()
{
m_homeserverReachable = false;
m_connection = nullptr;
m_matrixId = QString();
m_password = QString();
m_deviceName = QString();
m_supportsSso = false;
m_supportsPassword = false;
m_ssoUrl = QUrl();
connect(this, &Login::matrixIdChanged, this, [=](){
setHomeserverReachable(false);
if (m_connection) {
delete m_connection;
m_connection = nullptr;
}
if(m_matrixId == "@") {
return;
}
m_testing = true;
Q_EMIT testingChanged();
m_connection = new Connection(this);
m_connection->resolveServer(m_matrixId);
connect(m_connection, &Connection::loginFlowsChanged, this, [=](){
setHomeserverReachable(true);
m_testing = false;
Q_EMIT testingChanged();
m_supportsSso = m_connection->supportsSso();
m_supportsPassword = m_connection->supportsPasswordAuth();
Q_EMIT loginFlowsChanged();
});
});
}
void Login::setHomeserverReachable(bool reachable)
{
m_homeserverReachable = reachable;
Q_EMIT homeserverReachableChanged();
}
bool Login::homeserverReachable() const
{
return m_homeserverReachable;
}
QString Login::matrixId() const
{
return m_matrixId;
}
void Login::setMatrixId(const QString &matrixId)
{
m_matrixId = matrixId;
if(!m_matrixId.startsWith('@')) {
m_matrixId.prepend('@');
}
Q_EMIT matrixIdChanged();
}
QString Login::password() const
{
return m_password;
}
void Login::setPassword(const QString &password)
{
m_password = password;
Q_EMIT passwordChanged();
}
QString Login::deviceName() const
{
return m_deviceName;
}
void Login::setDeviceName(const QString &deviceName)
{
m_deviceName = deviceName;
Q_EMIT deviceNameChanged();
}
void Login::login()
{
m_isLoggingIn = true;
Q_EMIT isLoggingInChanged();
setDeviceName("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
m_connection = new Connection(this);
m_connection->resolveServer(m_matrixId);
connect(m_connection, &Connection::loginFlowsChanged, this, [=]() {
m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString());
connect(m_connection, &Connection::connected, this, [=] {
Q_EMIT connected();
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
AccountSettings account(m_connection->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
});
connect(m_connection, &Connection::networkError, [=](QString error, const QString &, int, int) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
connect(m_connection, &Connection::loginError, [=](QString error, const QString &) {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
m_isLoggingIn = false;
Q_EMIT isLoggingInChanged();
});
});
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
});
connect(m_connection, &Connection::syncDone, this, [=]() {
Q_EMIT initialSyncFinished();
disconnect(m_connection, &Connection::syncDone, this, nullptr);
});
}
bool Login::supportsPassword() const
{
return m_supportsPassword;
}
bool Login::supportsSso() const
{
return m_supportsSso;
}
QUrl Login::ssoUrl() const
{
return m_ssoUrl;
}
void Login::loginWithSso()
{
SsoSession *session = m_connection->prepareForSso("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
m_ssoUrl = session->ssoUrl();
Q_EMIT ssoUrlChanged();
connect(m_connection, &Connection::connected, [=](){
Q_EMIT connected();
AccountSettings account(m_connection->userId());
account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(m_connection->homeserver());
account.setDeviceId(m_connection->deviceId());
account.setDeviceName(m_deviceName);
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
qWarning() << "Couldn't save access token";
}
account.sync();
Controller::instance().addConnection(m_connection);
Controller::instance().setActiveConnection(m_connection);
});
connect(m_connection, &Connection::syncDone, this, [=]() {
Q_EMIT initialSyncFinished();
disconnect(m_connection, &Connection::syncDone, this, nullptr);
});
}
bool Login::testing() const
{
return m_testing;
}
bool Login::isLoggingIn() const
{
return m_isLoggingIn;
}

85
src/login.h Normal file
View File

@@ -0,0 +1,85 @@
/**
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QObject>
#include "csapi/wellknown.h"
#include "connection.h"
using namespace Quotient;
class Login : public QObject
{
Q_OBJECT
Q_PROPERTY(bool homeserverReachable READ homeserverReachable NOTIFY homeserverReachableChanged)
Q_PROPERTY(bool testing READ testing NOTIFY testingChanged)
Q_PROPERTY(QString matrixId READ matrixId WRITE setMatrixId NOTIFY matrixIdChanged)
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(bool supportsPassword READ supportsPassword NOTIFY loginFlowsChanged STORED false)
Q_PROPERTY(QUrl ssoUrl READ ssoUrl NOTIFY ssoUrlChanged)
Q_PROPERTY(bool isLoggingIn READ isLoggingIn NOTIFY isLoggingInChanged)
public:
explicit Login(QObject *parent = nullptr);
Q_INVOKABLE void init();
bool homeserverReachable() const;
QString matrixId() const;
void setMatrixId(const QString &matrixId);
QString password() const;
void setPassword(const QString &password);
QString deviceName() const;
void setDeviceName(const QString &deviceName);
bool supportsPassword() const;
bool supportsSso() const;
bool testing() const;
QUrl ssoUrl() const;
bool isLoggingIn() const;
Q_INVOKABLE void login();
Q_INVOKABLE void loginWithSso();
Q_SIGNALS:
void homeserverReachableChanged();
void testHomeserverFinished();
void matrixIdChanged();
void passwordChanged();
void deviceNameChanged();
void initialSyncFinished();
void loginFlowsChanged();
void ssoUrlChanged();
void connected();
void errorOccured(QString message);
void testingChanged();
void isLoggingInChanged();
private:
void setHomeserverReachable(bool reachable);
bool m_homeserverReachable;
QString m_matrixId;
QString m_password;
QString m_deviceName;
bool m_supportsSso = false;
bool m_supportsPassword = false;
Connection *m_connection = nullptr;
QUrl m_ssoUrl;
bool m_testing;
bool m_isLoggingIn = false;
};

View File

@@ -14,11 +14,12 @@
#include <QQuickWindow>
#include <KAboutData>
#ifndef Q_OS_ANDROID
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
#endif
#include <KLocalizedContext>
#include <KLocalizedString>
#include <KWindowConfig>
#include "neochat-version.h"
@@ -28,9 +29,13 @@
#include "controller.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
#include "devicesmodel.h"
#include "emojimodel.h"
#include "filetypesingleton.h"
#include "login.h"
#include "matriximageprovider.h"
#include "messageeventmodel.h"
#include "messagefiltermodel.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
@@ -41,6 +46,7 @@
#include "sortfilterroomlistmodel.h"
#include "userdirectorylistmodel.h"
#include "userlistmodel.h"
#include "actionshandler.h"
using namespace Quotient;
@@ -64,6 +70,10 @@ int main(int argc, char *argv[])
}
#endif
#ifdef Q_OS_WINDOWS
QApplication::setStyle(QStringLiteral("breeze"));
#endif
QApplication::setOrganizationName("KDE");
KAboutData about(QStringLiteral("neochat"), i18n("Neochat"), QStringLiteral(NEOCHAT_VERSION_STRING), i18n("Matrix client"), KAboutLicense::GPL_V3, i18n("© 2018-2020 Black Hat, 2020 KDE Community"));
@@ -75,32 +85,39 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about);
QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
#ifndef Q_OS_ANDROID
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf",
"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
QFile::copy("/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf", "/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf");
#endif
Clipboard clipboard;
auto config = NeoChatConfig::self();
FileTypeSingleton fileTypeSingleton;
Login *login = new Login();
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<EmojiModel>("org.kde.neochat", 1, 0, "EmojiModel");
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<RoomType>("org.kde.neochat", 1, 0, "RoomType", "ENUM");
qmlRegisterUncreatableType<UserType>("org.kde.neochat", 1, 0, "UserType", "ENUM");
@@ -114,6 +131,7 @@ int main(int argc, char *argv[])
qRegisterMetaType<NeoChatRoom *>("NeoChatRoom*");
qRegisterMetaType<NeoChatUser *>("NeoChatUser*");
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
qRegisterMetaTypeStreamOperators<Emoji>();
@@ -139,7 +157,7 @@ int main(int argc, char *argv[])
return -1;
}
#ifndef Q_OS_ANDROID
#ifdef HAVE_KDBUSADDONS
QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList & /*arguments*/, const QString & /*workingDirectory*/) {
const auto rootObjects = engine.rootObjects();
for (auto obj : rootObjects) {
@@ -151,6 +169,17 @@ int main(int argc, char *argv[])
}
}
});
const auto rootObjects = engine.rootObjects();
for (auto obj : rootObjects) {
auto view = qobject_cast<QQuickWindow*>(obj);
if (view) {
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup windowGroup(&dataResource, "Window");
KWindowConfig::restoreWindowSize(view, windowGroup);
KWindowConfig::restoreWindowPosition(view, windowGroup);
break;
}
}
#endif
return QApplication::exec();
}

View File

@@ -4,8 +4,7 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef MatrixImageProvider_H
#define MatrixImageProvider_H
#pragma once
#include <QQuickAsyncImageProvider>
@@ -53,5 +52,3 @@ class MatrixImageProvider : public QQuickAsyncImageProvider
public:
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
};
#endif // MatrixImageProvider_H

View File

@@ -14,6 +14,8 @@
#include <events/simplestateevents.h>
#include <user.h>
#include "stickerevent.h"
#include <QDebug>
#include <QQmlEngine> // for qmlRegisterType()
#include <QTimeZone>
@@ -45,6 +47,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[ShowSectionRole] = "showSection";
roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited";
roles[FormattedBodyRole] = "formattedBody";
return roles;
}
@@ -62,8 +65,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
return;
}
m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [=](){
if(m_currentRoom->readMarkerEventId().isEmpty()) {
connect(this, &QAbstractListModel::rowsInserted, this, [=]() {
if (m_currentRoom->readMarkerEventId().isEmpty()) {
return;
}
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
@@ -299,7 +302,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
inline QVariantMap userAtEvent(NeoChatUser *user, NeoChatRoom *room, const RoomEvent &evt)
{
Q_UNUSED(evt)
return QVariantMap {
return QVariantMap{
{"isLocalUser", user->id() == room->localUser()->id()},
{"id", user->id()},
{"avatarMediaId", user->avatarMediaId(room)},
@@ -329,9 +332,20 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
auto reason = evt.redactedBecause()->reason();
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>").arg(evt.redactedBecause()->reason());
}
return m_currentRoom->eventToString(evt, Qt::RichText);
}
if (role == FormattedBodyRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
return static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
}
}
return {};
}
if (role == MessageRole) {
return m_currentRoom->eventToString(evt);
}
@@ -362,6 +376,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return "message";
}
if (is<const StickerEvent>(evt)) {
return "sticker";
}
if (evt.isStateEvent()) {
return "state";
}
@@ -398,6 +415,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
// content JSON stored in EventContent::Base
return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
};
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(e->image().originalJson);
}
}
if (role == HighlightRole) {
@@ -466,6 +487,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
}
}
if (auto e = eventCast<const StickerEvent>(&evt)) {
return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id()));
}
}
if (role == AnnotationRole) {
@@ -502,7 +526,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
};
const auto &replyEvt = **replyIt;
return QVariantMap {{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
return QVariantMap{{"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
}
if (role == ShowAuthorRole) {
@@ -556,7 +580,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
authors.append(userAtEvent(author, m_currentRoom, evt));
}
bool hasLocalUser = i.value().contains(static_cast<NeoChatUser *>(m_currentRoom->localUser()));
res.append(QVariantMap {{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}});
++i;
}

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef MESSAGEEVENTMODEL_H
#define MESSAGEEVENTMODEL_H
#pragma once
#include <QAbstractListModel>
@@ -32,6 +32,7 @@ public:
LongOperationRole,
AnnotationRole,
UserMarkerRole,
FormattedBodyRole,
ReplyRole,
@@ -39,6 +40,7 @@ public:
ShowSectionRole,
ReactionRole,
IsEditedRole,
// For debugging
@@ -89,5 +91,3 @@ private:
Q_SIGNALS:
void roomChanged();
};
#endif // MESSAGEEVENTMODEL_H

View File

@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "messagefiltermodel.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt();
if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) {
return false;
}
const QString eventType = index.data(MessageEventModel::EventTypeRole).toString();
if (eventType == QLatin1String("other")) {
return false;
}
if (!NeoChatConfig::self()->showLeaveJoinEvent() && eventType == QLatin1String("state")) {
return false;
}
return true;
}

16
src/messagefiltermodel.h Normal file
View File

@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QSortFilterProxyModel>
class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};

View File

@@ -32,6 +32,18 @@
<label>Show avatar in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowRename" type="bool">
<label>Show rename events in the timeline</label>
<default>true</default>
</entry>
<entry name="ShowAvatarUpdate" type="bool">
<label>Show avatar update events in the timeline</label>
<default>true</default>
</entry>
<entry name="SystemTray" type="bool">
<label>Close NeoChat to system tray</label>
<default>true</default>
</entry>
</group>
</kcfg>

View File

@@ -27,12 +27,13 @@
#include "events/roomcanonicalaliasevent.h"
#include "events/roommessageevent.h"
#include "events/roompowerlevelsevent.h"
#include "stickerevent.h"
#include "events/typingevent.h"
#include "jobs/downloadfilejob.h"
#include "neochatconfig.h"
#include "notificationsmanager.h"
#include "user.h"
#include "utils.h"
#include "neochatconfig.h"
#include <KLocalizedString>
@@ -50,7 +51,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return;
}
const RoomEvent *lastEvent = messageEvents().rbegin()->get();
if(lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
if (lastEvent->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
return;
}
if (lastEvent->isStateEvent()) {
@@ -61,14 +62,25 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
return;
}
NotificationsManager::instance().postNotification(this, lastEvent->id(), displayName(), sender->displayname(this), eventToString(*lastEvent), avatar(128));
QImage avatar_image;
if (!sender->avatarUrl(this).isEmpty()) {
avatar_image = sender->avatar(128, this);
} else {
avatar_image = this->avatar(128);
}
NotificationsManager::instance().postNotification(this, displayName(), sender->displayname(this), eventToString(*lastEvent), avatar_image);
});
connect(this, &Room::aboutToAddHistoricalMessages,
this, &NeoChatRoom::readMarkerLoadedChanged);
connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
connect(this, &Quotient::Room::eventsHistoryJobChanged,
this, &NeoChatRoom::lastActiveTimeChanged);
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
connect(this, &Room::joinStateChanged, this, [=](JoinState oldState, JoinState newState) {
if(oldState == JoinState::Invite && newState != JoinState::Invite) {
Q_EMIT isInviteChanged();
}
});
}
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
@@ -93,7 +105,6 @@ void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
});
connect(this, &Room::fileTransferProgress, [=](const QString &id, qint64 progress, qint64 total) {
if (id == txnId) {
qDebug() << "Progress:" << progress << total;
setFileUploadingProgress(int(float(progress) / float(total) * 100));
}
});
@@ -115,7 +126,12 @@ QVariantList NeoChatRoom::getUsersTyping() const
users.removeAll(localUser());
QVariantList userVariants;
for (User *user : users) {
userVariants.append(QVariant::fromValue(user));
userVariants.append(QVariantMap {
{"id", user->id()},
{"avatarMediaId", user->avatarMediaId(this)},
{"displayName", user->displayname(this)},
{"display", user->name()},
});
}
return userVariants;
}
@@ -166,7 +182,6 @@ QString NeoChatRoom::lastEventToString() const
return QLatin1String("");
}
bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
{
return highlights.contains(e);
@@ -261,7 +276,15 @@ QVariantList NeoChatRoom::getUsers(const QString &keyword) const
QVariantList matchedList;
for (const auto u : userList) {
if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) {
matchedList.append(QVariant::fromValue(u));
NeoChatUser user(u->id(), u->connection());
QVariantMap userVariant {
{ QStringLiteral("id"), user.id() },
{ QStringLiteral("displayName"), user.displayname(this) },
{ QStringLiteral("avatarMediaId"), user.avatarMediaId(this) },
{ QStringLiteral("color"), user.color() }
};
matchedList.append(QVariant::fromValue(userVariant));
}
}
@@ -341,6 +364,9 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
}
return plainBody;
},
[](const StickerEvent &e) {
return e.body();
},
[this](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->user(e.userId())->displayname();
@@ -348,25 +374,38 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
switch (e.membership()) {
case MembershipType::Invite:
if (e.repeatsState()) {
return i18n("reinvited %1 to the room", subjectName);
auto text = i18n("reinvited %1 to the room", subjectName);
if (!e.reason().isEmpty()) {
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
}
return text;
}
break;
Q_FALLTHROUGH();
case MembershipType::Join: {
if (e.repeatsState()) {
return i18n("joined the room (repeated)");
}
if (!e.prevContent() || e.membership() != e.prevContent()->membership) {
return e.membership() == MembershipType::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
}
QString text {};
if (e.isRename()) {
if (e.displayName().isEmpty()) {
// Part 1: invites and joins
if (e.repeatsState()) {
text = i18n("joined the room (repeated)");
} else if (e.changesMembership()) {
text = e.membership() == MembershipType::Invite
? i18n("invited %1 to the room", subjectName)
: i18n("joined the room");
}
if (!text.isEmpty()) {
if (!e.reason().isEmpty()) {
text += i18n(": %1", e.reason().toHtmlEscaped());
}
return text;
}
// Part 2: profile changes of joined members
if (e.isRename() && NeoChatConfig::self()->showRename()) {
if (!e.displayName().isEmpty()) {
text = i18n("cleared their display name");
} else {
text = i18n("changed their display name to %1", e.displayName().toHtmlEscaped());
}
}
if (e.isAvatarUpdate()) {
if (e.isAvatarUpdate() && NeoChatConfig::self()->showAvatarUpdate()) {
if (!text.isEmpty()) {
text += i18n(" and ");
}
@@ -427,7 +466,7 @@ void NeoChatRoom::changeAvatar(const QUrl &localFile)
const auto job = connection()->uploadFile(localFile.toLocalFile());
if (isJobRunning(job)) {
connect(job, &BaseJob::success, this, [this, job] {
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject {{"url", job->contentUri()}});
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar", localUser()->id(), QJsonObject{{"url", job->contentUri()}});
});
}
}
@@ -475,18 +514,6 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
return result;
}
void NeoChatRoom::postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId)
{
const auto parsedHTML = markdownToHTML(text);
const bool isRichText = Qt::mightBeRichText(parsedHTML);
if (isRichText) { // Markdown
postHtmlMessage(text, parsedHTML, type, replyEventId);
} else { // Plain text
postPlainMessage(text, type, replyEventId);
}
}
QString msgTypeToString(MessageEventType msgType)
{
switch (msgType) {
@@ -511,59 +538,52 @@ QString msgTypeToString(MessageEventType msgType)
}
}
void NeoChatRoom::postPlainMessage(const QString &text, MessageEventType type, const QString &replyEventId)
void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
{
bool isReply = !replyEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) {
isReply = false;
}
if (isReply) {
const auto &replyEvt = **replyIt;
// clang-format off
QJsonObject json{
{"msgtype", msgTypeToString(type)},
{"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text},
{"format", "org.matrix.custom.html"},
{"m.relates_to",
QJsonObject {
{"m.in_reply_to",
QJsonObject {
{"event_id", replyEventId}
}
}
}
},
{"formatted_body",
"<mx-reply><blockquote><a href=\"https://matrix.to/#/" +
id() + "/" +
replyEventId +
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + text
}
};
// clang-format on
postJson("m.room.message", json);
return;
}
Room::postMessage(text, type);
const auto html = markdownToHTML(text);
QString cleanText(text);
cleanText.replace(QRegularExpression("\\[(.+)\\]\\(.+\\)"), "\\1");
postHtmlMessage(rawText, html, type, replyEventId, relateToEventId);
}
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId)
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
{
bool isRichText = Qt::mightBeRichText(html);
bool isReply = !replyEventId.isEmpty();
bool isEdit = !relateToEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) {
isReply = false;
}
if (isEdit) {
QJsonObject json {
{"type", "m.room.message"},
{"msgtype", msgTypeToString(type)},
{"body", "* " + text},
{"format", "org.matrix.custom.html"},
{"formatted_body", html},
{"m.new_content",
QJsonObject {
{"body", text},
{"msgtype", msgTypeToString(type)},
{"format", "org.matrix.custom.html"},
{"formatted_body", html}
}
},
{"m.relates_to",
QJsonObject {
{"rel_type", "m.replace"},
{"event_id", relateToEventId}
}
}
};
postJson("m.room.message", json);
return;
}
if (isReply) {
const auto &replyEvt = **replyIt;
@@ -588,7 +608,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + html
"</blockquote></mx-reply>" + (isRichText ? html : text)
}
};
// clang-format on
@@ -598,7 +618,11 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
return;
}
Room::postHtmlMessage(text, html, type);
if (isRichText) {
Room::postHtmlMessage(text, html, type);
} else {
Room::postMessage(text, type);
}
}
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
@@ -675,3 +699,8 @@ bool NeoChatRoom::readMarkerLoaded() const
const auto it = findInTimeline(readMarkerEventId());
return it != timelineEdge();
}
bool NeoChatRoom::isInvite() const
{
return joinState() == JoinState::Invite;
}

View File

@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <events/encryptionevent.h>
@@ -32,6 +33,7 @@ class NeoChatRoom : public Room
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
Q_PROPERTY(QDateTime lastActiveTime READ lastActiveTime NOTIFY lastActiveTimeChanged)
Q_PROPERTY(bool isInvite READ isInvite NOTIFY isInviteChanged)
public:
explicit NeoChatRoom(Connection *connection, QString roomId, JoinState joinState = {});
@@ -102,6 +104,8 @@ public:
Q_INVOKABLE [[nodiscard]] bool canSendEvent(const QString &eventType) const;
Q_INVOKABLE [[nodiscard]] bool canSendState(const QString &eventType) const;
bool isInvite() const;
private:
QString m_cachedInput;
QSet<const Quotient::RoomEvent *> highlights;
@@ -128,15 +132,17 @@ Q_SIGNALS:
void backgroundChanged();
void readMarkerLoadedChanged();
void lastActiveTimeChanged();
void isInviteChanged();
public Q_SLOTS:
void uploadFile(const QUrl &url, const QString &body = "");
void uploadFile(const QUrl &url, const QString &body = QString());
void acceptInvitation();
void forget();
void sendTypingNotification(bool isTyping);
void postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId);
void postPlainMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type = Quotient::MessageEventType::Text, const QString &replyEventId = "");
void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = "");
/// @param rawText The text as it was typed.
/// @param cleanedText The text with link to the users.
void postMessage(const QString &rawText, const QString &cleanedText, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString());
void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString());
void changeAvatar(const QUrl &localFile);
void addLocalAlias(const QString &alias);
void removeLocalAlias(const QString &alias);

View File

@@ -10,13 +10,20 @@
#include "csapi/profile.h"
#include "controller.h"
static Kirigami::PlatformTheme * s_theme = nullptr;
NeoChatUser::NeoChatUser(QString userId, Connection *connection)
: User(std::move(userId), connection)
{
m_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true));
Q_ASSERT(m_theme);
if (!s_theme) {
s_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(&Controller::instance(), true));
Q_ASSERT(s_theme);
}
connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
connect(s_theme, &Kirigami::PlatformTheme::colorsChanged, this, &NeoChatUser::polishColor);
polishColor();
}
QColor NeoChatUser::color()
@@ -31,11 +38,11 @@ void NeoChatUser::setColor(const QColor &color)
}
m_color = color;
emit colorChanged(m_color);
Q_EMIT colorChanged(m_color);
}
void NeoChatUser::polishColor()
{
// https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal
setColor(QColor::fromHslF(hueF(), 1 - m_theme->alternateBackgroundColor().saturationF(), -0.7 * m_theme->alternateBackgroundColor().lightnessF() + 0.9, m_theme->textColor().alphaF()));
setColor(QColor::fromHslF(hueF(), 1 - s_theme->alternateBackgroundColor().saturationF(), -0.7 * s_theme->alternateBackgroundColor().lightnessF() + 0.9, s_theme->textColor().alphaF()));
}

View File

@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <QObject>
@@ -32,7 +33,6 @@ Q_SIGNALS:
void colorChanged(QColor _t1);
private:
Kirigami::PlatformTheme *m_theme = nullptr;
QColor m_color;
void polishColor();

View File

@@ -11,8 +11,8 @@
#include <KLocalizedString>
#include <KNotification>
#include "neochatconfig.h"
#include "controller.h"
#include "neochatconfig.h"
NotificationsManager &NotificationsManager::instance()
{
@@ -25,7 +25,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{
}
void NotificationsManager::postNotification(NeoChatRoom *room, const QString &eventid, const QString &roomname, const QString &sender, const QString &text, const QImage &icon)
void NotificationsManager::postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon)
{
if (!NeoChatConfig::self()->showNotifications()) {
return;
@@ -35,10 +35,10 @@ void NotificationsManager::postNotification(NeoChatRoom *room, const QString &ev
img.convertFromImage(icon);
KNotification *notification = new KNotification("message");
if (sender == roomname) {
if (sender == roomName) {
notification->setTitle(sender);
} else {
notification->setTitle(i18n("%1 (%2)", sender, roomname));
notification->setTitle(i18n("%1 (%2)", sender, roomName));
}
notification->setText(text.toHtmlEscaped());

View File

@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QImage>
@@ -21,7 +22,7 @@ class NotificationsManager : public QObject
public:
static NotificationsManager &instance();
Q_INVOKABLE void postNotification(NeoChatRoom *roomId, const QString &eventId, const QString &roomName, const QString &senderName, const QString &text, const QImage &icon);
Q_INVOKABLE void postNotification(NeoChatRoom *room, const QString &roomName, const QString &sender, const QString &text, const QImage &icon);
private:
NotificationsManager(QObject *parent = nullptr);

View File

@@ -115,7 +115,7 @@ void PublicRoomListModel::next(int count)
return;
}
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter {m_keyword});
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, count, nextBatch, QueryPublicRoomsJob::Filter{m_keyword});
connect(job, &BaseJob::finished, this, [=] {
attempted = true;

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef PUBLICROOMLISTMODEL_H
#define PUBLICROOMLISTMODEL_H
#pragma once
#include <QAbstractListModel>
#include <QObject>
@@ -83,5 +83,3 @@ Q_SIGNALS:
void keywordChanged();
void hasMoreChanged();
};
#endif // PUBLICROOMLISTMODEL_H

View File

@@ -14,11 +14,26 @@
#include <QBrush>
#include <QColor>
#include <QDebug>
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusInterface>
#endif
#include <QStandardPaths>
#include <KLocalizedString>
#include <utility>
#ifndef Q_OS_ANDROID
bool useUnityCounter() {
static const auto Result = QDBusInterface(
"com.canonical.Unity",
"/").isValid();
return Result;
}
#endif
RoomListModel::RoomListModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -26,6 +41,37 @@ RoomListModel::RoomListModel(QObject *parent)
for (auto collapsedSection : collapsedSections) {
m_categoryVisibility[collapsedSection] = false;
}
#ifndef Q_OS_ANDROID
connect(this, &RoomListModel::notificationCountChanged, this, [this]() {
if (useUnityCounter()) {
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop";
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(m_notificationCount, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"] = counterSlice;
dbusUnityProperties["count-visible"] = true;
} else {
dbusUnityProperties["count-visible"] = false;
}
auto signal = QDBusMessage::createSignal(
"/com/canonical/unity/launcherentry/neochat",
"com.canonical.Unity.LauncherEntry",
"Update");
signal.setArguments({
launcherUrl,
dbusUnityProperties
});
QDBusConnection::sessionBus().send(signal);
}
});
#endif
}
RoomListModel::~RoomListModel() = default;
@@ -182,6 +228,9 @@ void RoomListModel::refreshNotificationCount()
for (auto room : qAsConst(m_rooms)) {
count += room->notificationCount();
}
if (m_notificationCount == count) {
return;
}
m_notificationCount = count;
Q_EMIT notificationCountChanged();
}
@@ -380,3 +429,13 @@ bool RoomListModel::categoryVisible(int category) const
{
return m_categoryVisibility.value(category, true);
}
NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId)
{
for(const auto &room : m_rooms) {
if(room->aliases().contains(aliasOrId) || room->id() == aliasOrId) {
return room;
}
}
return nullptr;
}

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef ROOMLISTMODEL_H
#define ROOMLISTMODEL_H
#pragma once
#include "connection.h"
#include "events/roomevent.h"
@@ -80,6 +80,8 @@ public:
return m_notificationCount;
}
Q_INVOKABLE NeoChatRoom *roomByAliasOrId(const QString &aliasOrId);
private Q_SLOTS:
void doAddRoom(Quotient::Room *room);
void updateRoom(Quotient::Room *room, Quotient::Room *prev);
@@ -105,5 +107,3 @@ Q_SIGNALS:
void newMessage(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
void newHighlight(const QString &_t1, const QString &_t2, const QString &_t3, const QString &_t4, const QString &_t5, const QImage &_t6);
};
#endif // ROOMLISTMODEL_H

26
src/stickerevent.cpp Normal file
View File

@@ -0,0 +1,26 @@
// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "stickerevent.h"
using namespace Quotient;
StickerEvent::StickerEvent(const QJsonObject &obj)
: RoomEvent(typeId(), obj)
, m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject()))
{}
QString StickerEvent::body() const
{
return content<QString>("body"_ls);
}
const EventContent::ImageContent &StickerEvent::image() const
{
return m_imageContent;
}
QUrl StickerEvent::url() const
{
return m_imageContent.url;
}

38
src/stickerevent.h Normal file
View File

@@ -0,0 +1,38 @@
// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "events/roomevent.h"
#include "events/eventcontent.h"
namespace Quotient {
/// Sticker messages are specialised image messages that are displayed without
/// controls (e.g. no "download" link, or light-box view on click, as would be
/// displayed for for m.image events).
class StickerEvent : public RoomEvent
{
public:
DEFINE_EVENT_TYPEID("m.sticker", StickerEvent)
explicit StickerEvent(const QJsonObject &obj);
/// \brief A textual representation or associated description of the
/// sticker image.
///
/// This could be the alt text of the original image, or a message to
/// accompany and further describe the sticker.
QString body() const;
/// \brief Metadata about the image referred to in url including a
/// thumbnail representation.
const EventContent::ImageContent &image() const;
/// \brief The URL to the sticker image. This must be a valid mxc:// URI.
QUrl url() const;
private:
EventContent::ImageContent m_imageContent;
};
REGISTER_EVENT_TYPE(StickerEvent)
} // namespace Quotient

View File

@@ -1,5 +1,6 @@
/**
* SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
* SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -11,34 +12,28 @@
#include <KLocalizedString>
TrayIcon::TrayIcon(QObject *parent)
: KStatusNotifierItem(parent)
: QSystemTrayIcon(parent)
{
setIcon(QIcon::fromTheme("org.kde.neochat"));
QMenu *menu = new QMenu();
auto viewAction_ = new QAction(i18n("Show"), parent);
connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow);
connect(this, &KStatusNotifierItem::activateRequested, this, [this](bool active) {
if (active) {
connect(this, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) {
Q_EMIT showWindow();
}
});
menu->addAction(viewAction_);
setCategory(Communications);
menu->addSeparator();
auto quitAction = new QAction(i18n("Quit"), parent);
quitAction->setIcon(QIcon::fromTheme("application-exit"));
connect(quitAction, &QAction::triggered, QCoreApplication::instance(), QCoreApplication::quit);
menu->addAction(quitAction);
setContextMenu(menu);
}
void TrayIcon::setIsOnline(bool online)
{
m_isOnline = online;
setStatus(Active);
Q_EMIT isOnlineChanged();
}
void TrayIcon::setIconSource(const QString &source)
{
m_iconSource = source;
setIconByName(source);
Q_EMIT iconSourceChanged();
}

View File

@@ -3,48 +3,24 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef TRAYICON_H
#define TRAYICON_H
#pragma once
// Modified from mujx/nheko's TrayIcon.
#include <KStatusNotifierItem>
#include <QSystemTrayIcon>
#include <QAction>
#include <QIcon>
#include <QIconEngine>
#include <QPainter>
#include <QRect>
class TrayIcon : public KStatusNotifierItem
class TrayIcon : public QSystemTrayIcon
{
Q_OBJECT
Q_PROPERTY(QString iconSource READ iconSource WRITE setIconSource NOTIFY iconSourceChanged)
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
public:
TrayIcon(QObject *parent = nullptr);
QString iconSource()
{
return m_iconSource;
}
void setIconSource(const QString &source);
bool isOnline() const
{
return m_isOnline;
}
void setIsOnline(bool online);
Q_SIGNALS:
void notificationCountChanged();
void iconSourceChanged();
void isOnlineChanged();
void showWindow();
private:
QString m_iconSource;
bool m_isOnline = true;
};
#endif // TRAYICON_H

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef USERDIRECTORYLISTMODEL_H
#define USERDIRECTORYLISTMODEL_H
#pragma once
#include <QAbstractListModel>
#include <QObject>
@@ -71,5 +71,3 @@ Q_SIGNALS:
void keywordChanged();
void limitedChanged();
};
#endif // USERDIRECTORYLISTMODEL_H

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef USERLISTMODEL_H
#define USERLISTMODEL_H
#pragma once
#include "room.h"
@@ -76,5 +76,3 @@ private:
int findUserPos(Quotient::User *user) const;
[[nodiscard]] int findUserPos(const QString &username) const;
};
#endif // USERLISTMODEL_H

View File

@@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef Utils_H
#define Utils_H
#pragma once
#include "room.h"
#include "user.h"
@@ -20,11 +20,9 @@
namespace utils
{
static const QRegularExpression removeReplyRegex {"> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReplyRegex {"<mx-reply>.*?</mx-reply>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePillRegExp {"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression userPillRegExp {"<a href=\"https://matrix.to/#/@.*?:.*?\">(.*?)</a>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression strikethroughRegExp {"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeReplyRegex{"> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression removeRichReplyRegex{"<mx-reply>.*?</mx-reply>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression codePillRegExp{"<pre><code[^>]*>(.*?)</code></pre>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression userPillRegExp{"<a href=\"https://matrix.to/#/@.*?:.*?\">(.*?)</a>", QRegularExpression::DotMatchesEverythingOption};
static const QRegularExpression strikethroughRegExp{"<del>(.*?)</del>", QRegularExpression::DotMatchesEverythingOption};
} // namespace utils
#endif