Compare commits

...

128 Commits

Author SHA1 Message Date
Bhushan Shah
be116e1ba7 GIT_SILENT: add changelog entries for 22.04 2022-04-23 18:25:50 +05:30
Bhushan Shah
3396f831d4 GIT_SILENT: bump version to 22.04 2022-04-23 17:04:13 +05:30
l10n daemon script
a0f6170539 GIT_SILENT Sync po/docbooks with svn 2022-04-23 01:49:18 +00:00
l10n daemon script
731c6f924c GIT_SILENT Sync po/docbooks with svn 2022-04-22 01:53:07 +00:00
l10n daemon script
3011c3d885 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"
2022-04-22 01:44:58 +00:00
Nicolas Fella
709b2c8fd9 Use undeprecated install dirs
Using kde-dev-scripts/kf5/cmakelists_install_vars.pl
2022-04-21 20:58:00 +02:00
l10n daemon script
0cfa87e23d GIT_SILENT Sync po/docbooks with svn 2022-04-21 01:48:36 +00:00
l10n daemon script
538ed7dd02 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"
2022-04-21 01:42:02 +00:00
l10n daemon script
180a754e67 GIT_SILENT Sync po/docbooks with svn 2022-04-19 01:51:51 +00:00
l10n daemon script
81ba5f6ee5 GIT_SILENT Sync po/docbooks with svn 2022-04-17 01:57:04 +00:00
l10n daemon script
a15b406cff 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"
2022-04-16 01:44:16 +00:00
Marcus Harrison
d0bc8f3d05 Fix mis-aligned user messages
In compact mode with userMessagesOnRight, the user
avatar disappeared and their messages left space
on the right for an avatar that wasn't displayed
anymore.
2022-04-14 14:38:02 +02:00
l10n daemon script
c83f4b4f75 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"
2022-04-10 01:45:29 +00:00
Tobias Fella
0f5425e030 Require passing tests on CI 2022-04-09 21:33:52 +02:00
Tobias Fella
f381cc4623 Close WelcomePage after account is loaded 2022-04-09 19:47:36 +02:00
Tobias Fella
decd528079 Disable busyindicator 2022-04-09 19:47:17 +02:00
Tobias Fella
0c5bd57976 Fix REUSE check on CI
The CI installs files to _include and _build in the source directory, which breaks
the REUSE check
2022-04-09 15:19:35 +00:00
Tobias Fella
7362b90c42 Don't try to load more messages than there are in the timeline
The function call from qml is removed because it is redundant
2022-04-08 18:44:30 +00:00
Tobias Fella
aef6d6fc85 More typing notification improvements 2022-04-08 20:37:17 +02:00
Tobias Fella
432e209b16 Try fixing stuck read notifications 2022-04-08 20:33:41 +02:00
l10n daemon script
a72cac5ea3 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"
2022-04-08 01:44:45 +00:00
Tobias Fella
b9152dc93c Add ki18n_install 2022-04-07 17:25:16 +02:00
l10n daemon script
e5791970da 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"
2022-04-07 01:43:10 +00:00
Carl Schwan
c157625645 Fix link 2022-04-06 14:04:21 +00:00
Nicolas Fella
026c7660bc Add Windows CI 2022-04-06 12:01:47 +02:00
Nicolas Fella
be10e66974 Fix condition to build runner 2022-04-06 12:01:47 +02:00
l10n daemon script
024fb1a97a 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"
2022-04-04 01:46:19 +00:00
l10n daemon script
e4c8b6b676 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"
2022-04-03 01:54:27 +00:00
l10n daemon script
863508b629 GIT_SILENT made messages (after extraction) 2022-04-03 00:48:36 +00:00
l10n daemon script
ef5550bafd 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"
2022-04-02 01:41:06 +00:00
Nicolas Fella
1cc8d915bc Add rooms runner
This allows to search for and open rooms in KRunner
2022-04-01 10:56:19 +00:00
Snehit Sah
9a5f2e4938 Show subtitle text without markdown
Create new role in RoomListModel to send back cleaned subtitle text
2022-03-31 17:39:34 +00:00
l10n daemon script
a747d44cac 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"
2022-03-14 01:43:37 +00:00
Tobias Fella
334c13b36c Set preferredWidth and preferredHeight of images 2022-03-11 15:09:57 +01:00
Tobias Fella
aac96da2e2 Revert "Show RoomList when cached state is loaded"
This reverts commit db5f328539.
2022-03-08 21:10:38 +01:00
Tobias Fella
12f3f72a67 Lower typing notification timeouts 2022-03-08 15:00:00 +01:00
Tobias Fella
62f6cfbf9a Force RoomListDelegate to use plaintext
Text.AutoText isn't robust enough to handle this
2022-03-08 14:45:33 +01:00
l10n daemon script
c59e3db1dd 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"
2022-03-05 01:43:56 +00:00
Tobias Fella
9252e0e65e Disable the BusyIndicator
For some reason having the busyindicator running increases the time
required to load the state cache by several orders of magnitude
2022-03-01 12:18:07 +00:00
Carl Schwan
80ee5e9356 Apply 1 suggestion(s) to 1 file(s) 2022-03-01 00:29:07 +00:00
Tobias Fella
be802a28c2 Make invitation notifications persistent 2022-03-01 00:29:07 +00:00
Tobias Fella
b2a8430fa2 Don't apply autocompletion when autocomplete list is empty
Fixes sending messages like ':)'
2022-03-01 00:26:28 +00:00
Tobias Fella
db5f328539 Show RoomList when cached state is loaded
This should somewhat speed up the loading since we don't need to wait
until the first sync is done.

It's still slow though since loading the cache is slow
2022-03-01 00:29:48 +01:00
l10n daemon script
9ac1fbd99b GIT_SILENT made messages (after extraction) 2022-02-27 00:46:56 +00:00
Tobias Fella
022951a9df Add nicer delegate message for widget events 2022-02-25 20:49:57 +00:00
Tobias Fella
47a0d30e57 Fix quitting without tray icon
Setting KSNI status to Passive doesn't *disable* the tray icon, it just
moves it to the overflow menu. This causes the application to not quit
when closing the app even when disabling the tray icon
2022-02-25 20:19:12 +00:00
Tobias Fella
faeb1964bd Prepare Image & Video loading for E2EE
Changes the urls to make sure they are decrypted, while making sure that
it is backwards compatible to libQuotient 0.6
2022-02-25 21:15:46 +01:00
Tobias Fella
db8b2fd64b Aggregate similar state events 2022-02-25 20:10:07 +00:00
Tobias Fella
37c7fe380b Don't load backlog until read marker
This is bad if there are a lot of unread messages
2022-02-25 12:29:03 +01:00
l10n daemon script
537a1e44b1 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"
2022-02-25 01:50:55 +00:00
Tobias Fella
dc9d574b58 Fix login regex 2022-02-23 22:49:58 +01:00
Jose Flores
7c807e6a25 Modifies regex check for valid matrix server to accept ip addresses. 2022-02-22 22:54:43 +00:00
Jose Flores
f74c6a41ae Wraps the checkbox text for messages on the right to wrap on mobile devices (PinePhone). 2022-02-22 03:47:20 +00:00
Jose Flores
7d5a8c87a1 Wraps the quick edit checkbox using workaround 2022-02-22 03:47:20 +00:00
Sandro Knauß
ca719b835e The component of QtCoro5 is called Core and not Coro ;) 2022-02-21 16:31:24 +00:00
l10n daemon script
9b5ad3a3a0 GIT_SILENT made messages (after extraction) 2022-02-21 00:43:44 +00:00
Jose Flores
fdfbbb1b04 Uses the formatted message to enable clickable links for mobile. 2022-02-19 14:30:16 +00:00
Tobias Fella
dd91cb91d0 Load replied-to message when it isn't in the timeline already 2022-02-18 16:11:51 +01:00
Tobias Fella
290b2249c4 Port away from CMake deprecation 2022-02-14 22:41:42 +01:00
Jose Flores
8b8e521c56 Fix issue with clear image button. Will only be visible if the user has an avatar (local or saved) 2022-02-13 22:46:51 +00:00
Tobias Fella
cba88e1af7 Allow disabling notification inline reply
Is temporarily required for encrypted rooms
2022-02-12 22:33:10 +01:00
Tobias Fella
1661d34d7c Use Quotient's NetworkAccessManager in QML
Will be required for showing encrypted images
2022-02-12 22:23:59 +01:00
Tobias Fella
dc3b1a3c87 Remove unneeded parameter 2022-02-12 22:09:38 +01:00
Tobias Fella
f55dc19d95 Make user colors update when colortheme changes 2022-02-11 02:06:46 +01:00
Vitaly Zaitsev
6014c15b4f SingleMainWindow is a part of XDG SPEC version 1.5 and bogus on 1.0.
Signed-off-by: Vitaly Zaitsev <vitaly@easycoding.org>
2022-02-09 17:30:19 +01:00
l10n daemon script
a5f835b1eb GIT_SILENT made messages (after extraction) 2022-02-09 00:47:40 +00:00
Bhushan Shah
1fd6b615ff GIT_SILENT: Update version and appstream data for 22.02 2022-02-08 18:23:50 +05:30
Carl Schwan
dd4ef2e4ac GIT_SILENT: Add appstream description 2022-02-08 12:51:47 +00:00
Nicolas Fella
3b73409b7a Don't recreate config group when saving last room
Instead have the group as a member of the room manager
2022-02-07 21:54:46 +01:00
Tobias Fella
335ef240f5 Don't crash on empty creation event 2022-02-07 15:35:45 +01:00
Carl Schwan
ca8a21c0eb Implement sharing with Purpose (export)
This provide both a mobile and desktop view

Fix #181
2022-02-05 16:30:02 +00:00
ivan tkachenko
3e6f38c8ea Use ellipsis in «Loading…» strings 2022-02-04 20:59:17 +03:00
Tobias Fella
a6ab447955 Implement adding labels for account
This gives the user the ability to label different account (e.g. "work",
"private") and shows this label in the account switcher. Showing the
label in more places will be done in future MRs.

The label is stored in the user's account data and thus transfers
automatically to other instances of neochat
2022-01-31 22:45:17 +01:00
Tobias Fella
0b7dcd70ac Immediately apply leave/join event setting
Fixes #374
2022-01-31 21:52:28 +01:00
Tobias Fella
bce560b03b Fix left margin in EncryptedDelegate 2022-01-30 23:21:00 +01:00
Tobias Fella
5a1198d28c Set empty state key for room avatar change events 2022-01-29 01:13:55 +01:00
l10n daemon script
b236e61ea7 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"
2022-01-22 01:48:34 +00:00
l10n daemon script
79faeb21c9 GIT_SILENT made messages (after extraction) 2022-01-22 00:44:10 +00:00
Felipe Kinoshita
6ac6234886 Give settings window a title
Now the settings window title is "Configure ─ NeoChat" instead of
"Neochat <2>".
2022-01-21 21:42:46 +00:00
Devin Lin
fc9e4fc961 Reduce minimum height of the window
Reduce the minimum height of the window so that it doesn't go off the screen when on mobile landscape (Pinephone)
2022-01-18 23:56:57 +00:00
Friedrich W. H. Kossebau
24644887e0 Modernize code to activate window on user activation
* use KWindowSystem::updateStartupId() for abstract update of X11
  startup id or wayland activation token
* call QWindow::raise() also for wayland to prepare when it will
  support it
* call KWindowSystem::activateWindow() for all platforms, like done
  by other apps on user-triggered activation
2022-01-16 19:15:22 +00:00
Christopher Hock
a29ec0a18a Use the x-kde-origin-name notification hint to pass the account name to push notifications 2022-01-15 17:56:25 +01:00
Antonio Rojas
9300e65239 Fix build with qcoro 0.4
Cmake targets and config files have been renamed. Check for the 0.4 name first and fall back to the old one
2022-01-06 21:37:07 +00:00
Nate Graham
ee59006c08 Change X-GNOME-SingleWindow key to SingleMainWindow
X-GNOME-SingleWindow was upstreamed to be an XDG thing with the name
"SingleMainWindow" in
https://gitlab.freedesktop.org/xdg/xdg-specs/-/merge_requests/53
2022-01-05 17:02:54 -07:00
Tobias Fella
ca8702fd5e Don't set cmake policy when using libquotient 0.7 2022-01-02 22:47:42 +01:00
Tobias Fella
183c3227a9 Set bugs url 2022-01-02 22:34:06 +01:00
l10n daemon script
e86f70db85 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"
2022-01-02 01:57:27 +00:00
l10n daemon script
00d8fb75e3 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"
2022-01-01 01:42:37 +00:00
l10n daemon script
c3d149765c GIT_SILENT made messages (after extraction) 2021-12-29 00:48:31 +00:00
Carl Schwan
200281702a Raise windows also on other platforms
And unify code related to unique application handling
2021-12-28 23:50:25 +01:00
Carl Schwan
297684a139 Fix issues with saveFileAs
Fix #491
2021-12-28 23:42:16 +01:00
Jack Hill
aeee367e82 Changed "Settings" to "Configure NeoChat" in menu
Changed both the hamburger menu and the global menu
Fix #489
2021-12-28 16:51:30 +00:00
Carl Schwan
aa9dcc87cb Fix variable lookup in the timeline delegates
This fix issues with downloading and interacting with files
2021-12-28 16:18:05 +01:00
Carl Schwan
8a70e240e4 Improve toolbar on mobile
* Use Toolbar style with only back button when needed
* Don't show context drawer on room list

![image](/uploads/681f11e7d1a340a1b6a834df2b32960a/image.png)

![image](/uploads/564a91df531e7de363743efd4915b2e8/image.png)
2021-12-28 15:00:30 +00:00
Carl Schwan
50a7df8e03 ifdef version for compatibility with our minimal required version 2021-12-26 20:30:10 +00:00
Carl Schwan
dd977976db Improve emoji pane 2021-12-26 13:26:42 +00:00
Carl Schwan
de666b9377 Check if password can be changed 2021-12-26 13:08:05 +00:00
Carl Schwan
5f41378214 Fix image tooltip
display needs to be from the model and not from the Control
2021-12-26 00:14:09 +01:00
Carl Schwan
383b31c185 Expose hiddent GUI option
Fix #49
2021-12-25 20:34:47 +00:00
Carl Schwan
de2fbadba5 Adapt list setting pages to new style 2021-12-25 19:50:31 +00:00
Carl Schwan
67bc66ee0c Allow using ESC to go back to room list
Fix #392
2021-12-25 18:12:13 +00:00
Carl Schwan
924a1fed21 Port away from QNetworkConfigurationManager
QNetworkConfigurationManager was removed from Qt6 and it's better to
check the status of the sync job anyway. Only two issues:

* The timeout in quotient is quite high so it might take up to one
  minute before the message appear.
* Only sync job are listened but since they are continuously done, this
  is not a big issue and other job are affected by the same issue of an
  high timeout anyway.

Fix #414
2021-12-25 17:55:48 +00:00
Carl Schwan
b0a1de7572 Fix reuse issue 2021-12-25 18:55:32 +01:00
Carl Schwan
ca2b5fde8e Remove lag when starting user autocompletion
We realistically don't need to interate over every user when typing '@',
since this is not usefull for the user and create some lag.
2021-12-25 18:31:22 +01:00
Nicolas Fella
26f0cd4cf4 Set single window hint in desktop file
NeoChat is a single window application

This allows shells to hide the 'Open new window' action for NeoChat
2021-12-25 17:30:50 +01:00
Carl Schwan
0801b815c8 Make room address selectable
Help with #469
2021-12-25 15:33:26 +01:00
Carl Schwan
28137c8c86 Display monochrome icon in tray
Fix #471
2021-12-25 15:30:20 +01:00
Carl Schwan
e79e06235f Fix QuickSwitcher activation
By making sure the global menu bar is disabled when not needed. This
should also help with memory usage.

Fix #482
2021-12-25 14:41:43 +01:00
Nicolas Fella
dce7fde7a6 Fix Windows/mac build 2021-12-23 23:24:29 +01:00
Tobias Fella
8d9f3b8658 Revert "Add CI for FreeBSD"
This reverts commit d71ccc46d0
2021-12-22 16:52:43 +00:00
Nicolas Fella
5e1adf7ea7 Fix notifications on Android
Bundle the notifyrc file in qrc so that KNotifications finds it
2021-12-22 14:23:47 +00:00
Tobias Fella
d71ccc46d0 Add CI for FreeBSD 2021-12-22 14:14:30 +00:00
Aleix Pol
284a1734ae Support raising when we receive a notification 2021-12-15 15:08:32 +00:00
Tobias Fella
8722c99c93 Remove unused function 2021-12-15 01:04:51 +01:00
Tobias Fella
0c5932b3da Use a reasonable role for message source 2021-12-15 01:03:05 +01:00
Tobias Fella
332d6c9782 Minor improvements
- Rename TextDelegate to RichLabel since it's not actually a delegate
- Allow web search for whole messages
2021-12-15 00:53:43 +01:00
Tobias Fella
91f3f64bb5 Don't connect to something that isn't a signal 2021-12-14 22:34:32 +00:00
Tobias Fella
599ab11656 Refactor delegates 2021-12-14 22:27:29 +00:00
Tobias Fella
ff707b7a58 Remove dead code 2021-12-14 16:48:42 +01:00
Tobias Fella
e551319245 Don't render html in RoomDrawer heading 2021-12-14 15:54:14 +01:00
Fushan Wen
59430cce89 Add support for minimizing to system tray on startup
If the user wants to automatically launch NeoChat when the system
starts up, the user may also want to minimize the window to system tray
on startup. So a new option named "Minimize to system tray on startup"
is added.

The option is only visible on desktop platforms, and is only enabled
when "Close to system tray" is checked.

In order to restore window geometry for the first time the user opens
the window if the option is checked,

1. a new function named `restoreWindowGeometry` is added, and
   `restoreWindowGeometryConnections` will be enabled if the option is
   checked, and will be disabled after the window debuts.
2. `saveWindowGeometryConnections` will be enabled if the option is not
   checked, and will be disabled if checked and enabled after the window
   debuts.
2021-12-13 22:05:20 +08:00
Carl Schwan
d1bbb5e3f7 Use non blocking passord reading
This also remove the do while loop that might cause problem and expose
the error message to the user.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-12-12 22:13:19 +00:00
Carl Schwan
6e1c07047e Add a mobile oriented context menu for the room list
It works similarly as in the timeline with a bottom based drawer on
mobile and a normal context menu on desktop

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-12-12 22:09:46 +00:00
Carl Schwan
738270f513 Fix loading room settings on mobile
Required properties don't work correctly with StackLayou.push

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-12-12 16:57:40 +00:00
l10n daemon script
9496127e88 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-12-12 01:44:42 +00:00
Nicolas Fella
16d43e9ee8 Use icon from qrc for system tray icon
Fixes the system tray on Windows
2021-12-11 20:10:59 +01:00
Tobias Fella
d0e04e0c97 Adapt to libQuotient API change 2021-12-10 18:06:12 +01:00
Carl Schwan
658eb187c9 Prevent instability with TextArea with null as background 2021-12-08 14:16:34 +01:00
119 changed files with 72202 additions and 1277 deletions

View File

@@ -5,3 +5,4 @@ include:
- https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-reuse.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml

View File

@@ -23,3 +23,6 @@ Dependencies:
- 'on': ['Linux', 'FreeBSD']
'require':
'frameworks/kdbusaddons': '@stable'
Options:
require-passing-tests-on: [ 'Linux', 'Windows' ]

View File

@@ -2,7 +2,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: NeoChat
Upstream-Contact: Carl Schwan <carlschwan@kde.org>
Files: 128-logo.png icons/* logo.png org.kde.neochat.svg org.kde.neochat-symbolic.svg android/res/drawable/neochat.png
Files: 128-logo.png icons/* logo.png org.kde.neochat.svg org.kde.neochat.tray.svg android/res/drawable/neochat.png
Copyright: 2020 Carson Black <uhhadd@gmail.com>
License: LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
@@ -22,7 +22,7 @@ Files: .gitlab/issue_templates/bug.md
Copyright: 2021 Carl Schwan <carlschwan@kde.org>
License: CC0-1.0
Files: res.qrc
Files: res.qrc res_android.qrc res_desktop.qrc
Copyright: None
License: CC0-1.0
@@ -35,7 +35,7 @@ Copyright: 2020-2021 Carl Schwan <carlschwan@kde.org>
Copyright: 2020-2021 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause
Files: neochat.notifyrc
Files: src/neochat.notifyrc
Copyright: 2020 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause

View File

@@ -7,7 +7,7 @@
cmake_minimum_required(VERSION 3.16)
project(NeoChat)
set(PROJECT_VERSION "21.12")
set(PROJECT_VERSION "22.04")
set(KF5_MIN_VERSION "5.88.0")
set(QT_MIN_VERSION "5.15.2")
@@ -24,7 +24,7 @@ set(KDE_COMPILERSETTINGS_LEVEL 5.84)
include(FeatureSummary)
include(ECMSetupVersion)
include(KDEInstallDirs)
include(ECMQMLModules)
include(ECMFindQmlModule)
include(KDEClangFormat)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
@@ -36,9 +36,6 @@ 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(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
@@ -111,22 +108,27 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro REQUIRED)
find_package(QCoro5 COMPONENTS Core QUIET)
if(NOT QCoro5_FOUND)
find_package(QCoro REQUIRED)
endif()
qcoro_enable_coroutines()
if(NOT Quotient_VERSION_MINOR GREATER 6)
cmake_policy(SET CMP0063 OLD)
endif()
if(ANDROID)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
endif()
ki18n_install(po)
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})
install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
add_definitions(-DQT_NO_FOREACH)
@@ -138,5 +140,9 @@ file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
# CI installs dependency headers to _install and _build, which break the reuse check
# Fixes the test by excluding this directory
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})

175
LICENSES/LGPL-2.1-only.txt Normal file
View File

@@ -0,0 +1,175 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
one line to give the library's name and an idea of what it does.
Copyright (C) year name of author
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 2.1 of the License, or (at your option) any later version.
This library 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
signature of Ty Coon, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -14,7 +14,7 @@ KConfig and KI18n.
## Get it
A stable release [is available](https://apps.kde.org/en/neochat) for download for Linux distributions.
A stable release [is available](https://apps.kde.org/neochat) for download for Linux distributions.
Along with the stable release, a Flatpak version is available for the nightly

View File

@@ -120,36 +120,21 @@ ToolBar {
room: currentRoom ?? null
}
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)
}
function sendMessage(event) {
if (isCompleting) {
if (isCompleting && completionMenu.count > 0) {
chatBar.complete();
isCompleting = false;
return;
}
if (event.modifiers & Qt.ShiftModifier) {
} else if (event.modifiers & Qt.ShiftModifier) {
inputField.insert(cursorPosition, "\n")
} else {
currentRoom.sendTypingNotification(false)
chatBar.postMessage()
}
isCompleting = false;
}
Keys.onReturnPressed: { sendMessage(event) }
@@ -244,8 +229,11 @@ ToolBar {
}
onTextChanged: {
timeoutTimer.restart()
if (!repeatTimer.running) {
currentRoom.sendTypingNotification(true)
}
repeatTimer.start()
currentRoom.cachedInput = text
autoAppeared = false;
@@ -268,7 +256,7 @@ ToolBar {
completionMenu.completionType = completionInfo.type
if (completionInfo.type === ChatDocumentHandler.User) {
completionMenu.model = currentRoom.getUsers(completionInfo.keyword);
completionMenu.model = currentRoom.getUsers(completionInfo.keyword, 10);
} else if (completionInfo.type === ChatDocumentHandler.Command) {
completionMenu.model = CommandModel.filterModel(completionInfo.keyword);
} else {

View File

@@ -95,9 +95,16 @@ Item {
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: replySeparator.top
sourceComponent: EmojiPicker{
textArea: chatBar.textField
onChosen: addText(emoji)
sourceComponent: QQC2.Pane {
topPadding: 0
bottomPadding: 0
rightPadding: 0
leftPadding: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: EmojiPicker {
textArea: chatBar.textField
onChosen: addText(emoji)
}
}
Behavior on height {
NumberAnimation {

View File

@@ -87,7 +87,7 @@ Loader {
readOnly: true
wrapMode: Label.Wrap
textFormat: TextEdit.RichText
background: null
background: Item {}
HoverHandler {
cursorShape: textArea.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}

View File

@@ -63,7 +63,7 @@ ColumnLayout {
Layout.preferredWidth: del.label === "custom" ? implicitWidth + Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit * 2
font.family: del.label === "custom" ? "" : 'emoji'
font.family: del.label === "custom" ? Kirigami.Theme.defaultFont.family : 'emoji'
text: del.label === "custom" ? i18n("Custom") : del.label
}

View File

@@ -9,8 +9,8 @@ import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow {
id: root
property alias source: image.source
property string filename
property url localPath
property string blurhash: ""
property int imageWidth: -1
property int imageHeight: -1
@@ -45,8 +45,6 @@ ApplicationWindow {
fillMode: Image.PreserveAspectFit
source: localPath
Image {
anchors.centerIn: parent
width: image.width

View File

@@ -13,7 +13,7 @@ import NeoChat.Component 1.0
Kirigami.PlaceholderMessage {
property var showContinueButton: false
property var showBackButton: false
property string title: i18n("Loading")
property string title: i18n("Loading")
anchors.centerIn: parent
@@ -23,5 +23,6 @@ Kirigami.PlaceholderMessage {
QQC2.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: false
}
}

View File

@@ -44,13 +44,13 @@ LoginStep {
}
validator: RegularExpressionValidator {
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/
regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(\:[0-9]+)?$/
}
}
}
action: Kirigami.Action {
text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue")
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");

View File

@@ -11,6 +11,12 @@ import org.kde.neochat 1.0
QQC2.Popup {
id: _popup
Shortcut {
sequence: "Ctrl+K"
enabled: !Kirigami.Settings.hasPlatformMenuBar
onActivated: _popup.open()
}
onVisibleChanged: {
if (!visible) {
return

View File

@@ -15,47 +15,62 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Control {
id: root
TimelineContainer {
id: audioDelegate
Layout.fillWidth: true
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: Control {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.bubbleMaxWidth
Audio {
id: audio
source: currentRoom.urlToMxcUrl(content.url)
autoLoad: false
}
Audio {
id: audio
source: currentRoom.urlToMxcUrl(content.url)
autoLoad: false
}
contentItem: ColumnLayout {
RowLayout {
ToolButton {
icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
onClicked: {
if (audio.playbackState == Audio.PlayingState) {
audio.pause()
} else {
audio.play()
contentItem: ColumnLayout {
RowLayout {
ToolButton {
icon.name: audio.playbackState == Audio.PlayingState ? "media-playback-pause" : "media-playback-start"
onClicked: {
if (audio.playbackState == Audio.PlayingState) {
audio.pause()
} else {
audio.play()
}
}
}
Label {
text: model.display
}
}
Label {
text: model.display
}
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
// Server doesn't support seeking, so use ProgressBar instead of Slider :(
ProgressBar {
from: 0
to: audio.duration
value: audio.position
}
RowLayout {
visible: audio.hasAudio
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
// Server doesn't support seeking, so use ProgressBar instead of Slider :(
ProgressBar {
from: 0
to: audio.duration
value: audio.position
}
Label {
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
Label {
text: Controller.formatDuration(audio.position) + "/" + Controller.formatDuration(audio.duration)
}
}
}
}

View File

@@ -3,16 +3,24 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TextEdit {
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
TimelineContainer {
id: encryptedDelegate
width: ListView.view.width
innerObject: TextEdit {
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize
selectByMouse: !Kirigami.Settings.isMobile
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
Layout.maximumWidth: encryptedDelegate.bubbleMaxWidth
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
}
}

View File

@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
DelegateChooser {
role: "eventType"
DelegateChoice {
roleValue: "state"
delegate: StateDelegate {}
}
DelegateChoice {
roleValue: "emote"
delegate: MessageDelegate {
isEmote: true
}
}
DelegateChoice {
roleValue: "message"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: "notice"
delegate: MessageDelegate {}
}
DelegateChoice {
roleValue: "image"
delegate: ImageDelegate {}
}
DelegateChoice {
roleValue: "sticker"
delegate: ImageDelegate {
cardBackground: false
}
}
DelegateChoice {
roleValue: "audio"
delegate: AudioDelegate {}
}
DelegateChoice {
roleValue: "video"
delegate: VideoDelegate {}
}
DelegateChoice {
roleValue: "file"
delegate: FileDelegate {}
}
DelegateChoice {
roleValue: "encrypted"
delegate: EncryptedDelegate {}
}
DelegateChoice {
roleValue: "readMarker"
delegate: ReadMarkerDelegate {}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
}

View File

@@ -14,107 +14,17 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
RowLayout {
id: root
property bool openOnFinished: false
TimelineContainer {
id: fileDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
readonly property bool downloaded: progressInfo && progressInfo.completed
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloaded"
when: progressInfo.completed
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: progressInfo.active
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
onClicked: currentRoom.cancelFileTransfer(eventId)
}
},
State {
name: "raw"
when: true
PropertyChanges {
target: downloadButton
onClicked: root.saveFileAs()
}
}
]
Kirigami.Icon {
id: ikon
source: model.fileMimetypeIcon
fallback: "unknown"
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
QQC2.Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
QQC2.Label {
id: sizeLabel
text: Controller.formatByteSize(content.info ? content.info.size : 0)
opacity: 0.7
Layout.fillWidth: true
}
}
QQC2.Button {
id: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
function saveFileAs() {
var dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
const dialog = fileDialog.createObject(QQC2.ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
@@ -123,4 +33,112 @@ RowLayout {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
innerObject: RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: fileDelegate.bubbleMaxWidth
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
states: [
State {
name: "downloaded"
when: progressInfo.completed
PropertyChanges {
target: downloadButton
icon.name: "document-open"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
onClicked: openSavedFile()
}
},
State {
name: "downloading"
when: progressInfo.active
PropertyChanges {
target: sizeLabel
text: i18nc("file download progress", "%1 / %2", Controller.formatByteSize(progressInfo.progress), Controller.formatByteSize(progressInfo.total))
}
PropertyChanges {
target: downloadButton
icon.name: "media-playback-stop"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
onClicked: currentRoom.cancelFileTransfer(eventId)
}
},
State {
name: "raw"
when: true
PropertyChanges {
target: downloadButton
onClicked: fileDelegate.saveFileAs()
}
}
]
Kirigami.Icon {
id: ikon
source: model.fileMimetypeIcon
fallback: "unknown"
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
QQC2.Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
QQC2.Label {
id: sizeLabel
text: Controller.formatByteSize(content.info ? content.info.size : 0)
opacity: 0.7
Layout.fillWidth: true
}
}
QQC2.Button {
id: downloadButton
icon.name: "download"
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
QQC2.ToolTip.visible: hovered
}
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
}

View File

@@ -12,8 +12,14 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Image {
id: img
TimelineContainer {
id: imageDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
property var content: model.content
readonly property bool isAnimated: contentType === "image/gif"
@@ -25,75 +31,92 @@ 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
property bool readonly: false
source: "image://mxc/" + mediaId
innerObject: Image {
id: img
Image {
anchors.fill: parent
source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : ""
visible: parent.status !== Image.Ready
}
Layout.maximumWidth: imageDelegate.bubbleMaxWidth
Layout.maximumHeight: imageDelegate.bubbleMaxWidth / imageDelegate.info.w * imageDelegate.info.h
Layout.preferredWidth: imageDelegate.info.w
Layout.preferredHeight: imageDelegate.info.h
source: model.mediaUrl
fillMode: Image.PreserveAspectFit
ToolTip.text: display
ToolTip.visible: hoverHandler.hovered
HoverHandler {
id: hoverHandler
enabled: img.readonly
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
Image {
anchors.fill: parent
source: content.info["xyz.amorgan.blurhash"] ? ("image://blurhash/" + content.info["xyz.amorgan.blurhash"]) : ""
visible: parent.status !== Image.Ready
}
}
function saveFileAs() {
var dialog = fileDialog.createObject(ApplicationWindow.overlay)
dialog.open()
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId)
}
fillMode: Image.PreserveAspectFit
Component {
id: fileDialog
ToolTip.text: model.display
ToolTip.visible: hoverHandler.hovered
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
HoverHandler {
id: hoverHandler
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
}
function downloadAndOpen()
{
if (downloaded) openSavedFile()
else
{
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
Component {
id: fileDialog
FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
currentRoom.downloadFile(eventId, file)
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onTapped: {
fullScreenImage.createObject(parent, {
filename: eventId,
source: model.mediaUrl,
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h
}).showFullScreen();
}
}
function downloadAndOpen() {
if (downloaded) {
openSavedFile()
} else {
openOnFinished = true
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
}
}
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}
function openSavedFile()
{
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
}
}

View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TimelineContainer {
id: messageDelegate
width: ListView.view.width
property bool isEmote: false
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: RichLabel {
isEmote: messageDelegate.isEmote
Layout.maximumWidth: messageDelegate.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(model, parent.selectedText)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openMessageContext(model, parent.selectedText)
}
}
}

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
width: ListView.view.width - Kirigami.Units.gridUnit
x: Kirigami.Units.gridUnit / 2
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
opacity: 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
}
Timer {
id: makeMeDisapearTimer
interval: Kirigami.Units.humanMoment * 2
onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
}
ListView.onPooled: makeMeDisapearTimer.stop()
ListView.onAdd: {
const view = ListView.view;
if (view.atYEnd) {
makeMeDisapearTimer.start()
}
}
// When the read marker is visible and we are at the end of the list,
// start the makeMeDisapearTimer
Connections {
target: ListView.view
function onAtYEndChanged() {
makeMeDisapearTimer.start();
}
}
ListView.onRemove: {
const view = ListView.view;
if (view.atYEnd) {
// easy case just mark everything as read
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
return;
}
// mark the last visible index
const lastVisibleIdx = lastVisibleIndex();
if (lastVisibleIdx < index) {
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole)
}
}
}

View File

@@ -75,11 +75,10 @@ MouseArea {
Component {
id: textComponent
TextDelegate {
RichLabel {
id: replyText
textMessage: reply.display
textFormat: Text.RichText
hasContextMenu: false
width: Math.min(implicitWidth, bubbleMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}

View File

@@ -18,10 +18,6 @@ TextEdit {
property string textMessage: model.display
property bool spoilerRevealed: !hasSpoiler.test(textMessage)
property bool hasContextMenu: true
signal requestOpenMessageContext()
ListView.onReused: Qt.binding(() => !hasSpoiler.test(textMessage))
Layout.fillWidth: Config.compactLayout
@@ -77,16 +73,4 @@ a{
enabled: !parent.hoveredLink && !spoilerRevealed
onTapped: spoilerRevealed = true
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(model, parent.selectedText)
enabled: hasContextMenu
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: requestOpenMessageContext()
enabled: hasContextMenu
}
}

View File

@@ -10,41 +10,57 @@ import org.kde.kirigami 2.15 as Kirigami
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
RowLayout {
id: row
Control {
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
width: ListView.view.width - Kirigami.Units.largeSpacing - x
height: label.contentHeight
Kirigami.Avatar {
id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignTop
name: author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
}
height: sectionDelegate.height + rowLayout.height
SectionDelegate {
id: sectionDelegate
width: parent.width
anchors.top: parent.top
anchors.leftMargin: Kirigami.Units.smallSpacing
visible: model.showSection
height: visible ? implicitHeight : 0
}
Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.preferredHeight: icon.height
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: "<style>a {text-decoration: none;}</style><a href=\"https://matrix.to/#/" + author.id + "\" style='color: " + author.color + "'>" + currentRoom.htmlSafeMemberName(author.id) + "</a> " + display
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
RowLayout {
id: rowLayout
height: label.contentHeight
width: parent.width
anchors.bottom: parent.bottom
Kirigami.Avatar {
id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignTop
name: author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
}
Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.preferredHeight: icon.height
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${currentRoom.htmlSafeMemberName(author.id)}</a> ${aggregateDisplay}`
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
}
}

View File

@@ -16,19 +16,26 @@ QQC2.ItemDelegate {
id: messageDelegate
default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed
property bool isLoaded
property bool isEmote: false
property bool cardBackground: true
readonly property int bubbleMaxWidth: Config.compactLayout && !Config.showAvatarInTimeline ? width : (Config.compactLayout ? width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, Kirigami.Units.gridUnit * 20))
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !applicationWindow().wideScreen
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight &&
model.author.isLocalUser &&
!applicationWindow().wideScreen &&
!Config.compactLayout
signal saveFileAs()
signal openExternally()
signal replyClicked(string eventID)
Component.onCompleted: {
if (model.isReply && model.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0)))
}
}
topPadding: 0
bottomPadding: 0
background: null
@@ -74,7 +81,9 @@ QQC2.ItemDelegate {
leftMargin: Kirigami.Units.largeSpacing
}
visible: model.showAuthor && Config.showAvatarInTimeline && !showUserMessageOnRight
visible: model.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: model.author.name ?? model.author.displayName
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color

View File

@@ -15,13 +15,19 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
Video {
id: vid
TimelineContainer {
id: videoDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
property bool supportStreaming: true
readonly property int maxWidth: 1000 // TODO messageListView.width
onDownloadedChanged: {
if (downloaded) {
@@ -34,102 +40,104 @@ Video {
}
}
innerObject: Video {
id: vid
readonly property int maxWidth: 1000 // TODO messageListView.width
Layout.maximumWidth: videoDelegate.bubbleMaxWidth
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w
Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h
Layout.preferredWidth: (model.content.info.w === undefined || model.content.info.w > videoDelegate.maxWidth) ? videoDelegate.maxWidth : content.info.w
Layout.preferredHeight: model.content.info.w === undefined ? (videoDelegate.maxWidth * 3 / 4) : (model.content.info.w > videoDelegate.maxWidth ? (model.content.info.h / model.content.info.w * videoDelegate.maxWidth) : model.content.info.h)
loops: MediaPlayer.Infinite
loops: MediaPlayer.Infinite
fillMode: VideoOutput.PreserveAspectFit
fillMode: VideoOutput.PreserveAspectFit
Component.onCompleted: {
if (downloaded) {
source = progressInfo.localPath
} else {
source = currentRoom.urlToMxcUrl(content.url)
onDurationChanged: {
if (!duration) {
vid.supportStreaming = false;
}
}
}
onDurationChanged: {
if (!duration) {
supportStreaming = false;
onErrorChanged: {
if (error != MediaPlayer.NoError) {
vid.supportStreaming = false;
}
}
}
onErrorChanged: {
if (error != MediaPlayer.NoError) {
supportStreaming = false;
Image {
anchors.fill: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
source: model.content.thumbnailMediaId ? "image://mxc/" + model.content.thumbnailMediaId : ""
fillMode: Image.PreserveAspectFit
}
}
Image {
readonly property bool isThumbnail: content.info.thumbnail_info && content.thumbnailMediaId
readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info
anchors.fill: parent
visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError)
source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "")
sourceSize.width: info.w
sourceSize.height: info.h
fillMode: Image.PreserveAspectFit
}
Label {
anchors.centerIn: parent
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white"
text: i18n("Video")
font.pixelSize: 16
padding: 8
background: Rectangle {
radius: Kirigami.Units.smallSpacing
color: "black"
opacity: 0.3
}
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !downloaded
color: "#BB000000"
ProgressBar {
Label {
anchors.centerIn: parent
width: parent.width * 0.8
visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError
color: "white"
text: i18n("Video")
font.pixelSize: 16
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
padding: 8
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
background: Rectangle {
radius: Kirigami.Units.smallSpacing
color: "black"
opacity: 0.3
}
} else {
downloadAndPlay()
}
Rectangle {
anchors.fill: parent
visible: progressInfo.active && !videoDelegate.downloaded
color: "#BB000000"
ProgressBar {
anchors.centerIn: parent
width: parent.width * 0.8
from: 0
to: progressInfo.total
value: progressInfo.progress
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: if (vid.supportStreaming || progressInfo.completed) {
if (vid.playbackState == MediaPlayer.PlayingState) {
vid.pause()
} else {
vid.play()
}
} else {
videoDelegate.downloadAndPlay()
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
function downloadAndPlay() {
if (downloaded) {
if (vid.downloaded) {
playSavedFile()
} else {
playOnFinished = true

View File

@@ -1,6 +1,6 @@
module NeoChat.Component.Timeline
RichLabel 1.0 RichLabel.qml
TimelineContainer 1.0 TimelineContainer.qml
TextDelegate 1.0 TextDelegate.qml
StateDelegate 1.0 StateDelegate.qml
SectionDelegate 1.0 SectionDelegate.qml
ImageDelegate 1.0 ImageDelegate.qml
@@ -8,4 +8,7 @@ FileDelegate 1.0 FileDelegate.qml
VideoDelegate 1.0 VideoDelegate.qml
ReactionDelegate 1.0 ReactionDelegate.qml
AudioDelegate 1.0 AudioDelegate.qml
EncryptedDelegate 1.0 EncryptedDelegate.qml
EncryptedDelegate 1.0 EncryptedDelegate.qml
EventDelegate 1.0 EventDelegate.qml
MessageDelegate 1.0 MessageDelegate.qml
ReadMarkerDelegate 1.0 ReadMarkerDelegate.qml

View File

@@ -52,7 +52,7 @@ Kirigami.OverlaySheet {
onClicked: {
if (avatarMediaId) {
fullScreenImage.createObject(parent, {"filename": displayName, "localPath": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
fullScreenImage.createObject(parent, {"filename": displayName, "source": room.urlToMxcUrl(avatarUrl)}).showFullScreen()
}
}
}

View File

@@ -24,11 +24,13 @@ Labs.MenuBar {
// text: i18nc("menu", "About NeoChat")
// }
Labs.MenuItem {
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
text: i18nc("menu", "Preferences…")
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat...")
text: i18nc("menu", "Configure NeoChat...")
shortcut: StandardKey.Preferences
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml", {}, {
title: i18n("Configure")
})
}
Labs.MenuItem {
text: i18nc("menu", "Quit NeoChat")

View File

@@ -4,67 +4,157 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Page 1.0
/**
* Context menu when clicking on a room in the room list
*/
Menu {
Loader {
id: root
property var room
signal closed()
MenuItem {
id: newWindow
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
Component {
id: regularMenu
Menu {
MenuItem {
id: newWindow
text: i18n("Open in new window")
onTriggered: RoomManager.openWindow(room);
visible: !Kirigami.Settings.isMobile
}
MenuSeparator {
visible: newWindow.visible
}
MenuSeparator {
visible: newWindow.visible
}
MenuItem {
text: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
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)
}
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
MenuItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
MenuItem {
text: i18n("Mark as Read")
onTriggered: room.markAllMessagesAsRead()
}
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
}
MenuItem {
text: i18nc("@action:inmenu", "Copy address to clipboard")
onTriggered: if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
Clipboard.saveText(room.canonicalAlias)
}
}
MenuItem {
text: i18n("Mark as Read")
MenuSeparator {}
onTriggered: room.markAllMessagesAsRead()
}
MenuItem {
text: i18n("Leave Room")
onTriggered: RoomManager.leaveRoom(room)
}
MenuItem {
text: i18nc("@action:inmenu", "Copy address to clipboard")
onTriggered: {
if (room.canonicalAlias.length === 0) {
Clipboard.saveText(room.id)
} else {
Clipboard.saveText(room.canonicalAlias)
onClosed: {
root.closed()
destroy()
}
}
}
MenuSeparator {}
Component {
id: mobileMenu
MenuItem {
text: i18n("Leave Room")
onTriggered: RoomManager.leaveRoom(room)
Kirigami.OverlayDrawer {
id: drawer
height: popupContent.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
rightPadding: 0
bottomPadding: 0
topPadding: 0
parent: applicationWindow().overlay
ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: room.avatarMediaId ? ("image://mxc/" + room.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
Kirigami.Heading {
level: 5
Layout.fillWidth: true
text: room.displayName
wrapMode: Text.WordWrap
}
ToolButton {
checked: room.isFavourite
checkable: true
icon.name: 'favorite'
Accessible.name: room.isFavourite ? i18n("Remove from Favourites") : i18n("Add to Favourites")
onClicked: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
}
ToolButton {
icon.name: 'settings-configure'
onClicked: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}
}
Kirigami.BasicListItem {
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
onClicked: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
Kirigami.BasicListItem {
text: i18n("Mark as Read")
onClicked: room.markAllMessagesAsRead()
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
Kirigami.BasicListItem {
text: i18n("Leave Room")
onClicked: RoomManager.leaveRoom(room)
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
onClosed: root.closed()
}
}
onClosed: destroy()
asynchronous: true
sourceComponent: Kirigami.Settings.isMobile ? mobileMenu : regularMenu
function open() {
active = true;
}
onStatusChanged: if (status == Loader.Ready) {
if (Kirigami.Settings.isMobile) {
item.open();
} else {
item.popup();
}
}
}

View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as Controls
import org.kde.kirigami 2.14 as Kirigami
/**
* Action that allows an user to share data with other apps and service
* installed on their computer. The goal of this high level API is to
* adapte itself for each platform and adopt the native component.
*
* TODO add Android support
*/
Kirigami.Action {
id: shareAction
iconName: "emblem-shared-symbolic"
text: i18n("Share")
tooltip: i18n("Share the selected media")
property var doBeforeSharing: () => {}
visible: false
/**
* This property holds the input data for purpose.
*
* @code{.qml}
* Purpose.ShareAction {
* inputData: {
* 'urls': ['file://home/notroot/Pictures/mypicture.png'],
* 'mimeType': ['image/png']
* }
* }
* @endcode
*/
property var inputData: ({})
property Instantiator _instantiator: Instantiator {
Component.onCompleted: {
const purposeModel = Qt.createQmlObject('import org.kde.purpose 1.0 as Purpose;
Purpose.PurposeAlternativesModel {
pluginType: "Export"
}', shareAction._instantiator);
purposeModel.inputData = Qt.binding(function() {
return shareAction.inputData;
});
_instantiator.model = purposeModel;
shareAction.visible = true;
}
delegate: Kirigami.Action {
property int index
text: model.display
icon.name: model.iconName
onTriggered: {
doBeforeSharing();
applicationWindow().pageStack.pushDialogLayer('qrc:/imports/NeoChat/Menu/ShareDialog.qml', {
title: shareAction.tooltip,
index: index,
model: shareAction._instantiator.model
})
}
}
onObjectAdded: {
object.index = index;
shareAction.children.push(object)
}
onObjectRemoved: shareAction.children = Array.from(shareAction.children).filter(obj => obj.pluginId !== object.pluginId)
}
}

View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import org.kde.kirigami 2.14 as Kirigami
Kirigami.Action {
property var inputData: ({})
property var doBeforeSharing: () => {}
visible: false
}

View File

@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2017 Atul Sharma <atulsharma406@gmail.com>
* SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.15 as Controls
import org.kde.purpose 1.0 as Purpose
import org.kde.notification 1.0
import org.kde.kirigami 2.14 as Kirigami
Kirigami.Page {
id: window
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
property alias index: jobView.index
property alias model: jobView.model
Controls.Action {
shortcut: 'Escape'
onTriggered: window.closeDialog()
}
Notification {
id: sharingFailed
eventId: "sharingFailed"
text: i18n("Sharing failed")
urgency: Notification.NormalUrgency
}
Notification {
id: sharingSuccess
eventId: "sharingSuccess"
flags: Notification.Persistent
}
Component.onCompleted: jobView.start()
contentItem: Purpose.JobView {
id: jobView
onStateChanged: {
if (state === Purpose.PurposeJobController.Finished) {
if (jobView.job.output.url !== "") {
// Show share url
// TODO no needed anymore in purpose > 5.90
sharingSuccess.text = i18n("Shared url for image is <a href='%1'>%1</a>", jobView.output.url);
sharingSuccess.sendEvent();
Clipboard.saveText(jobView.output.url);
}
window.closeDialog()
} else if (state === Purpose.PurposeJobController.Error) {
// Show failure notification
sharingFailed.sendEvent();
window.closeDialog()
} else if (state === Purpose.PurposeJobController.Cancelled) {
// Do nothing
window.closeDialog()
}
}
}
}

View File

@@ -16,6 +16,7 @@ MessageDelegateContextMenu {
required property var file
required property var progressInfo
required property string mimeType
property list<Kirigami.Action> actions: [
Kirigami.Action {
@@ -74,6 +75,27 @@ MessageDelegateContextMenu {
}
}
]
property list<Kirigami.Action> nestedActions: [
ShareAction {
id: shareAction
inputData: {
'urls': [],
'mimeType': [mimeType]
}
property string filename: StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId);
doBeforeSharing: () => {
currentRoom.downloadFile(eventId, filename)
}
Component.onCompleted: {
shareAction.inputData = {
urls: [filename],
mimeType: [mimeType]
};
}
}
]
Component {
id: saveAsDialog
FileDialog {

View File

@@ -21,6 +21,8 @@ Loader {
required property string source
property string selectedText: ""
property list<Kirigami.Action> nestedActions
property list<Kirigami.Action> actions: [
Kirigami.Action {
text: i18n("Edit")
@@ -63,9 +65,36 @@ Loader {
id: regularMenu
QQC2.Menu {
id: menu
Instantiator {
model: loadRoot.nestedActions
delegate: QQC2.Menu {
id: menuItem
visible: modelData.visible
title: modelData.text
Instantiator {
model: modelData.children
delegate: QQC2.MenuItem {
text: modelData.text
icon.name: modelData.icon.name
onTriggered: modelData.trigger()
}
onObjectAdded: {
menuItem.insertItem(0, object)
}
}
}
onObjectAdded: {
object.visible = false;
menu.addMenu(object)
}
}
Repeater {
model: loadRoot.actions
QQC2.MenuItem {
id: menuItem
visible: modelData.visible
action: modelData
onClicked: loadRoot.item.close();
@@ -74,13 +103,15 @@ Loader {
QQC2.Menu {
id: webshortcutmenu
title: i18n("Search for '%1'", webshortcutmodel.trunkatedSearchText)
property bool isVisible: selectedText && selectedText.length > 0 && webshortcutmodel.enabled
Component.onCompleted: webshortcutmenu.parent.visible = isVisible
property bool isVisible: webshortcutmodel.enabled
Component.onCompleted: {
webshortcutmenu.parent.visible = isVisible
}
onIsVisibleChanged: webshortcutmenu.parent.visible = isVisible
Instantiator {
model: WebShortcutModel {
id: webshortcutmodel
selectedText: loadRoot.selectedText
selectedText: loadRoot.selectedText ? loadRoot.selectedText : loadRoot.message
onOpenUrl: RoomManager.visitNonMatrix(url)
}
delegate: QQC2.MenuItem {
@@ -104,7 +135,7 @@ Loader {
Kirigami.OverlayDrawer {
id: drawer
height: popupContent.implicitHeight
height: stackView.implicitHeight
edge: Qt.BottomEdge
padding: 0
leftPadding: 0
@@ -114,85 +145,146 @@ Loader {
parent: applicationWindow().overlay
ColumnLayout {
id: popupContent
QQC2.StackView {
id: stackView
width: parent.width
spacing: 0
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
implicitHeight: currentItem.implicitHeight
Component {
id: nestedActionsComponent
ColumnLayout {
Layout.fillWidth: true
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: currentRoom.htmlSafeMemberName(author.id)
wrapMode: Text.WordWrap
id: actionLayout
property string title: ""
property list<Kirigami.Action> actions
width: parent.width
spacing: 0
RowLayout {
QQC2.ToolButton {
icon.name: 'draw-arrow-back'
onClicked: stackView.pop()
}
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: actionLayout.title
wrapMode: Text.WordWrap
}
}
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
Repeater {
id: listViewAction
model: actionLayout.actions
onLinkActivated: RoomManager.openResource(link);
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
initialItem: ColumnLayout {
id: popupContent
width: parent.width
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
RowLayout {
id: headerLayout
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
Kirigami.Heading {
level: 3
Layout.fillWidth: true
text: currentRoom.htmlSafeMemberName(author.id)
wrapMode: Text.WordWrap
}
QQC2.Label {
text: message
Layout.fillWidth: true
wrapMode: Text.WordWrap
onLinkActivated: RoomManager.openResource(link);
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5
Repeater {
model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.fillHeight: true
contentItem: Kirigami.Heading {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
text: modelData
}
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
loadRoot.item.close();
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: loadRoot.actions
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
currentRoom.toggleReaction(eventId, modelData);
modelData.triggered()
loadRoot.item.close();
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
Repeater {
id: listViewAction
model: loadRoot.actions
Kirigami.BasicListItem {
icon: modelData.icon.name
iconColor: modelData.icon.color ?? undefined
enabled: modelData.enabled
visible: modelData.visible
text: modelData.text
onClicked: {
modelData.triggered()
loadRoot.item.close();
Repeater {
model: loadRoot.nestedActions
Kirigami.BasicListItem {
action: modelData
visible: modelData.visible
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
onClicked: {
stackView.push(nestedActionsComponent, {
title: modelData.text,
actions: modelData.children
});
}
}
implicitHeight: visible ? Kirigami.Units.gridUnit * 3 : 0
}
}
}

View File

@@ -2,3 +2,5 @@ module NeoChat.Menu
RoomListContextMenu 1.0 RoomListContextMenu.qml
GlobalMenu 1.0 GlobalMenu.qml
EditMenu 1.0 EditMenu.qml
ShareAction 1.0 ShareAction.qml
ShareDialog 1.0 ShareDialog.qml

View File

@@ -6,14 +6,14 @@ import QtQuick.Controls 2.12 as QQC2
import org.kde.kirigami 2.12 as Kirigami
Kirigami.Page {
title: i18n("Loading")
title: i18n("Loading")
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
text: i18n("Loading")
text: i18n("Loading")
QQC2.BusyIndicator {
running: loadingIndicator.visible
running: false
Layout.alignment: Qt.AlignHCenter
}
}

View File

@@ -236,12 +236,18 @@ Kirigami.ScrollablePage {
Keys.onReturnPressed: enterRoomAction.trigger()
bold: unreadCount > 0
label: name ?? ""
subtitle: {
let txt = (lastEvent.length === 0 ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm, " ")
if (txt.length) {
return txt
}
return " "
labelItem.textFormat: Text.PlainText
subtitle: subtitleText
subtitleItem.textFormat: Text.PlainText
onPressAndHold: {
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.open()
}
leading: Kirigami.Avatar {
@@ -269,7 +275,7 @@ Kirigami.ScrollablePage {
}
QQC2.Button {
id: configButton
visible: roomListItem.hovered || Kirigami.Settings.isMobile
visible: roomListItem.hovered
Accessible.name: i18n("Configure room")
action: Kirigami.Action {
@@ -283,7 +289,7 @@ Kirigami.ScrollablePage {
configButton.down = undefined
configButton.visible = Qt.binding(function() { return roomListItem.hovered || Kirigami.Settings.isMobile })
})
menu.popup()
menu.open()
}
}
}
@@ -311,6 +317,7 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Layout.fillHeight: true
text: model.connection.localUserId
subtitle: model.connection.localUser.accountLabel
}
}
}

View File

@@ -26,6 +26,10 @@ Kirigami.ScrollablePage {
/// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false;
/// Disable cancel shortcut. Used by the seperate window since it provide its own
/// cancel implementation.
property bool disableCancelShortcut: false
title: currentRoom.htmlSafeDisplayName
KeyNavigation.left: pageStack.get(0)
@@ -37,6 +41,8 @@ Kirigami.ScrollablePage {
if(pageStack.lastItem == page) {
pageStack.pop()
}
} else if (page.currentRoom.isInvite) {
page.currentRoom.clearInvitationNotification();
}
}
}
@@ -55,6 +61,12 @@ Kirigami.ScrollablePage {
connection: Controller.activeConnection
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: applicationWindow().pageStack.get(0).forceActiveFocus()
enabled: !page.disableCancelShortcut
}
Connections {
target: Controller.activeConnection
function onJoinedRoom(room, invited) {
@@ -112,7 +124,7 @@ Kirigami.ScrollablePage {
id: loadingIndicator
anchors.centerIn: parent
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
text: i18n("Loading")
text: i18n("Loading")
QQC2.BusyIndicator {
running: loadingIndicator.visible
Layout.alignment: Qt.AlignHCenter
@@ -229,6 +241,11 @@ Kirigami.ScrollablePage {
}
}
CollapseStateProxyModel {
id: collapseStateProxyModel
sourceModel: sortedMessageEventModel
}
ListView {
id: messageListView
visible: !invitation.visible
@@ -241,7 +258,7 @@ Kirigami.ScrollablePage {
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
model: !isLoaded ? undefined : sortedMessageEventModel
model: !isLoaded ? undefined : collapseStateProxyModel
MessageEventModel {
id: messageEventModel
@@ -341,295 +358,7 @@ Kirigami.ScrollablePage {
sourceModel: messageEventModel
}
delegate: DelegateChooser {
id: timelineDelegateChooser
role: "eventType"
property bool delegateLoaded: true
ListView.onPooled: delegateLoaded = false
ListView.onReused: delegateLoaded = true
DelegateChoice {
roleValue: "state"
delegate: QQC2.Control {
leftPadding: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
topPadding: 0
bottomPadding: 0
height: contentItem.height
contentItem: StateDelegate { }
implicitWidth: messageListView.width - Kirigami.Units.largeSpacing
}
}
DelegateChoice {
roleValue: "emote"
delegate: TimelineContainer {
id: emoteContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
isEmote: true
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: TextDelegate {
isEmote: true
Layout.maximumWidth: emoteContainer.bubbleMaxWidth
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
}
}
}
DelegateChoice {
roleValue: "message"
delegate: TimelineContainer {
id: messageContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: TextDelegate {
Layout.maximumWidth: messageContainer.bubbleMaxWidth
onRequestOpenMessageContext: openMessageContext(model, parent.selectedText)
}
}
}
DelegateChoice {
roleValue: "notice"
delegate: TimelineContainer {
id: noticeContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
innerObject: TextDelegate {
Layout.fillWidth: !Config.compactLayout
hasContextMenu: false
Layout.maximumWidth: noticeContainer.bubbleMaxWidth
}
}
}
DelegateChoice {
roleValue: "image"
delegate: TimelineContainer {
id: imageContainer
isLoaded: timelineDelegateChooser.delegateLoaded
width: messageListView.width
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: ImageDelegate {
Layout.preferredWidth: Kirigami.Units.gridUnit * 15
Layout.maximumWidth: imageContainer.bubbleMaxWidth
Layout.preferredHeight: info.h / info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
onTapped: {
fullScreenImage.createObject(parent, {
filename: eventId,
localPath: currentRoom.urlToDownload(eventId),
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h
}).showFullScreen();
}
}
}
}
}
DelegateChoice {
roleValue: "sticker"
delegate: TimelineContainer {
isLoaded: timelineDelegateChooser.delegateLoaded
width: messageListView.width
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
cardBackground: false
innerObject: ImageDelegate {
readonly: true
Layout.maximumWidth: Kirigami.Units.gridUnit * 10
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.preferredHeight: info.h / info.w * width
}
}
}
DelegateChoice {
roleValue: "audio"
delegate: TimelineContainer {
id: audioContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: AudioDelegate {
Layout.fillWidth: true
Layout.maximumWidth: audioContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
}
}
DelegateChoice {
roleValue: "video"
delegate: TimelineContainer {
id: videoContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
hoverComponent: hoverActions
innerObject: VideoDelegate {
Layout.fillWidth: true
Layout.maximumWidth: videoContainer.bubbleMaxWidth
Layout.preferredHeight: content.info.h / content.info.w * width
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
}
}
DelegateChoice {
roleValue: "file"
delegate: TimelineContainer {
id: fileContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
onReplyClicked: goToEvent(eventID)
innerObject: FileDelegate {
Layout.fillWidth: true
Layout.maximumWidth: fileContainer.bubbleMaxWidth
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
}
}
DelegateChoice {
roleValue: "encrypted"
delegate: TimelineContainer {
id: encryptedContainer
width: messageListView.width
isLoaded: timelineDelegateChooser.delegateLoaded
innerObject: EncryptedDelegate {
Layout.fillWidth: Config.compactLayout
Layout.maximumWidth: encryptedContainer.bubbleMaxWidth
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
}
}
}
DelegateChoice {
roleValue: "readMarker"
delegate: QQC2.ItemDelegate {
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
width: ListView.view.width - Kirigami.Units.gridUnit
x: Kirigami.Units.gridUnit / 2
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
opacity: 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
}
Timer {
id: makeMeDisapearTimer
interval: Kirigami.Units.humanMoment * 2
onTriggered: if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
}
ListView.onPooled: makeMeDisapearTimer.stop()
ListView.onAdd: {
const view = ListView.view;
if (view.atYEnd) {
makeMeDisapearTimer.start()
}
}
// When the read marker is visible and we are at the end of the list,
// start the makeMeDisapearTimer
Connections {
target: ListView.view
function onAtYEndChanged() {
makeMeDisapearTimer.start();
}
}
ListView.onRemove: {
const view = ListView.view;
if (view.atYEnd) {
// easy case just mark everything as read
if (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden) {
currentRoom.markAllMessagesAsRead();
}
return;
}
// mark the last visible index
const lastVisibleIdx = lastVisibleIndex();
if (lastVisibleIdx < index) {
currentRoom.readMarkerEventId = sortedMessageEventModel.data(sortedMessageEventModel.index(lastVisibleIdx, 0), MessageEventModel.EventIdRole)
}
}
}
}
DelegateChoice {
roleValue: "other"
delegate: Item {}
}
}
delegate: EventDelegate {}
QQC2.RoundButton {
anchors.right: parent.right
@@ -644,7 +373,7 @@ Kirigami.ScrollablePage {
visible: currentRoom && currentRoom.hasUnreadMessages && currentRoom.readMarkerLoaded
action: Kirigami.Action {
onTriggered: {
goToEvent(currentRoom.readMarkerEventId)
messageListView.goToEvent(currentRoom.readMarkerEventId)
}
icon.name: "go-up"
}
@@ -678,12 +407,6 @@ Kirigami.ScrollablePage {
}
Component.onCompleted: {
if (currentRoom) {
if (currentRoom.timelineSize < 20) {
currentRoom.getPreviousContent(50);
}
}
positionViewAtBeginning();
}
@@ -752,6 +475,9 @@ Kirigami.ScrollablePage {
}
headerPositioning: ListView.OverlayHeader
function goToEvent(eventID) {
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
}
@@ -833,10 +559,6 @@ Kirigami.ScrollablePage {
messageListView.positionViewAtIndex(0, ListView.End)
}
function goToEvent(eventID) {
messageListView.positionViewAtIndex(eventToIndex(eventID), ListView.Contain)
}
function eventToIndex(eventID) {
const index = messageEventModel.eventIDToIndex(eventID)
if (index === -1)
@@ -872,8 +594,9 @@ Kirigami.ScrollablePage {
author: event.author,
message: event.message,
eventId: event.eventId,
source: event.toolTip,
source: event.source,
file: file,
mimeType: event.mimeType,
progressInfo: event.progressInfo,
});
contextMenu.open();
@@ -884,10 +607,10 @@ Kirigami.ScrollablePage {
const contextMenu = messageDelegateContextMenu.createObject(page, {
selectedText: selectedText,
author: event.author,
message: event.message,
message: event.display,
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.toolTip,
source: event.source,
eventType: event.eventType
});
contextMenu.open();

View File

@@ -12,8 +12,14 @@ Kirigami.ApplicationWindow {
required property var currentRoom
minimumWidth: Kirigami.Units.gridUnit * 10
minimumHeight: Kirigami.Units.gridUnit * 15
Shortcut {
sequence: StandardKey.Cancel
onActivated: window.close()
}
pageStack.initialPage: RoomPage {
visible: true
currentRoom: window.currentRoom
disableCancelShortcut: true
}
}

View File

@@ -37,6 +37,13 @@ Kirigami.ScrollablePage {
}
}
Connections {
target: Controller
function onInitiated() {
pageStack.layers.pop();
}
}
ColumnLayout {
Kirigami.Icon {
source: "org.kde.neochat"

View File

@@ -150,9 +150,15 @@ Kirigami.OverlayDrawer {
font.bold: true
wrapMode: Label.Wrap
text: room ? room.displayName : i18n("No name")
textFormat: Text.PlainText
}
Label {
TextEdit {
Layout.fillWidth: true
textFormat: TextEdit.PlainText
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
readOnly: true
text: room && room.canonicalAlias ? room.canonicalAlias : i18n("No Canonical Alias")
}
}

View File

@@ -8,7 +8,7 @@ import QtQuick.Layouts 1.15
Kirigami.CategorizedSettings {
id: root
required property var room
property var room
objectName: "settingsPage"
actions: [
Kirigami.SettingAction {

View File

@@ -11,14 +11,9 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Kirigami.Page {
Kirigami.ScrollablePage {
title: i18n("Accounts")
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
actions.main: Kirigami.Action {
text: i18n("Add an account")
icon.name: "list-add-user"
@@ -26,82 +21,59 @@ Kirigami.Page {
visible: !pageSettingStack.wideMode
}
Connections {
target: pageSettingStack
function onWideModeChanged() {
scroll.background.visible = pageSettingStack.wideMode
}
}
ListView {
model: AccountRegistry
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
Kirigami.BasicListItem {
anchors.top: parent.top
anchors.bottom: parent.bottom
Controls.ScrollView {
id: scroll
Component.onCompleted: background.visible = pageSettingStack.wideMode
text: model.connection.localUser.displayName
labelItem.textFormat: Text.PlainText
subtitle: model.connection.localUserId
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
anchors.fill: parent
Controls.ScrollBar.horizontal.policy: Controls.ScrollBar.AlwaysOff
ListView {
clip: true
model: AccountRegistry
delegate: Kirigami.SwipeListItem {
leftPadding: 0
rightPadding: 0
Kirigami.BasicListItem {
anchors.top: parent.top
anchors.bottom: parent.bottom
text: model.connection.localUser.displayName
labelItem.textFormat: Text.PlainText
subtitle: model.connection.localUserId
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
onClicked: {
Controller.activeConnection = model.connection
pageStack.layers.pop()
}
onClicked: {
Controller.activeConnection = model.connection
pageStack.layers.pop()
}
actions: [
Kirigami.Action {
text: i18n("Edit this account")
iconName: "document-edit"
onTriggered: {
userEditSheet.connection = model.connection
userEditSheet.open()
}
},
Kirigami.Action {
text: i18n("Logout")
iconName: "im-kick-user"
onTriggered: {
Controller.logout(model.connection, true)
if(Controller.accountCount === 1)
pageStack.layers.pop()
}
}
]
}
}
}
footer: Column {
height: visible ? implicitHeight : 0
Kirigami.ActionToolBar {
alignment: Qt.AlignRight
visible: pageSettingStack.wideMode
rightPadding: Kirigami.Units.smallSpacing
width: parent.width
flat: false
actions: [
Kirigami.Action {
text: i18n("Add an account")
icon.name: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
text: i18n("Edit this account")
iconName: "document-edit"
onTriggered: {
userEditSheet.connection = model.connection
userEditSheet.open()
}
},
Kirigami.Action {
text: i18n("Logout")
iconName: "im-kick-user"
onTriggered: {
Controller.logout(model.connection, true)
if(Controller.accountCount === 1)
pageStack.layers.pop()
}
}
]
}
Item {
}
footer: Controls.ToolBar {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.ActionToolBar {
alignment: Qt.AlignRight
rightPadding: Kirigami.Units.smallSpacing
width: parent.width
height: Kirigami.Units.smallSpacing
flat: false
actions: Kirigami.Action {
text: i18n("Add an account")
icon.name: "list-add-user"
onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml")
}
}
}
Connections {
@@ -167,7 +139,7 @@ Kirigami.Page {
}
}
Controls.Button {
visible: avatar.source.length !== 0
visible: avatar.source.toString().length !== 0
icon.name: "edit-clear"
onClicked: avatar.source = ""
@@ -179,20 +151,28 @@ Kirigami.Page {
text: userEditSheet.connection ? userEditSheet.connection.localUser.displayName : ""
Kirigami.FormData.label: i18n("Name:")
}
Controls.TextField {
id: accountLabel
text: userEditSheet.connection ? userEditSheet.connection.localUser.accountLabel : ""
Kirigami.FormData.label: i18n("Label:")
}
Controls.TextField {
id: currentPassword
Kirigami.FormData.label: i18n("Current Password:")
enabled: userEditSheet.connection !== undefined && userEditSheet.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: newPassword
Kirigami.FormData.label: i18n("New Password:")
enabled: userEditSheet.connection !== undefined && userEditSheet.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: confirmPassword
Kirigami.FormData.label: i18n("Confirm new Password:")
enabled: userEditSheet.connection !== undefined && userEditSheet.connection.canChangePassword !== false
echoMode: TextInput.Password
}
@@ -204,6 +184,8 @@ Kirigami.Page {
showPassiveNotification("The Avatar could not be set")
if(userEditSheet.connection.localUser.displayName !== name.text)
userEditSheet.connection.localUser.rename(name.text)
if(userEditSheet.connection.localUser.accountLabel !== accountLabel.text)
userEditSheet.connection.localUser.setAccountLabel(accountLabel.text)
if(currentPassword.text !== "" && newPassword.text !== "" && confirmPassword.text !== "") {
if(newPassword.text === confirmPassword.text) {
Controller.changePassword(userEditSheet.connection, currentPassword.text, newPassword.text)

View File

@@ -175,6 +175,7 @@ Kirigami.ScrollablePage {
}
}
Kirigami.FormLayout {
Layout.maximumWidth: parent.width
QQC2.CheckBox {
Kirigami.FormData.label: "Show Avatar:"
text: i18n("In Chat")

View File

@@ -9,71 +9,53 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
Kirigami.ScrollablePage {
title: i18n("Devices")
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
Connections {
target: pageSettingStack
function onWideModeChanged() {
scroll.background.visible = pageSettingStack.wideMode
ListView {
model: DevicesModel {
id: devices
}
}
Controls.ScrollView {
id: scroll
Component.onCompleted: background.visible = pageSettingStack.wideMode
anchors.fill: parent
ListView {
clip: true
model: DevicesModel {
id: devices
Kirigami.PlaceholderMessage {
visible: parent.model.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
text: i18n("Loading…")
Controls.BusyIndicator {
running: parent.visible
}
}
Kirigami.PlaceholderMessage {
visible: parent.model.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
text: i18n("Loading")
Controls.BusyIndicator {
running: parent.visible
}
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"
}
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()
}
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()
}
}
]
}
}

View File

@@ -13,92 +13,100 @@ import NeoChat.Settings 1.0
import NeoChat.Component 1.0 as Components
import NeoChat.Dialog 1.0
Kirigami.Page {
Kirigami.ScrollablePage {
title: i18nc('@title:window', 'Custom Emojis')
leftPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
topPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
bottomPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
rightPadding: pageSettingStack.wideMode ? Kirigami.Units.smallSpacing : 0
ListView {
model: CustomEmojiModel {
id: emojiModel
ColumnLayout {
id: column
anchors.fill: parent
Connections {
target: pageSettingStack
function onWideModeChanged() {
scroll.background.visible = pageSettingStack.wideMode
}
connection: Controller.activeConnection
}
QQC2.ScrollView {
id: scroll
Component.onCompleted: background.visible = pageSettingStack.wideMode
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
clip: true
model: CustomEmojiModel {
id: emojiModel
connection: Controller.activeConnection
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("No custom inline stickers found")
visible: parent.model.count === 0
}
delegate: Kirigami.BasicListItem {
id: del
required property string name
required property url imageURL
text: name
reserveSpaceForSubtitle: true
leading: Image {
width: height
sourceSize.width: width
sourceSize.height: height
source: imageURL
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: Components.ShimmerGradient { }
}
}
trailing: QQC2.ToolButton {
width: height
icon.name: "delete"
onClicked: emojiModel.removeEmoji(del.name)
}
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
text: i18n("No custom inline stickers found")
visible: parent.model.count === 0
}
Loader {
active: pageSettingStack.wideMode
sourceComponent: addEmojiComponent
Layout.fillWidth: true
delegate: Kirigami.BasicListItem {
id: del
required property string name
required property url imageURL
text: name
reserveSpaceForSubtitle: true
leading: Image {
width: height
sourceSize.width: width
sourceSize.height: height
source: imageURL
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
radius: height/2
gradient: Components.ShimmerGradient { }
}
}
trailing: QQC2.ToolButton {
width: height
icon.name: "delete"
onClicked: emojiModel.removeEmoji(del.name)
}
}
}
footer: QQC2.ToolBar {
id: toolbar
width: parent.width
visible: !pageSettingStack.wideMode
height: visible ? implicitHeight : 0
contentItem: Loader {
active: !pageSettingStack.wideMode
sourceComponent: addEmojiComponent
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.ActionToolBar {
id: emojiCreator
alignment: Qt.AlignRight
rightPadding: Kirigami.Units.smallSpacing
width: parent.width
flat: false
property string name
actions: [
Kirigami.Action {
displayComponent: QQC2.TextField {
id: emojiField
placeholderText: i18n("new_emoji_name_here")
validator: RegularExpressionValidator {
regularExpression: /[a-zA-Z_0-9]*/
}
onTextChanged: emojiCreator.name = text
}
},
Kirigami.Action {
text: i18n("Add Emoji...")
enabled: emojiCreator.name.length > 0
property var fileDialog: null
icon.name: 'list-add'
onTriggered: {
if (this.fileDialog !== null) {
return;
}
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
this.fileDialog.chosen.connect((url) => {
emojiModel.addEmoji(emojiField.text, url)
this.fileDialog = null
})
this.fileDialog.onRejected.connect(() => {
rej()
this.fileDialog = null
})
this.fileDialog.open()
}
}
]
}
}
@@ -109,48 +117,4 @@ Kirigami.Page {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
}
}
property Component addEmojiComponent: RowLayout {
Item {
Layout.fillWidth: Qt.application.layoutDirection == Qt.LeftToRight
}
QQC2.TextField {
id: emojiField
placeholderText: i18n("new_emoji_name_here")
validator: RegularExpressionValidator {
regularExpression: /[a-zA-Z_0-9]*/
}
}
QQC2.Button {
text: i18n("Add Emoji...")
enabled: emojiField.text != ""
property var fileDialog: null
onClicked: {
if (this.fileDialog != null) {
return;
}
this.fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay)
this.fileDialog.chosen.connect((url) => {
emojiModel.addEmoji(emojiField.text, url)
this.fileDialog = null
})
this.fileDialog.onRejected.connect(() => {
rej()
this.fileDialog = null
})
this.fileDialog.open()
}
}
Item {
Layout.fillWidth: Qt.application.layoutDirection == Qt.RightToLeft
}
}
}

View File

@@ -25,6 +25,16 @@ Kirigami.ScrollablePage {
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Minimize to system tray on startup")
checked: Config.minimizeToSystemTrayOnStartup
visible: Controller.supportSystemTray && !Kirigami.Settings.isMobile
enabled: Config.systemTray && !Config.isMinimizeToSystemTrayOnStartupImmutable
onToggled: {
Config.minimizeToSystemTrayOnStartup = checked
Config.save()
}
}
QQC2.CheckBox {
// TODO: When there are enough notification and timeline event
// settings, make 2 separate groups with FormData labels.
@@ -46,6 +56,24 @@ Kirigami.ScrollablePage {
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show name change events")
checked: Config.showRename
enabled: !Config.isShowRenameImmutable
onToggled: {
Config.showRename = checked
Config.save()
}
}
QQC2.CheckBox {
text: i18n("Show avatar update events")
checked: Config.showAvatarUpdate
enabled: !Config.isShowAvatarUpdateImmutable
onToggled: {
Config.showAvatarUpdate = checked
Config.save()
}
}
QQC2.RadioButton {
Kirigami.FormData.label: i18n("Rooms and private chats:")
text: i18n("Separated")
@@ -66,7 +94,15 @@ Kirigami.ScrollablePage {
}
}
QQC2.CheckBox {
text: i18n("Use s/text/replacement syntax to edit your last message")
id: quickEditCheckbox
Layout.maximumWidth: parent.width
contentItem: QQC2.Label {
text: i18n("Use s/text/replacement syntax to edit your last message")
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
leftPadding: quickEditCheckbox.indicator.width + quickEditCheckbox.spacing
wrapMode: QQC2.Label.Wrap
}
checked: Config.allowQuickEdit
enabled: !Config.isAllowQuickEditImmutable
onToggled: {

View File

@@ -1,13 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1007 B

View File

@@ -10,6 +10,7 @@
<binary>neochat</binary>
</provides>
<name>NeoChat</name>
<name xml:lang="ar">نيوتشات</name>
<name xml:lang="az">NeoChat</name>
<name xml:lang="ca">NeoChat</name>
<name xml:lang="ca-valencia">NeoChat</name>
@@ -39,6 +40,7 @@
<name xml:lang="x-test">xxNeoChatxx</name>
<name xml:lang="zh-CN">NeoChat</name>
<summary>A client for matrix, the decentralized communication protocol</summary>
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
<summary xml:lang="ca">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="ca-valencia">Un client per al Matrix, el protocol de comunicacions descentralitzat</summary>
@@ -68,9 +70,10 @@
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
<description>
<p>NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="ar">نيوتشات هو عميل ماتركس Matrix. يتيح لك إرسال رسائل نصية ومقاطع فيديو وملفات صوتية إلى عائلتك وزملائك وأصدقائك باستخدام بروتوكول ماتركس</p>
<p xml:lang="az">NeoChat Mtrix müştərisidir. O, Matrix protokolundan istifadə edərək, ailənizə, dostlarınıza, iş yoldaşlarınıza mətn, səsli və görüntülü ismarıclar göndərməyə imkan verir.</p>
<p xml:lang="ca">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="ca-valencia">El NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="ca-valencia">NeoChat és un client de Matrix. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics usant el protocol Matrix.</p>
<p xml:lang="de">NeoChat ist ein Matrix-Client. Er ermöglicht Ihnen das Senden von Textnachrichten, Videos und Audiodateien an Ihre Familie, Kollegen und Freunde unter Verwendung des Matrix-Protokolls.</p>
<p xml:lang="en-GB">NeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.</p>
<p xml:lang="es">NeoChat es un cliente para Matrix. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos usando el protocolo Matrix.</p>
@@ -79,6 +82,7 @@
<p xml:lang="fr">NeoChat est un client Matrix. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis en utilisant le protocole Matrix.</p>
<p xml:lang="hu">A NeoChat egy Matrix kliens. Szöveges üzeneteket, videókat ésaudio fájlokat küldhet családjának, kollégáinak és barátainak a Matrix protokoll használatával.</p>
<p xml:lang="ia">NeoChat es un cliente de Matrix. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante le protocollo de Matrix.</p>
<p xml:lang="id">NeoChat adalah sebuah klien Matrix. Memungkinkan Anda untuk mengirim pesan teks, file video dan audio ke keluarga, kolega dan teman Anda menggunakan protokol Matrix.</p>
<p xml:lang="it">NeoChat è un client Matrix. Ti consente di inviare messaggi di testo, file video e audio a familiari, colleghi e amici utilizzando il protocollo Matrix.</p>
<p xml:lang="ko">NeoChat은 Matrix 클라이언트입니다. Matrix 프로토콜을 사용하여 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다.</p>
<p xml:lang="nl">NeoChat is een Matrix-client. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden met het Matrix-protocol.</p>
@@ -92,17 +96,19 @@
<p xml:lang="x-test">xxNeoChat is a Matrix client. It allows you to send text messages, videos and audio files to your family, colleagues and friends using the Matrix protocol.xx</p>
<p xml:lang="zh-CN">NeoChat 是一个 Matrix 客户端。 它允许您使用 Matrix 协议向您的家人、同事和朋友发送文本消息、视频和音频文件。</p>
<p>Matrix is a decentralized communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="ar">ماتريكس هو بروتوكول اتصال لامركزي ، يعيد المستخدم إلى السيطرة. يطبق نيوتشات حاليًا جزءًا كبيرًا من الميفاق باستثناء الدردشات المشفرة ودردشة الفيديو.</p>
<p xml:lang="az">Matrix, istifadəçini nəzarətdə saxlayan, mərkəzləşməmişi rabitə protokoludur. NeoChat, söhbətin və video əlaqəsinin şifrələnməsindən başqa bir çox protokolları həyata keçirə bilir.</p>
<p xml:lang="ca">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment el NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="ca-valencia">Matrix és un protocol de comunicacions descentralitzat, que retorna el control a l'usuari. Actualment NeoChat implementa una gran part del protocol amb l'excepció dels xats encriptats i els xats de vídeo.</p>
<p xml:lang="de">Matrix ist ein dezentralisiertes Kommunikationsprotokoll, das dem Benutzer wieder die Kontrolle zurückgibt. Derzeit implementiert NeoChat einen großen Teil des Protokolls mit der Ausnahme von verschlüsselten Chats und Video-Chat.</p>
<p xml:lang="en-GB">Matrix is a decentralised communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.</p>
<p xml:lang="es">Matrix es un protocolo de comunicaciones descentralizado, que devuelve el control al usuario. En la actualidad, NeoChat implementa gran parte del protocolo con la excepción de chats cifrados y chats de vídeo.</p>
<p xml:lang="eu">Matrix komunikazio-protokolo deszentralizatu bat da, erabiltzaileari kontrola itzultzen diona. Gaur egun, NeoChat-ek protokoloaren zati handi bat inplementatzen du, berriketa zifratuak eta bideo berriketak izan ezik.</p>
<p xml:lang="fi">Matrix on hajautettu viestintäyhteyskäytäntö, joka antaa hallinnan takaisin käyttäjille. NeoChat tarjoaa nykyisellään valtaosan yhteyskäytännöstä salattuja keskustelu- ja videokeskusteluja lukuun ottamatta.</p>
<p xml:lang="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions cryptées et du chat vidéo.</p>
<p xml:lang="fr">Matrix est un protocole de communication décentralisé, donnant le contrôle à l'utilisateur. Actuellement, NeoChat met en œuvre une grande partie du protocole, à l'exception des discussions chiffrées et du chat vidéo.</p>
<p xml:lang="hu">A Matrix egy decentralizált kommunikációs protokoll, amely a felhasználók kezébe adja az irányítást.</p>
<p xml:lang="ia">Matrix es un protocollo de communication decentrate, ponente le usator in le controlo. Currentemente NeoChat implementa un grande parte del protocollo con le exception de conversationes cryptate e conversationes video.</p>
<p xml:lang="id">Matrix adalah protokol komunikasi terdesentralisasi, menempatkan pengguna kembali dalam kendali. Saat ini NeoChat mengimplementasikan sebagian besar protokol dengan pengecualian obrolan terenkripsi dan obrolan video.</p>
<p xml:lang="it">Matrix è un protocollo di comunicazione decentralizzato, che restituisce all'utente il controllo. Attualmente NeoChat implementa gran parte del protocollo ad eccezione delle chat cifrate e delle chat video.</p>
<p xml:lang="ko">Matrix는 사용자에게 제어권을 돌려 주는 분산 통신 프로토콜입니다. NeoChat은 암호화된 대화 및 영상 통화를 제외한 프로토콜의 대부분 기능을 구현합니다.</p>
<p xml:lang="nl">Matrix is een gedecentraliseerd communicatieprotocol, dat de gebruiker de controle teruggeeft. Op dit moment implementeert NeoChat grote delen van het protocol met de uitzondering van versleutelde chats en video-chat.</p>
@@ -116,9 +122,10 @@
<p xml:lang="x-test">xxMatrix is a decentralized communication protocol, putting the user back in control. Currently NeoChat implements large part of the protocol with the exception of encrypted chats and video chat.xx</p>
<p xml:lang="zh-CN">Matrix 是一个分布式通讯协议,使用户重新得到控制权。 目前NeoChat 实现了协议的大部分,除了加密聊天和视频聊天。</p>
<p>NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="ar">يعمل نيوتشات على كل من الأجهزة المحمولة وسطح المكتب مع توفير تجربة مستخدم متسقة.</p>
<p xml:lang="az">Vahid istifadəçi interfeysi ilə təmin olunan NeoChat, həm mobil telefonda həm də kompyuterlərdə işləyir.</p>
<p xml:lang="ca">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">El NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="ca-valencia">NeoChat funciona en els mòbils i a l'escriptori, proporcionant una experiència d'usuari coherent.</p>
<p xml:lang="de">NeoChat funktioniert sowohl auf dem Mobiltelefon als auch auf dem Arbeitsfläche und bietet ein einheitliches Benutzererlebnis. </p>
<p xml:lang="en-GB">NeoChat works both on mobile and desktop while providing a consistent user experience.</p>
<p xml:lang="es">NeoChat funciona en móviles y en el escritorio a la vez que proporciona una experiencia de usuario consistente.</p>
@@ -127,6 +134,7 @@
<p xml:lang="fr">NeoChat fonctionne aussi bien sur les mobiles que sur les ordinateurs de bureau, tout en offrant une expérience utilisateur cohérente.</p>
<p xml:lang="hu">A NeoChat mobilon és asztali számítógépen is működik, egységes felhasználói élményt nyújtva.</p>
<p xml:lang="ia">NeoChat functiona sia sur mobile que ur scriptorio durante que forni un experientia de usator consistente.</p>
<p xml:lang="id">NeoChat berfungsi baik di ponsel dan desktop sambil memberikan pengalaman pengguna yang konsisten.</p>
<p xml:lang="it">NeoChat funziona sia su dispositivi mobili che desktop, fornendo un'esperienza utente coerente.</p>
<p xml:lang="ko">NeoChat은 모바일과 데스크톱 모두에서 일관된 사용자 경험을 제공합니다.</p>
<p xml:lang="nl">NeoChat werkt zowel op de mobiel en het bureaublad met het leveren van een consistente gebruikerservaring.</p>
@@ -146,6 +154,7 @@
<category>Network</category>
</categories>
<developer_name>The KDE Community</developer_name>
<developer_name xml:lang="ar">مجتمع كدي</developer_name>
<developer_name xml:lang="az">KDE Cəmiyyəti</developer_name>
<developer_name xml:lang="ca">La comunitat KDE</developer_name>
<developer_name xml:lang="ca-valencia">La comunitat KDE</developer_name>
@@ -188,6 +197,26 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="22.04" date="2022-04-26">
<url>https://www.plasma-mobile.org/2022/04/26/plasma-mobile-gear-22-04/</url>
<description>
<p>NeoChat now lets you filter and enter a room directly from KRunner (Plasma Search). Aside from that there is also various bug fixes regarding the typing notifications.</p>
</description>
</release>
<release version="22.02" date="2022-02-09">
<description>
<p>NeoChat 22.02 focus on stability and adds a few quality of life improvements</p>
<ul>
<li>Add support for minimizing to system tray on startup</li>
<li>Improved internet connectivity check</li>
<li>Add support for sharing images and files with other apps (Nextcloud, Imgur, ...)</li>
<li>Implement adding labels for account. This allow for an easier organization when using multiple accounts.</li>
<li>Redesign of our config dialogs to follow the new Plasma System Settings style</li>
<li>Fix various others issues and small feature requests. Decreasing the total amount of open issues by 20%.</li>
</ul>
</description>
<url>https://www.plasma-mobile.org/2022/02/09/plasma-mobile-gear-22-02/#neochat</url>
</release>
<release version="21.12" date="2021-12-07">
<description>
<p>NeoChat 21.12 brings lots of new features and fixes</p>

View File

@@ -1,7 +1,9 @@
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
[Desktop Entry]
Version=1.5
Name=NeoChat
Name[ar]=نيوتشات
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
@@ -14,6 +16,7 @@ Name[fi]=NeoChat
Name[fr]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
Name[it]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
@@ -32,6 +35,7 @@ Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat
GenericName=Matrix Client
GenericName[ar]=عميل ماتركس
GenericName[az]=Matrix Müştərisi
GenericName[ca]=Client de Matrix
GenericName[ca@valencia]=Client de Matrix
@@ -44,6 +48,7 @@ GenericName[fi]=Matrix-asiakas
GenericName[fr]=Client « Matrix »
GenericName[hu]=Matrix kliens
GenericName[ia]=Cliente de Matrice
GenericName[id]=Klien Matrix
GenericName[it]=Client Matrix
GenericName[ko]=Matrix 클라이언트
GenericName[lt]=Matrix kliento programą
@@ -62,6 +67,7 @@ GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
Comment=Client for the Matrix protocol
Comment[ar]=عميل لميفاق ماتركس
Comment[az]=Matrix protokolu üçün müştəri
Comment[ca]=Client per al protocol Matrix
Comment[ca@valencia]=Client per al protocol Matrix
@@ -73,6 +79,7 @@ Comment[fi]=Asiakas Matrix-yhteyskäytännölle
Comment[fr]=Client pour le protocole « Matrix »
Comment[hu]=Kliens a Matrix protokollhoz
Comment[ia]=Cliente per le protocollo de Matrix
Comment[id]=Klien untuk protokol Matrix
Comment[it]=Client per il protocollo Matrix
Comment[ko]=Matrix 프로토콜용 클라이언트
Comment[lt]=Matrix protokolo kliento programa
@@ -96,3 +103,4 @@ Terminal=false
Icon=org.kde.neochat
Type=Application
Categories=Network;InstantMessaging;
SingleMainWindow=true

1
org.kde.neochat.tray.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="22" height="22" fill="none" version="1.1" id="svg13" 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="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>

After

Width:  |  Height:  |  Size: 928 B

2020
po/ar/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2151
po/az/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2201
po/ca/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2015
po/ca@valencia/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1998
po/cs/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2101
po/da/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2165
po/de/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2166
po/en_GB/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2197
po/es/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2187
po/eu/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2156
po/fi/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2185
po/fr/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2193
po/hu/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2092
po/ia/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2005
po/id/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2183
po/it/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1990
po/ja/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2080
po/ko/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2195
po/nl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2001
po/nn/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2149
po/pa/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2168
po/pl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2005
po/pt/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2167
po/pt_BR/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2179
po/sk/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2014
po/sl/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2194
po/sv/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2100
po/ta/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2007
po/tok/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2049
po/tr/neochat.po Normal file

File diff suppressed because it is too large Load Diff

2216
po/uk/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1998
po/x-test/neochat.po Normal file

File diff suppressed because it is too large Load Diff

1994
po/zh_CN/neochat.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,11 @@ Kirigami.ApplicationWindow {
property int columnWidth: Kirigami.Units.gridUnit * 13
minimumWidth: Kirigami.Units.gridUnit * 15
minimumHeight: Kirigami.Units.gridUnit * 20
minimumHeight: Kirigami.Units.gridUnit * 15
visible: false // Will be overridden in Component.onCompleted
wideScreen: width > columnWidth * 5
onClosing: Controller.saveWindowGeometry(root)
pageStack.initialPage: LoadingPage {}
pageStack.globalToolBar.canContainHandles: true
@@ -41,7 +40,7 @@ Kirigami.ApplicationWindow {
}
Loader {
active: !Kirigami.Settings.isMobile
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
source: Qt.resolvedUrl("qrc:/imports/NeoChat/Menu/GlobalMenu.qml")
}
@@ -55,21 +54,21 @@ Kirigami.ApplicationWindow {
onTriggered: Controller.saveWindowGeometry(root)
}
onWidthChanged: saveWindowGeometryTimer.restart()
onHeightChanged: saveWindowGeometryTimer.restart()
onXChanged: saveWindowGeometryTimer.restart()
onYChanged: saveWindowGeometryTimer.restart()
Connections {
id: saveWindowGeometryConnections
enabled: false // Disable on startup to avoid writing wrong values if the window is hidden
target: root
Shortcut {
sequence: "Ctrl+K"
onActivated: {
quickView.item.open()
}
function onClosing() { Controller.saveWindowGeometry(root); }
function onWidthChanged() { saveWindowGeometryTimer.restart(); }
function onHeightChanged() { saveWindowGeometryTimer.restart(); }
function onXChanged() { saveWindowGeometryTimer.restart(); }
function onYChanged() { saveWindowGeometryTimer.restart(); }
}
Loader {
id: quickView
active: !Kirigami.Settings.isMobile
sourceComponent: QuickSwitcher { }
}
@@ -154,7 +153,7 @@ Kirigami.ApplicationWindow {
onEnabledChanged: drawerOpen = enabled && !modal
onModalChanged: drawerOpen = !modal
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 && (root.wideScreen || pageStack.currentIndex > 0)
}
readonly property int defaultPageWidth: Kirigami.Units.gridUnit * 17
@@ -172,6 +171,8 @@ Kirigami.ApplicationWindow {
}
pageStack.defaultColumnWidth: pageWidth
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.globalToolBar.showNavigationButtons: pageStack.currentIndex > 0 ? Kirigami.ApplicationHeaderStyle.ShowBackButton : 0
pageStack.columnView.columnResizeMode: shouldUseSidebars ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn
MouseArea {
@@ -250,10 +251,12 @@ Kirigami.ApplicationWindow {
enabled: pageStack.layers.currentItem.title !== i18n("Start a Chat") && Controller.accountCount > 0
},
Kirigami.Action {
text: i18n("Settings")
text: i18n("Configure NeoChat...")
icon.name: "settings-configure"
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml")
enabled: pageStack.layers.currentItem.title !== i18n("Settings")
onTriggered: pageStack.pushDialogLayer("qrc:/imports/NeoChat/Settings/SettingsPage.qml", {}, {
title: i18n("Configure")
})
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat...")
shortcut: StandardKey.Preferences
},
Kirigami.Action {
@@ -271,7 +274,15 @@ Kirigami.ApplicationWindow {
]
}
Component.onCompleted: Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
Component.onCompleted: {
Controller.setBlur(pageStack, Config.blur && !Config.compactLayout);
if (Config.minimizeToSystemTrayOnStartup && !Kirigami.Settings.isMobile && Controller.supportSystemTray && Config.systemTray) {
restoreWindowGeometryConnections.enabled = true; // To restore window size and position
} else {
visible = true;
saveWindowGeometryConnections.enabled = true;
}
}
Connections {
target: Config
function onBlurChanged() {
@@ -338,9 +349,14 @@ Kirigami.ApplicationWindow {
showPassiveNotification(i18n("%1: %2", error, detail));
}
function onShowWindow() {
function onShowWindow(token = null) {
root.showWindow()
root.raise()
if (token && KWindowSystem) {
KWindowSystem.setCurrentXdgActivationToken(basicNotification.xdgActivationToken)
KWindowSystem.activateWindow(root)
} else {
root.raise()
}
}
function onUserConsentRequired(url) {
@@ -349,6 +365,21 @@ Kirigami.ApplicationWindow {
}
}
Connections {
id: restoreWindowGeometryConnections
enabled: false
target: root
function onVisibleChanged() {
if (!visible) {
return;
}
Controller.restoreWindowGeometry(root);
restoreWindowGeometryConnections.enabled = false; // Only restore window geometry for the first time
saveWindowGeometryConnections.enabled = true;
}
}
Connections {
target: Controller.activeConnection
function onDirectChatAvailable(directChat) {

View File

@@ -1,5 +1,7 @@
<RCC>
<qresource prefix="/">
<file alias="icons/org.kde.neochat.svg">org.kde.neochat.svg</file>
<file alias="icons/org.kde.neochat.tray.svg">org.kde.neochat.tray.svg</file>
<file>qml/main.qml</file>
<file>imports/NeoChat/Page/qmldir</file>
<file>imports/NeoChat/Page/LoadingPage.qml</file>
@@ -32,7 +34,7 @@
<file>imports/NeoChat/Component/Timeline/qmldir</file>
<file>imports/NeoChat/Component/Timeline/ReplyComponent.qml</file>
<file>imports/NeoChat/Component/Timeline/StateDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/TextDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/RichLabel.qml</file>
<file>imports/NeoChat/Component/Timeline/TimelineContainer.qml</file>
<file>imports/NeoChat/Component/Timeline/SectionDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/VideoDelegate.qml</file>
@@ -41,6 +43,9 @@
<file>imports/NeoChat/Component/Timeline/FileDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ImageDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/EncryptedDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/EventDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/MessageDelegate.qml</file>
<file>imports/NeoChat/Component/Timeline/ReadMarkerDelegate.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>

5
res_android.qrc Normal file
View File

@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file alias="imports/NeoChat/Menu/ShareAction.qml">imports/NeoChat/Menu/ShareActionAndroid.qml</file>
</qresource>
</RCC>

6
res_desktop.qrc Normal file
View File

@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/">
<file>imports/NeoChat/Menu/ShareAction.qml</file>
<file>imports/NeoChat/Menu/ShareDialog.qml</file>
</qresource>
</RCC>

View File

@@ -36,13 +36,14 @@ add_executable(neochat
blurhash.cpp
blurhashimageprovider.cpp
joinrulesevent.cpp
collapsestateproxymodel.cpp
../res.qrc
)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PRIVATE QUOTIENT_07)
else()
target_sources(neochat PRIVATE accountregistry.cpp)
target_sources(neochat PRIVATE neochataccountregistry.cpp)
endif()
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -61,8 +62,20 @@ if(NOT ANDROID)
target_compile_definitions(neochat PRIVATE -DHAVE_WINDOWSYSTEM)
endif()
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE ../res_desktop.qrc runner.cpp)
target_compile_definitions(neochat PRIVATE -DHAVE_RUNNER)
else()
target_sources(neochat PRIVATE ../res_android.qrc)
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
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 ${QTKEYCHAIN_LIBRARIES} QCoro::QCoro)
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 ${QTKEYCHAIN_LIBRARIES})
if(TARGET QCoro5::Coro)
target_link_libraries(neochat PRIVATE QCoro5::Coro)
else()
target_link_libraries(neochat PRIVATE QCoro::QCoro)
endif()
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
@@ -70,6 +83,7 @@ if(NEOCHAT_FLATPAK)
endif()
if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
"arrow-down"
@@ -113,6 +127,7 @@ if(ANDROID)
)
else()
target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
if(TARGET KF5::DBusAddons)
@@ -125,3 +140,8 @@ if (TARGET KF5::KIOWidgets)
endif()
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
endif()

View File

@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "collapsestateproxymodel.h"
#include "messageeventmodel.h"
#include <KLocalizedString>
bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
!= QLatin1String("state") // If this is not a state, show it
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
!= QLatin1String("state") // If this is the first state in a block, show it. TODO hidden events?
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool() // If it's a new day, show it
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0),
MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
|| sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
}
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
{
if (role == AggregateDisplayRole) {
return aggregateEventToString(mapToSource(index).row());
}
return sourceModel()->data(mapToSource(index), role);
}
QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
{
auto roles = sourceModel()->roleNames();
roles[AggregateDisplayRole] = "aggregateDisplay";
return roles;
}
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
{
QStringList parts;
for (int i = sourceRow; i >= 0; i--) {
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole) != QLatin1String("state") // If it's not a state event
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
|| (i > 0
&& sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
!= sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
) {
break;
}
}
if (!parts.isEmpty()) {
QStringList chunks;
while (!parts.isEmpty()) {
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1) {
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
}
}
QString text = chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and action 3]", ", ");
text += chunks.takeFirst();
}
text += i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
}
return text;
} else {
return {};
}
}

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QPair>
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
class CollapseStateProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum Roles {
AggregateDisplayRole = MessageEventModel::LastRole + 1,
};
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
[[nodiscard]] QString aggregateEventToString(int row) const;
};

View File

@@ -34,7 +34,12 @@
#include <signal.h>
#ifdef QUOTIENT_07
#include "accountregistry.h"
#else
#include "neochataccountregistry.h"
#endif
#include "csapi/account-data.h"
#include "csapi/content-repo.h"
#include "csapi/joining.h"
@@ -60,25 +65,26 @@ using namespace Quotient;
Controller::Controller(QObject *parent)
: QObject(parent)
, m_mgr(new QNetworkConfigurationManager(this))
{
Connection::setRoomType<NeoChatRoom>();
Connection::setUserType<NeoChatUser>();
#ifndef Q_OS_ANDROID
TrayIcon *trayIcon = new TrayIcon(this);
if (NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
QGuiApplication::setQuitOnLastWindowClosed(false);
}
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this, trayIcon]() {
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, [this]() {
if (NeoChatConfig::self()->systemTray()) {
trayIcon->show();
connect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
connect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
} else {
trayIcon->hide();
disconnect(trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
disconnect(m_trayIcon, &TrayIcon::showWindow, this, &Controller::showWindow);
delete m_trayIcon;
m_trayIcon = nullptr;
}
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
});
@@ -115,8 +121,6 @@ Controller::Controller(QObject *parent)
sigaction(sig, &sa, nullptr);
}
#endif
connect(m_mgr, &QNetworkConfigurationManager::onlineStateChanged, this, &Controller::isOnlineChanged);
}
Controller::~Controller()
@@ -262,31 +266,48 @@ void Controller::invokeLogin()
id = accountId;
}
if (!account.homeserver().isEmpty()) {
auto accessToken = loadAccessTokenFromKeyChain(account);
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account);
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, id, this, accessTokenLoadingJob](QKeychain::Job *) {
AccountSettings account{accountId};
QString accessToken;
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
accessToken = accessTokenLoadingJob->binaryData();
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
// No access token from the keychain, try token file
// TODO FIXME this code is racy since the file might have
// already been removed. But since the other code do a blocking
// dbus call, the probability are not high that it will happen.
// loadAccessTokenFromFile is also mostly legacy nowadays
accessToken = loadAccessTokenFromFile(account);
if (accessToken.isEmpty()) {
return;
}
}
Q_EMIT initiated();
auto connection = new Connection(account.homeserver());
connect(connection, &Connection::connected, this, [this, connection, id] {
connection->loadState();
addConnection(connection);
if (connection->userId() == id) {
setActiveConnection(connection);
connectSingleShot(connection, &Connection::syncDone, this, &Controller::initiated);
}
});
connect(connection, &Connection::loginError, this, [this, connection](const QString &error, const QString &) {
if (error == "Unrecognised access token") {
Q_EMIT errorOccured(i18n("Login Failed: Access Token invalid or revoked"));
logout(connection, false);
} else {
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
logout(connection, true);
}
Q_EMIT initiated();
});
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
});
connect(connection, &Connection::networkError, this, [this](const QString &error, const QString &, int, int) {
Q_EMIT errorOccured(i18n("Network Error: %1", error));
});
connection->assumeIdentity(account.userId(), accessToken, account.deviceId());
}
}
if (accounts.isEmpty()) {
@@ -309,48 +330,57 @@ QByteArray Controller::loadAccessTokenFromFile(const AccountSettings &account)
return {};
}
QByteArray Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
QKeychain::ReadPasswordJob *Controller::loadAccessTokenFromKeyChain(const AccountSettings &account)
{
QKeychain::Error error;
QString errorString;
do {
qDebug() << "Reading access token from the keychain for" << account.userId();
QKeychain::ReadPasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
QEventLoop loop;
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
qDebug() << "Reading access token from the keychain for" << account.userId();
auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
job->setKey(account.userId());
if (job.error() == QKeychain::Error::NoError) {
return job.binaryData();
// Handling of errors
connect(job, &QKeychain::Job::finished, this, [this, &account, job]() {
if (job->error() == QKeychain::Error::NoError) {
return;
}
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), i18n("Please make sure that the keychain is opened."));
error = job.error();
errorString = job.errorString();
} while (error == QKeychain::Error::OtherError);
qWarning() << "Could not read the access token from the keychain:" << errorString;
// no access token from the keychain, try token file
auto accessToken = loadAccessTokenFromFile(account);
if (error == QKeychain::Error::EntryNotFound) {
if (!accessToken.isEmpty()) {
qDebug() << "Migrating the access token from file to the keychain for " << account.userId();
bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken);
if (saved) {
QFile accountTokenFile{accessTokenFileName(account)};
removed = accountTokenFile.remove();
}
if (!(saved && removed)) {
qDebug() << "Migrating the access token from the file to the keychain "
"failed";
if (job->error() == QKeychain::Error::EntryNotFound) {
// no access token from the keychain, try token file
auto accessToken = loadAccessTokenFromFile(account);
if (!accessToken.isEmpty()) {
qDebug() << "Migrating the access token from file to the keychain for " << account.userId();
bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken);
if (saved) {
QFile accountTokenFile{accessTokenFileName(account)};
removed = accountTokenFile.remove();
}
if (!(saved && removed)) {
qDebug() << "Migrating the access token from the file to the keychain "
"failed";
}
return;
}
}
}
return accessToken;
switch (job->error()) {
case QKeychain::EntryNotFound:
Q_EMIT globalErrorOccured(i18n("Access token wasn't found"), i18n("Maybe it was deleted?"));
break;
case QKeychain::AccessDeniedByUser:
case QKeychain::AccessDenied:
Q_EMIT globalErrorOccured(i18n("Access to keychain was denied."), i18n("Please allow NeoChat to read the access token"));
break;
case QKeychain::NoBackendAvailable:
Q_EMIT globalErrorOccured(i18n("No keychain available."), i18n("Please install a keychain, e.g. KWallet or GNOME keyring on Linux"));
break;
case QKeychain::OtherError:
Q_EMIT globalErrorOccured(i18n("Unable to read access token"), job->errorString());
break;
default:
break;
}
});
job->start();
return job;
}
bool Controller::saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken)
@@ -388,16 +418,6 @@ bool Controller::saveAccessTokenToKeyChain(const AccountSettings &account, const
return true;
}
void Controller::playAudio(const QUrl &localFile)
{
auto player = new QMediaPlayer;
player->setMedia(localFile);
player->play();
connect(player, &QMediaPlayer::stateChanged, [player] {
player->deleteLater();
});
}
void Controller::changeAvatar(Connection *conn, const QUrl &localFile)
{
auto job = conn->uploadFile(localFile.toLocalFile());
@@ -547,9 +567,26 @@ void Controller::setActiveConnection(Connection *connection)
if (connection == m_connection) {
return;
}
if (m_connection != nullptr) {
disconnect(connection, &Connection::syncError, this, nullptr);
}
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &Connection::networkError, this, [this](QString message, QString details, int retriesTaken, int nextRetryInMilliseconds) {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &Connection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
} else {
NeoChatConfig::self()->setActiveConnection(QString());
}
@@ -557,6 +594,14 @@ void Controller::setActiveConnection(Connection *connection)
Q_EMIT activeConnectionChanged();
}
void Controller::restoreWindowGeometry(QQuickWindow *window)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
KConfigGroup windowGroup(&dataResource, "Window");
KWindowConfig::restoreWindowSize(window, windowGroup);
KWindowConfig::restoreWindowPosition(window, windowGroup);
}
void Controller::saveWindowGeometry(QQuickWindow *window)
{
KConfig dataResource("data", KConfig::SimpleConfig, QStandardPaths::AppDataLocation);
@@ -588,7 +633,7 @@ void Controller::createRoom(const QString &name, const QString &topic)
bool Controller::isOnline() const
{
return m_mgr->isOnline();
return m_isOnline;
}
// TODO: Remove in favor of RoomManager::joinRoom

View File

@@ -3,7 +3,6 @@
#pragma once
#include <QMediaPlayer>
#include <QObject>
#include <QQuickItem>
@@ -11,7 +10,6 @@
#include <KFormat>
class QKeySequences;
class QNetworkConfigurationManager;
#include "connection.h"
#include "csapi/list_public_rooms.h"
@@ -21,8 +19,14 @@ class QNetworkConfigurationManager;
class NeoChatRoom;
class NeoChatUser;
class TrayIcon;
class QQuickWindow;
namespace QKeychain
{
class ReadPasswordJob;
}
using namespace Quotient;
class Controller : public QObject
@@ -97,12 +101,14 @@ private:
QPointer<Connection> m_connection;
bool m_busy = false;
TrayIcon *m_trayIcon = nullptr;
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
QKeychain::ReadPasswordJob *loadAccessTokenFromKeyChain(const AccountSettings &account);
void loadSettings();
void saveSettings() const;
bool m_isOnline = true;
KAboutData m_aboutData;
bool hasWindowSystem() const;
@@ -135,13 +141,10 @@ Q_SIGNALS:
public Q_SLOTS:
void logout(Quotient::Connection *conn, bool serverSideLogout);
static void playAudio(const QUrl &localFile);
void changeAvatar(Quotient::Connection *conn, const QUrl &localFile);
static void markAllMessagesAsRead(Quotient::Connection *conn);
void restoreWindowGeometry(QQuickWindow *);
void saveWindowGeometry(QQuickWindow *);
private:
QNetworkConfigurationManager *m_mgr;
};
// TODO libQuotient 0.7: Drop

View File

@@ -6,10 +6,12 @@
#include <QFontDatabase>
#include <QGuiApplication>
#include <QIcon>
#include <QNetworkAccessManager>
#include <QNetworkProxy>
#include <QNetworkProxyFactory>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQmlNetworkAccessManagerFactory>
#include <QQuickStyle>
#include <QQuickWindow>
@@ -22,6 +24,9 @@
#include <KAboutData>
#ifdef HAVE_KDBUSADDONS
#include <KDBusService>
#endif
#ifdef HAVE_WINDOWSYSTEM
#include <kwindowsystem_version.h>
#include <KWindowSystem>
#endif
#include <KLocalizedContext>
@@ -30,12 +35,18 @@
#include "neochat-version.h"
#ifdef QUOTIENT_07
#include "accountregistry.h"
#else
#include "neochataccountregistry.h"
#endif
#include "actionshandler.h"
#include "blurhashimageprovider.h"
#include "chatboxhelper.h"
#include "chatdocumenthandler.h"
#include "clipboard.h"
#include "collapsestateproxymodel.h"
#include "commandmodel.h"
#include "controller.h"
#include "csapi/joining.h"
@@ -52,6 +63,7 @@
#include "neochatconfig.h"
#include "neochatroom.h"
#include "neochatuser.h"
#include "networkaccessmanager.h"
#include "notificationsmanager.h"
#include "publicroomlistmodel.h"
#include "roomlistmodel.h"
@@ -66,17 +78,33 @@
#include "colorschemer.h"
#endif
#ifdef HAVE_RUNNER
#include "runner.h"
#include <QDBusConnection>
#endif
using namespace Quotient;
#ifdef HAVE_KDBUSADDONS
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
QNetworkAccessManager *create(QObject *) override
{
return NetworkAccessManager::instance();
}
};
#ifdef HAVE_WINDOWSYSTEM
static void raiseWindow(QWindow *window)
{
#if KWINDOWSYSTEM_VERSION >= QT_VERSION_CHECK(5, 91, 0)
KWindowSystem::updateStartupId(window);
#else
if (KWindowSystem::isPlatformWayland()) {
KWindowSystem::setCurrentXdgActivationToken(qEnvironmentVariable("XDG_ACTIVATION_TOKEN"));
KWindowSystem::activateWindow(window->winId());
} else {
window->raise();
}
#endif
KWindowSystem::activateWindow(window->winId());
window->raise();
}
#endif
@@ -125,6 +153,7 @@ int main(int argc, char *argv[])
about.addAuthor(i18n("Carl Schwan"), QString(), QStringLiteral("carl@carlschwan.eu"));
about.addAuthor(i18n("Tobias Fella"), QString(), QStringLiteral("fella@posteo.de"));
about.setOrganizationDomain("kde.org");
about.setBugAddress("https://invent.kde.org/network/neochat/issues");
about.addComponent(QStringLiteral("libQuotient"),
i18n("A Qt5 library to write cross-platform clients for Matrix"),
@@ -135,20 +164,6 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about);
QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.neochat")));
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
service.connect(&service, &KDBusService::activateRequested, &RoomManager::instance(), [](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().openResource(arg);
}
});
#endif
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
@@ -189,6 +204,7 @@ int main(int argc, char *argv[])
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
@@ -209,12 +225,19 @@ int main(int argc, char *argv[])
qRegisterMetaType<GetRoomEventsJob *>("GetRoomEventsJob*");
qRegisterMetaType<QMimeType>("QMimeType");
#ifdef HAVE_WINDOWSYSTEM
qmlRegisterSingletonType<KWindowSystem>("org.kde.kwindowsystem.private", 1, 0, "KWindowSystem", [](QQmlEngine *, QJSEngine *) -> QObject * {
return KWindowSystem::self();
});
#endif
qRegisterMetaTypeStreamOperators<Emoji>();
QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
KLocalizedString::setApplicationDomain("neochat");
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
@@ -239,29 +262,50 @@ int main(int argc, char *argv[])
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);
}
#ifdef HAVE_RUNNER
Runner runner;
QDBusConnection::sessionBus().registerObject("/RoomRunner", &runner, QDBusConnection::ExportScriptableContents);
#endif
#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) {
auto view = qobject_cast<QQuickWindow *>(obj);
if (view) {
view->show();
raiseWindow(view);
return;
}
}
});
KDBusService service(KDBusService::Unique);
service.connect(&service,
&KDBusService::activateRequested,
&RoomManager::instance(),
[&engine](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(workingDirectory);
// Raise windows
const auto rootObjects = engine.rootObjects();
for (auto obj : rootObjects) {
auto view = qobject_cast<QQuickWindow *>(obj);
if (view) {
view->show();
raiseWindow(view);
return;
}
}
// Open matrix uri
if (arguments.isEmpty()) {
return;
}
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().openResource(arg);
}
});
#endif
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);
if (view->isVisible()) {
Controller::instance().restoreWindowGeometry(view);
}
break;
}
}
#endif
return app.exec();
}

View File

@@ -5,6 +5,7 @@
#include "neochatconfig.h"
#include <connection.h>
#include <csapi/rooms.h>
#include <events/reactionevent.h>
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
@@ -15,6 +16,7 @@
#include "stickerevent.h"
#include <QDebug>
#include <QGuiApplication>
#include <QQmlEngine> // for qmlRegisterType()
#include <QTimeZone>
@@ -40,13 +42,19 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
roles[FileMimetypeIcon] = "fileMimetypeIcon";
roles[AnnotationRole] = "annotation";
roles[EventResolvedTypeRole] = "eventResolvedType";
roles[IsReplyRole] = "isReply";
roles[ReplyRole] = "reply";
roles[ReplyIdRole] = "replyId";
roles[UserMarkerRole] = "userMarker";
roles[ShowAuthorRole] = "showAuthor";
roles[ShowSectionRole] = "showSection";
roles[ReactionRole] = "reaction";
roles[IsEditedRole] = "isEdited";
roles[SourceRole] = "source";
roles[MimeTypeRole] = "mimeType";
roles[FormattedBodyRole] = "formattedBody";
roles[AuthorIdRole] = "authorId";
roles[MediaUrlRole] = "mediaUrl";
return roles;
}
@@ -58,20 +66,8 @@ MessageEventModel::MessageEventModel(QObject *parent)
qmlRegisterAnonymousType<FileTransferInfo>("org.kde.neochat", 1);
qRegisterMetaType<FileTransferInfo>();
QTimer::singleShot(0, this, [this]() {
if (!m_currentRoom) {
return;
}
m_currentRoom->getPreviousContent(50);
connect(this, &QAbstractListModel::rowsInserted, this, [this]() {
if (m_currentRoom->readMarkerEventId().isEmpty()) {
return;
}
const auto it = m_currentRoom->findInTimeline(m_currentRoom->readMarkerEventId());
if (it == m_currentRoom->historyEdge()) {
m_currentRoom->getPreviousContent(50);
}
});
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyRole});
});
}
@@ -92,7 +88,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
if (room) {
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
room->setDisplayed();
if (m_currentRoom->timelineSize() < 10) {
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
room->getPreviousContent(50);
}
lastReadEventId = room->readMarkerEventId();
@@ -423,8 +419,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
}
case SpecialMarksRole:
return EventStatus::Hidden;
}
return {};
}
@@ -458,7 +452,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return m_currentRoom->eventToString(evt);
}
if (role == Qt::ToolTipRole) {
if (role == SourceRole) {
return evt.originalJson();
}
@@ -545,6 +539,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return e->content()->fileInfo()->mimeType.iconName();
}
if (role == MimeTypeRole) {
auto e = eventCast<const RoomMessageEvent>(&evt);
if (!e || !e->hasFileContent()) {
return QVariant();
}
return e->content()->fileInfo()->mimeType.name();
}
if (role == SpecialMarksRole) {
if (isPending) {
return pendingIt->deliveryStatus();
@@ -629,19 +632,35 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return variantList;
}
if (role == IsReplyRole) {
return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
}
if (role == ReplyIdRole) {
return evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
}
if (role == ReplyRole) {
const QString &replyEventId = evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
if (replyEventId.isEmpty()) {
return {};
};
const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
if (replyIt == m_currentRoom->historyEdge()) {
const RoomEvent *replyPtr = replyIt != m_currentRoom->historyEdge() ? &**replyIt : nullptr;
if (!replyPtr) {
for (const auto &e : m_extraEvents) {
if (e->id() == replyEventId) {
replyPtr = e.get();
break;
}
}
}
if (!replyPtr) {
return {};
};
const auto &replyEvt = **replyIt;
}
QString type;
if (auto e = eventCast<const RoomMessageEvent>(&replyEvt)) {
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
switch (e->msgtype()) {
case MessageEventType::Emote:
type = "emote";
@@ -666,29 +685,29 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
type = "message";
}
} else if (is<const StickerEvent>(replyEvt)) {
} else if (is<const StickerEvent>(*replyPtr)) {
type = "sticker";
} else {
type = "other";
}
QVariant content;
if (auto e = eventCast<const RoomMessageEvent>(&replyEvt)) {
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
// Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base
content = e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant();
};
if (auto e = eventCast<const StickerEvent>(&replyEvt)) {
if (auto e = eventCast<const StickerEvent>(replyPtr)) {
content = QVariant::fromValue(e->image().originalJson);
}
return QVariantMap{{"eventId", replyEventId},
{"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)},
{"display", m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
{"content", content},
{"type", type},
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}};
{"author", userAtEvent(static_cast<NeoChatUser *>(m_currentRoom->user(replyPtr->senderId())), m_currentRoom, evt)}};
}
if (role == ShowAuthorRole) {
@@ -750,6 +769,26 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return res;
}
if (role == AuthorIdRole) {
return evt.senderId();
}
if (role == MediaUrlRole) {
#ifdef QUOTIENT_07
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if (!e->hasFileContent()) {
return QVariant();
}
if (e->content()->originalJson.contains(QStringLiteral("file")) && e->content()->originalJson["file"].toObject().contains(QStringLiteral("url"))) {
return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["file"]["url"].toString());
}
if (e->content()->originalJson.contains(QStringLiteral("url"))) {
return m_currentRoom->makeMediaUrl(e->id(), e->content()->originalJson["url"].toString());
}
}
#endif
return m_currentRoom->urlToDownload(evt.id());
}
return {};
}
@@ -838,3 +877,13 @@ QVariant MessageEventModel::getLatestMessageFromIndex(const int baseline)
}
return replyResponse;
}
void MessageEventModel::loadReply(const QModelIndex &index)
{
auto job = m_currentRoom->connection()->callApi<GetOneRoomEventJob>(m_currentRoom->id(), data(index, ReplyIdRole).toString());
QPersistentModelIndex persistentIndex(index);
connect(job, &BaseJob::success, this, [this, job, persistentIndex] {
m_extraEvents.push_back(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData()));
Q_EMIT dataChanged(persistentIndex, persistentIndex, {ReplyRole});
});
}

View File

@@ -30,9 +30,12 @@ public:
UserMarkerRole,
FormattedBodyRole,
MimeTypeRole,
FileMimetypeIcon,
IsReplyRole,
ReplyRole,
ReplyIdRole,
ShowAuthorRole,
ShowSectionRole,
@@ -40,9 +43,12 @@ public:
ReactionRole,
IsEditedRole,
SourceRole,
MediaUrlRole,
// For debugging
EventResolvedTypeRole,
AuthorIdRole,
LastRole, // Keep this last
};
Q_ENUM(EventRoles)
@@ -62,6 +68,7 @@ public:
Q_INVOKABLE [[nodiscard]] int eventIDToIndex(const QString &eventID) const;
Q_INVOKABLE [[nodiscard]] QVariant getLastLocalUserMessageEventId();
Q_INVOKABLE [[nodiscard]] QVariant getLatestMessageFromIndex(const int baseline);
Q_INVOKABLE void loadReply(const QModelIndex &row);
private Q_SLOTS:
int refreshEvent(const QString &eventId);
@@ -86,6 +93,8 @@ private:
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
void moveReadMarker(const QString &toEventId);
std::vector<event_ptr_tt<RoomEvent>> m_extraEvents;
Q_SIGNALS:
void roomChanged();
void fancyEffectsReasonFound(const QString &fancyEffect);

View File

@@ -6,6 +6,15 @@
#include "messageeventmodel.h"
#include "neochatconfig.h"
MessageFilterModel::MessageFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLeaveJoinEventChanged, this, [this] {
beginResetModel();
endResetModel();
});
}
bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);

View File

@@ -9,5 +9,6 @@ class MessageFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
MessageFilterModel(QObject *parent = nullptr);
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};

View File

@@ -1,6 +1,7 @@
[Global]
IconName=org.kde.neochat
Name=NeoChat
Name[ar]=نيوتشات
Name[az]=NeoChat
Name[ca]=NeoChat
Name[ca@valencia]=NeoChat
@@ -13,6 +14,7 @@ Name[fi]=NeoChat
Name[fr]=NeoChat
Name[hu]=NeoChat
Name[ia]=Neochat
Name[id]=NeoChat
Name[it]=NeoChat
Name[ko]=NeoChat
Name[lt]=NeoChat
@@ -32,9 +34,11 @@ Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat
DesktopEntry=org.kde.neochat
Comment=A client for matrix, the decentralized communication protocol
Comment[ar]=عميل لماتركس، ميفاق الاتصال اللامركزي
Comment[az]=Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu
Comment[ca]=Un client per a Matrix, el protocol de comunicacions descentralitzat
Comment[ca@valencia]=Un client per a Matrix, el protocol de comunicacions descentralitzat
Comment[cs]=Klient pro decentralizovaný komunikační protokol matrix
Comment[de]=Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll
Comment[en_GB]=A client for matrix, the decentralised communication protocol
Comment[es]=Un cliente para Matrix, el protocolo de comunicaciones descentralizado
@@ -43,6 +47,7 @@ Comment[fi]=Hajautetun Matrix-viestintäyhteyskäytännön asiakasohjelma
Comment[fr]=Un client pour « Matrix », le protocole décentralisé de communications.
Comment[hu]=Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz
Comment[ia]=Un cliente per Matrix, le protocollo de communication decentralisate
Comment[id]=Sebuah klien untuk matrix, protokol komunikasi terdecentralisasi
Comment[it]=Un client per matrix, il protocollo di comunicazione decentralizzato
Comment[ko]=Matrix, 분산 대화 프로토콜 클라이언트
Comment[lt]=Matrix decentralizuoto bendravimo protokolo kliento programa
@@ -63,6 +68,7 @@ Comment[zh_CN]=分布式通讯协议 Matrix 的客户端
[Event/message]
Name=New message
Name[ar]=رسالة جديدة
Name[az]=Yeni ismarıc
Name[ca]=Missatge nou
Name[ca@valencia]=Missatge nou
@@ -75,6 +81,7 @@ Name[fi]=Uusi viesti
Name[fr]=Nouveau message
Name[hu]=Új üzenet
Name[ia]=Nove message
Name[id]=Pesan baru
Name[it]=Nuovo messaggio
Name[ko]=새 메시지
Name[lt]=Nauja žinutė
@@ -93,6 +100,7 @@ Name[uk]=Нове повідомлення
Name[x-test]=xxNew messagexx
Name[zh_CN]=新消息
Comment=There is a new message
Comment[ar]=توجد رسالة جديدة
Comment[az]=Yeni ismarıc var
Comment[ca]=Hi ha un missatge nou
Comment[ca@valencia]=Hi ha un missatge nou
@@ -104,6 +112,7 @@ Comment[fi]=Saapui uusi viesti
Comment[fr]=Il y a un nouveau message
Comment[hu]=Új üzenet érkezett
Comment[ia]=Il ha un nove message
Comment[id]=Ada pesan baru
Comment[it]=È presente un nuovo messaggio
Comment[ko]=새 메시지가 있음
Comment[lt]=Yra nauja žinutė
@@ -125,15 +134,22 @@ Action=Popup
[Event/invite]
Name=New Invitation
Name[ar]=دعوة جديدة
Name[az]=Yeni dəvət
Name[ca]=Invitació nova
Name[ca@valencia]=Invitació nova
Name[cs]=Nová pozvánka
Name[de]=Neue Einladung
Name[en_GB]=New Invitation
Name[es]=Nueva invitación
Name[fi]=Uusi kutsu
Name[fr]=Nouvelle invitation
Name[ia]=Nove invitation
Name[id]=Undangan Baru
Name[it]=Nuovo invito
Name[ko]=새 초대장
Name[nl]=Nieuwe uitnodiging
Name[pa]=ਨਵਾਂ ਸੱਦਾ
Name[pl]=Nowe zaproszenie
Name[pt]=Novo Convite
Name[pt_BR]=Novo convite
@@ -142,15 +158,22 @@ Name[sv]=Ny inbjudan
Name[uk]=Нове запрошення
Name[x-test]=xxNew Invitationxx
Comment=There is a new invitation to a room
Comment[ar]=توجد دعوة جديدة
Comment[az]=Otağa bir yeni dəvət var
Comment[ca]=Hi ha una invitació nova a una sala
Comment[ca@valencia]=Hi ha una invitació nova a una sala
Comment[cs]=Máte novou pozvánku do místnosti
Comment[de]=Es gibt eine neue Einladung zu einem Raum
Comment[en_GB]=There is a new invitation to a room
Comment[es]=Hay una nueva invitación a una sala
Comment[fi]=Uusi kutsu huoneeseen
Comment[fr]=Il y a une nouvelle invitation dans un salon.
Comment[ia]=Il ha un nove invitation a un sala
Comment[id]=Ada undangan baru ke sebuah ruangan
Comment[it]=È presente un nuovo invito a una stanza
Comment[ko]=새로운 대화방 초대장을 받음
Comment[nl]=Er is een nieuwe uitnodiging naar een room
Comment[pa]=ਰੂਮ ਲਈ ਨਵਾਂ ਸੱਦਾ ਹੈ
Comment[pl]=Dostępna jest nowe zaproszenie do pokoju
Comment[pt]=Existe um novo convite para uma sala
Comment[pt_BR]=Existe um novo convite para uma sala

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