Compare commits

...

93 Commits

Author SHA1 Message Date
James Graham
27455626fc Change the input field text being set after posting message to not use a Qt.binding fucntion so that the the same room in two windows doesn't get the textfields bound together. 2022-08-27 15:11:46 +01:00
l10n daemon script
696b34e094 GIT_SILENT Sync po/docbooks with svn 2022-08-27 01:57:39 +00:00
l10n daemon script
6ce74bbd0c GIT_SILENT made messages (after extraction) 2022-08-27 00:47:18 +00:00
Carl Schwan
3c7e85fbbf Fix loading state detection in devices page
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2022-08-26 23:39:05 +02:00
Carl Schwan
6c6e408497 Cleanup account page
- Fix small padding on the right when they is not enough elements for a
  scrollbar to appear (possibly due to a kirigami regression)
- Move account editor to a seperate page
- Cleanup a bit the code style
- Add tooltip to toolbuttons

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2022-08-26 23:29:29 +02:00
Jan Bidler
1ac79273b6 Fix emoji add button being unavailable 2022-08-26 20:35:09 +00:00
Tobias Fella
d4d99284cc Correctly hide spoiler links
BUG: 458311
2022-08-26 22:08:22 +02:00
Tobias Fella
25226aa61f Don't save connection state when destructing the controller
This causes neochat to crash and is done automatically by the connections

BUG: 458353
2022-08-26 22:00:38 +02:00
James Graham
6748a2d21d Fix the reply an edit text being shown in all chat rooms when multiple windows are open
Fix the reply an edit text being shown in all chat rooms when multiple windows are open. This is done by changing chatBoxHelper from a singleton to being instantiated for each instance of roompage.

BUG: 454963
2022-08-26 19:33:20 +00:00
Carl Schwan
fdb424e65e Improve contrast of new notification background
This is required to be accessible

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2022-08-26 21:24:07 +02:00
Jan Bidler
8cd0b12c4a Make typing indocator anchor on the left 2022-08-26 19:09:45 +00:00
James Graham
d133c4fab7 Improve showAuthor behaviour
2 changes to showAuthor role; the first removes the same eventRole condition as currently a video or image followed by text will both show the avatar, second is to fix it so that the avatar is actually shown if messages are 10 mins apart as this is currently broken.
2022-08-26 19:00:02 +00:00
Jan Bidler
2f8303348b Make NeoChat room a link 2022-08-26 18:43:56 +00:00
Tobias Fella
7dfac8a9f7 Fix audio playback
Since the preparation for encrypted events landed, audio playback was broken since QtMultimedia (gstreamer)
doesn't use our custom QNAM. Instead of letting QtMultimedia download the media, we thus need to manually download it
and point QML Audio to the local file. On the positive side, this also allows encrypted Audio files to be played and enables us to seek in the audio delegate :)
It does however mean that we do need to do an annoying bit of manual state management.

BUG: 457687
2022-08-26 18:37:18 +00:00
Jan Bidler
3c98a8fac4 Make clearing reply not remove your text 2022-08-26 18:29:19 +00:00
Jan Bidler
57b1bb659c Remove normal PageUp and PageDown from switching rooms 2022-08-26 18:22:17 +00:00
Jan Bidler
da8c8c48bc Fix CTRL+Page moving into wrong direction 2022-08-26 18:22:17 +00:00
Tobias Fella
07c5cd8016 Consider nested space when populating space hierarchy 2022-08-26 20:21:35 +02:00
Jan Bidler
a23ef130ca Make unread messages use an ascii circle 2022-08-26 12:33:43 +00:00
Jan Bidler
2f4116796a Rework notification colors 2022-08-26 12:33:43 +00:00
Tobias Fella
f004f9e3c8 Fix build 2022-08-26 12:52:46 +02:00
l10n daemon script
9a3726f9ff GIT_SILENT Sync po/docbooks with svn 2022-08-26 01:50:05 +00:00
Snehit Sah
91d1f6ffeb Show spaces horizontal bar
### Summary

This merge request adds a horizontal bar at top of room list, which shows spaces. By clicking on a space, user can filter out rooms belonging only to that specific space.

### Pending/ Help needed

#### Segfault when loading active connection on startup

Refer `void SortFilterRoomListModel::cacheSpaceHierarchy()` ([link](8c372800d7 (b969e462c30df43ef3714ea441948d8d8027f6a0_117_126))) in `src/sortfilterroomlistmodel.cpp`.

On [line 129](8c372800d7 (b969e462c30df43ef3714ea441948d8d8027f6a0_117_129)), I have called `connection->allRooms()`, which segfaults if the active connection hasn't been loaded yet. Is there a way to ensure that `Controller::instance().activeConnection()` on line 128 waits till connection is loaded?

#### Avatars

Avatars on space horizontal bar aren't aligned to vertical middle. I'll need help with that. 

Using the code below doesn't help with padding

```qml
delegate: QQC2.Control {
    topPadding: 10
    contentItem: Kirigami.Avatar { ..... }
}
```

This complains about uninitialized properties in `Kirigami.Avatar`. (`id`, `currentRoom`, `avatar`, `index` are properties utilized during run time)

After we get around these two issue, this MR will be ready from my side.
2022-08-25 13:46:09 +00:00
Tobias Fella
cd895f1b06 QML warnings -= 2 2022-08-23 21:24:41 +02:00
Tobias Fella
fead1a69b3 QML warning-- 2022-08-23 21:22:02 +02:00
James Graham
63af4cae77 Fix timeline layout for non text messages
Change the target of the hover component and delegate layout to timeline container so it applies to all delegates not just text messages.

BUG: 457689
2022-08-22 17:38:50 +00:00
l10n daemon script
89090690b4 GIT_SILENT Sync po/docbooks with svn 2022-08-21 01:56:52 +00:00
l10n daemon script
6a16334eab GIT_SILENT Sync po/docbooks with svn 2022-08-20 01:49:36 +00:00
l10n daemon script
5b05622058 GIT_SILENT Sync po/docbooks with svn 2022-08-16 01:54:13 +00:00
l10n daemon script
1eb705c17f GIT_SILENT Sync po/docbooks with svn 2022-08-14 01:52:46 +00:00
l10n daemon script
f9ad0e8426 GIT_SILENT Sync po/docbooks with svn 2022-08-13 01:50:50 +00:00
l10n daemon script
ef1ff04ef2 GIT_SILENT Sync po/docbooks with svn 2022-08-10 01:57:08 +00:00
James Graham
9b54aff3d5 Mark unread messages in the room as read when all messages are visible
This is an alternative to network/neochat!467. When discussing in the matrix channel this option seemed more popular so I implemented it.

Mark the unread messages in the room as read when all messages are visible to the user after a short timer. This happens on entry and when new messages come in as long as Neochat is active. If neochat isn't active, the room hasn't loaded or the read marker hasn't loaded the timer is for this is reset until all 3 conditions are false.
2022-08-09 16:42:32 +00:00
l10n daemon script
52a093d449 GIT_SILENT Sync po/docbooks with svn 2022-08-09 01:51:02 +00:00
Nate Graham
619369e148 Add hackaround for Qt bug to all non-horizontally-scrollable scrollviews
https://bugreports.qt.io/browse/QTBUG-83890 has been open for years with
a patch that's been stalled for years. There's no indication that it's
going to be fixed anytime soon, and it generates bug reports for us.
Let's add the typical hackaround for all non-horizontally-scrollable
scrollviews.

BUG: 457584
2022-08-08 15:02:09 -06:00
l10n daemon script
e63a9a9be1 GIT_SILENT Sync po/docbooks with svn 2022-08-08 01:58:51 +00:00
l10n daemon script
5c97e67404 GIT_SILENT Sync po/docbooks with svn 2022-08-04 01:52:39 +00:00
l10n daemon script
84a265b7f9 GIT_SILENT Sync po/docbooks with svn 2022-07-31 01:58:40 +00:00
l10n daemon script
61201a7097 GIT_SILENT Sync po/docbooks with svn 2022-07-29 01:56:35 +00:00
Snehit Sah
b9630ad2f2 Add Flatpak CI Support
Signed-off-by: Snehit Sah <snehitsah@protonmail.com>
2022-07-27 10:01:35 +00:00
l10n daemon script
179a201113 GIT_SILENT Sync po/docbooks with svn 2022-07-24 01:50:54 +00:00
l10n daemon script
6eef58e57d GIT_SILENT Sync po/docbooks with svn 2022-07-23 01:55:00 +00:00
l10n daemon script
94f325609a GIT_SILENT Sync po/docbooks with svn 2022-07-20 01:49:27 +00:00
l10n daemon script
a40ba493b6 GIT_SILENT Sync po/docbooks with svn 2022-07-19 01:46:04 +00:00
Tobias Fella
916e7465f1 Don't show a link preview for empty links
When hovering over a link without a target, qt5 will report the link target to be "1", which is wrong.
To work around this, we manually check if the link is "1" and if it is, we discard it.
In theory, this means that we won't get a preview for any link that actually *is* "1", but why would any link be "1"?

It's not worth reporting this to Qt since it seems fixed in Qt6

BUG: 456877
2022-07-18 21:18:48 +02:00
Tobias Fella
8257a9d65e Refactor delegates and improve context menu opening
This unifies the context menu opening and makes sure that clicking *anywhere* on the delegate opens the context menu, not just on the content
2022-07-18 20:12:26 +02:00
l10n daemon script
a75048761b GIT_SILENT Sync po/docbooks with svn 2022-07-18 01:47:32 +00:00
Carl Schwan
f21822aba7 Fix checkbox label on small screen
See https://invent.kde.org/frameworks/qqc2-desktop-style/-/merge_requests/173
2022-07-17 14:46:18 +00:00
l10n daemon script
75eb5a51af GIT_SILENT Sync po/docbooks with svn 2022-07-17 01:54:19 +00:00
l10n daemon script
7e37c31011 GIT_SILENT made messages (after extraction) 2022-07-17 00:45:21 +00:00
l10n daemon script
f80039a5c4 GIT_SILENT Sync po/docbooks with svn 2022-07-16 01:56:39 +00:00
James Graham
400d86a1e9 Precise time on hover
Implement showing a long datetime as a tooltip when hovering over the timestamp for a message. The timestamp is also moved to just after the username of the poster this means it will not be under the hover buttons unless the message is very short.

Also some cleanup of the alignment of items in the bubble. The reply text is now indented the same amount as the username and message and the padding isn't removed from the username and message when avatars are disabled.

Implements: network/neochat#223
2022-07-15 15:39:33 +00:00
l10n daemon script
b20b1c10d0 GIT_SILENT Sync po/docbooks with svn 2022-07-15 01:55:09 +00:00
Tobias Fella
a37fd0713f Convert C++ parts into a static library
This allows us to start adding unit tests
2022-07-14 15:27:29 +00:00
Tobias Fella
b1581a54d1 Allow sending encrypted messages if build supports it 2022-07-14 16:59:37 +02:00
l10n daemon script
ded60b906b GIT_SILENT Sync po/docbooks with svn 2022-07-14 01:58:39 +00:00
James Graham
6957dd0fa2 Apply margin in SectionDelegate both for Compact and Bubble mode 2022-07-13 17:27:13 +00:00
Tobias Fella
7de4014b28 Update formatting 2022-07-13 17:27:13 +00:00
James Graham
63e7ec1bd7 Spell like an American 2022-07-13 17:27:13 +00:00
James Graham
f24428fab3 Apply margin in ReadMarkerDelegate both for Compact and Bubble mode 2022-07-13 17:27:13 +00:00
James Graham
c3a5a767c2 Fix alignment of hover buttons when user message on the right is thin. In this case the buttons sould align to the right of the message not the left so they don't go off screen. 2022-07-13 17:27:13 +00:00
James Graham
5fb311b509 Implement new delegate behaviour on ReadMarkerDelegate
Make sure that the messgae hover buttons account for the delegate x displacement
2022-07-13 17:27:13 +00:00
James Graham
f0d832f756 Make sure extra width is never less than 0 2022-07-13 17:27:13 +00:00
James Graham
a7c137ca39 Allow the delegate and bubble widths to grow when the ListView is very wide.
Disable user message on the right setting when in compact mode as it doesn't work anyway.
2022-07-13 17:27:13 +00:00
James Graham
a96e8958c9 Centre the timline when using bubbles but not in compact mode 2022-07-13 17:27:13 +00:00
James Graham
7dc951d2cd Support user messages on the right even when wide
Limit maximum delegate width to ensure that the gap between user and non-user messages isn't too large
2022-07-13 17:27:13 +00:00
Tobias Fella
c3ee277ede Fix opening room using touch
The previous fix wasn't enough for non-mobile touch devices. Now, we limit the TapHandler to mouse instead
2022-07-13 14:59:16 +02:00
Tobias Fella
78d62e9376 Revert "Disable opening context menu by right-clicking on mobile"
This reverts commit 51efecaa25.
2022-07-13 14:55:55 +02:00
l10n daemon script
11e9eaf3e9 GIT_SILENT Sync po/docbooks with svn 2022-07-13 01:52:14 +00:00
James Graham
4337d0d5d8 Removing all \n is incorrect as these are used to show linebreaks in the html formatted body. Instead use the CMARK_OPT_HARDBREAKS options so these softbreaks in the markdown string are converted to hard breaks <br/> in the html.
Also remove 2 step process to replace <!-- raw HTML omitted --> and straight replace with "" to ensure no real breaks are removed
2022-07-12 14:03:21 +00:00
l10n daemon script
a07537367f GIT_SILENT Sync po/docbooks with svn 2022-07-12 01:55:45 +00:00
Tobias Fella
51efecaa25 Disable opening context menu by right-clicking on mobile
Apparently TapHandlers interpret a tap as a right click, which causes rooms to not open reliably
2022-07-11 12:41:27 +02:00
l10n daemon script
830a47c5ff GIT_SILENT Sync po/docbooks with svn 2022-07-11 01:57:02 +00:00
Tobias Fella
24748d42d8 Port C++ to Qt6
QML is still broken
2022-07-10 21:43:57 +00:00
l10n daemon script
19fe439e95 GIT_SILENT Sync po/docbooks with svn 2022-07-10 01:57:44 +00:00
Tobias Fella
2bcd7118f4 Ensure that text isn't formatted in context menu 2022-07-09 23:01:13 +02:00
l10n daemon script
27e660178e GIT_SILENT Sync po/docbooks with svn 2022-07-09 01:57:03 +00:00
Tobias Fella
e0df553a72 Remove unused imports & includes 2022-07-08 13:16:07 +02:00
l10n daemon script
53f040cb28 GIT_SILENT Sync po/docbooks with svn 2022-07-08 01:54:23 +00:00
Nicolas Fella
28cc7cf616 Add FreeBSD CI 2022-07-07 12:51:33 +02:00
l10n daemon script
d224df8aa2 GIT_SILENT Sync po/docbooks with svn 2022-07-07 01:48:15 +00:00
l10n daemon script
1dff2b8273 GIT_SILENT Sync po/docbooks with svn 2022-07-06 01:49:30 +00:00
Nicolas Fella
722aa422e7 Fix activating browser windows on Wayland
QDesktopServices::openUrl does not have XDG activation support yet so it can't raise an existing browser window when opening URLs

Instead use KIO::OpenUrlJob, which does support that
2022-07-05 22:55:12 +00:00
Akseli Lahtinen
70de0dc624 add settings button to room context menu 2022-07-05 21:26:12 +00:00
l10n daemon script
9d804e6ea7 GIT_SILENT Sync po/docbooks with svn 2022-07-05 01:46:44 +00:00
Volker Krause
52d552650d Use product screenshots from CDN rather than expensive direct Gitlab links 2022-07-03 12:16:43 +02:00
l10n daemon script
0c5007fd56 GIT_SILENT Sync po/docbooks with svn 2022-07-03 02:06:37 +00:00
l10n daemon script
af19829225 GIT_SILENT Sync po/docbooks with svn 2022-06-28 01:49:26 +00:00
l10n daemon script
0be8828dd4 GIT_SILENT Sync po/docbooks with svn 2022-06-27 01:47:51 +00:00
l10n daemon script
729b6bd354 GIT_SILENT Sync po/docbooks with svn 2022-06-26 01:48:37 +00:00
Heiko Becker
ef10042179 Sonnet is only a runtime dependency
Since 98571cb37d.
2022-06-25 15:16:54 +02:00
l10n daemon script
846d430947 GIT_SILENT Sync po/docbooks with svn 2022-06-25 02:08:13 +00:00
l10n daemon script
cce4a3ebdf GIT_SILENT made messages (after extraction) 2022-06-25 00:49:02 +00:00
96 changed files with 9784 additions and 8525 deletions

157
.flatpak-manifest.json Normal file
View File

@@ -0,0 +1,157 @@
{
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-21.08",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
"nightly"
],
"desktop-file-name-suffix": " (Nightly)",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--filesystem=xdg-download",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"modules": [
{
"name": "kquickimageeditor",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://invent.kde.org/libraries/kquickimageeditor"
}
]
},
{
"name": "olm",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://gitlab.matrix.org/matrix-org/olm.git",
"tag": "3.2.10",
"x-checker-data": {
"type": "git",
"tag-pattern": "^([\\d.]+)$"
},
"commit": "9908862979147a71dc6abaecd521be526ae77be1"
}
]
},
{
"name": "libsecret",
"buildsystem": "meson",
"config-opts": [
"-Dmanpage=false",
"-Dvapi=false",
"-Dgtk_doc=false",
"-Dintrospection=false",
"-Dgcrypt=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz",
"sha256": "3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d",
"x-checker-data": {
"type": "gnome",
"name": "libsecret",
"stable-only": true
}
}
]
},
{
"name": "qtkeychain",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "archive",
"url": "https://github.com/frankosterfeld/qtkeychain/archive/v0.13.2.tar.gz",
"sha256": "20beeb32de7c4eb0af9039b21e18370faf847ac8697ab3045906076afbc4caa5",
"x-checker-data": {
"type": "anitya",
"project-id": 4138,
"stable-only": true,
"url-template": "https://github.com/frankosterfeld/qtkeychain/archive/v$version.tar.gz"
}
}
],
"config-opts": [
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DLIB_INSTALL_DIR=/app/lib",
"-DBUILD_TRANSLATIONS=NO"
]
},
{
"name": "libQuotient",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/quotient-im/libQuotient.git",
"branch": "dev"
}
],
"config-opts": [
"-DQuotient_ENABLE_E2EE=ON"
]
},
{
"name": "cmark",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/commonmark/cmark.git"
}
],
"config-opts": [
"-DCMARK_TESTS=OFF",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=/app"
],
"builddir": true
},
{
"name": "qcoro",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "archive",
"url": "https://github.com/danvratil/qcoro/archive/refs/tags/v0.4.0.tar.gz",
"sha256": "0e68b3f0ce7bf521ffbdd731464d2d60d8d7a39a749b551ed26855a1707d86d1",
"x-checker-data": {
"type": "anitya",
"project-id": 236236,
"stable-only": true,
"url-template": "https://github.com/danvratil/qcoro/archive/refs/tags/v$version.tar.gz"
}
}
]
},
{
"name": "neochat",
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "dir",
"path": "."
}
],
"config-opts": [
"-DNEOCHAT_FLATPAK=ON"
]
}
]
}

View File

@@ -5,4 +5,8 @@ include:
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.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
# TODO enable once we can have qt6 libQuotient on the CI
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml

View File

@@ -42,3 +42,7 @@ License: BSD-2-Clause
Files: imports/NeoChat/Component/confetti.png imports/NeoChat/Component/glowdot.png
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
License: CC0-1.0
Files: .flatpak-manifest.json
Copyright: 2020-2022 Tobias Fella <fella@posteo.de>
License: BSD-2-Clause

View File

@@ -41,8 +41,8 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
)
find_package(Qt5 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt5 PROPERTIES
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
@@ -56,8 +56,8 @@ set_package_properties(KF5Kirigami2 PROPERTIES
PURPOSE "Kirigami application UI framework"
)
find_package(Qt5Keychain)
set_package_properties(Qt5Keychain PROPERTIES
find_package(Qt${QT_MAJOR_VERSION}Keychain)
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
TYPE REQUIRED
PURPOSE "Secure storage of account secrets"
)
@@ -69,11 +69,12 @@ if(ANDROID)
PURPOSE "Encrypted communications"
)
else()
find_package(Qt5 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem Sonnet)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
set_package_properties(KF5QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
)
ecm_find_qmlmodule(org.kde.sonnet 1.0)
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
endif()
@@ -108,8 +109,8 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro5 COMPONENTS Core QUIET)
if(NOT QCoro5_FOUND)
find_package(QCoro${QT_MAJOR_VERSION} COMPONENTS Core QUIET)
if(NOT QCoro${QT_MAJOR_VERSION}_FOUND)
find_package(QCoro REQUIRED)
endif()

View File

@@ -31,7 +31,7 @@ and can also directly be downloaded from the [binary factory](https://binary-fac
Nightly builds for [Windows](https://binary-factory.kde.org/job/NeoChat_Nightly_win64/), [MacOS](https://binary-factory.kde.org/job/NeoChat_Nightly_macos/) and [AppImages](https://binary-factory.kde.org/job/NeoChat_Nightly_appimage/) can also be downloaded from the [binary factory](https://binary-factory.kde.org/search/?q=neochat).
![Timeline](https://invent.kde.org/websites/product-screenshots/-/raw/master/neochat/application.png)
![Timeline](https://cdn.kde.org/screenshots/neochat/application.png)
## Features
@@ -55,7 +55,7 @@ We welcome contributions in this direction.
## Contact
You can reach the maintainers at #neochat:kde.org, if you are already on Matrix.
You can reach the maintainers at [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org), if you are already on Matrix.
Development happens in http://invent.kde.org/network/neochat (not in GitHub).
## Acknowledgement

View File

@@ -14,14 +14,14 @@ import NeoChat.Page 1.0
Loader {
id: root
property var attachmentMimetype: FileType.mimeTypeForUrl(ChatBoxHelper.attachmentPath)
property var attachmentMimetype: FileType.mimeTypeForUrl(chatBoxHelper.attachmentPath)
readonly property bool hasImage: attachmentMimetype.valid && FileType.supportedImageFormats.includes(attachmentMimetype.preferredSuffix)
active: visible
sourceComponent: Component {
Pane {
id: attachmentPane
property string baseFileName: ChatBoxHelper.attachmentPath.toString().substring(ChatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, ChatBoxHelper.attachmentPath.length)
property string baseFileName: chatBoxHelper.attachmentPath.toString().substring(chatBoxHelper.attachmentPath.toString().lastIndexOf('/') + 1, chatBoxHelper.attachmentPath.length)
Kirigami.Theme.colorSet: Kirigami.Theme.View
contentItem: Item {
@@ -46,7 +46,7 @@ Loader {
asynchronous: true
cache: false // Cache is not needed. Images will rarely be shown repeatedly.
smooth: height == preferredHeight && parent.height == parent.implicitHeight // Don't smooth until height animation stops
source: hasImage ? ChatBoxHelper.attachmentPath : ""
source: hasImage ? chatBoxHelper.attachmentPath : ""
visible: hasImage
fillMode: Image.PreserveAspectFit
@@ -162,14 +162,14 @@ Loader {
Component {
id: imageEditorPage
ImageEditorPage {
imagePath: ChatBoxHelper.attachmentPath
imagePath: chatBoxHelper.attachmentPath
}
}
onClicked: {
let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage);
imageEditor.newPathChanged.connect(function(newPath) {
applicationWindow().pageStack.layers.pop();
ChatBoxHelper.attachmentPath = newPath;
chatBoxHelper.attachmentPath = newPath;
});
}
ToolTip.text: text
@@ -180,7 +180,7 @@ Loader {
icon.name: "dialog-cancel"
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: ChatBoxHelper.clearAttachment();
onClicked: chatBoxHelper.clearAttachment();
ToolTip.text: text
ToolTip.visible: hovered
}

View File

@@ -63,6 +63,9 @@ ToolBar {
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
+ inputField.topPadding + inputField.bottomPadding
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
FontMetrics {
id: fontMetrics
font: inputField.font
@@ -94,11 +97,11 @@ ToolBar {
//property int lineHeight: contentHeight / lineCount
text: inputFieldText
placeholderText: currentRoom.usesEncryption ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : i18n("Write your message...")
placeholderText: readOnly ? i18n("This room is encrypted. Sending encrypted messages is not yet supported.") : editEventId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : i18n("Send a message")
verticalAlignment: TextEdit.AlignVCenter
horizontalAlignment: TextEdit.AlignLeft
wrapMode: Text.Wrap
readOnly: currentRoom.usesEncryption
readOnly: currentRoom.usesEncryption && !Controller.encryptionSupported
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
@@ -145,11 +148,7 @@ ToolBar {
}
Keys.onPressed: {
if (event.key === Qt.Key_PageDown) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp) {
switchRoomUp();
} else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
chatBar.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
replyPreviousUserMessage();
@@ -278,14 +277,14 @@ ToolBar {
}
Item {
visible: !ChatBoxHelper.isReplying && (!ChatBoxHelper.hasAttachment || uploadingBusySpinner.running)
visible: !chatBoxHelper.isReplying && (!chatBoxHelper.hasAttachment || uploadingBusySpinner.running)
implicitWidth: uploadButton.implicitWidth
implicitHeight: uploadButton.implicitHeight
ToolButton {
id: uploadButton
anchors.fill: parent
// Matrix does not allow sending attachments in replies
visible: !ChatBoxHelper.isReplying && !ChatBoxHelper.hasAttachment && !uploadingBusySpinner.running
visible: !chatBoxHelper.isReplying && !chatBoxHelper.hasAttachment && !uploadingBusySpinner.running
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: AbstractButton.IconOnly
@@ -297,7 +296,7 @@ ToolBar {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) { return }
ChatBoxHelper.attachmentPath = path;
chatBoxHelper.attachmentPath = path;
})
fileDialog.open()
}
@@ -380,16 +379,16 @@ ToolBar {
if (!Clipboard.saveImage(localPath)) {
return;
}
ChatBoxHelper.attachmentPath = localPath;
chatBoxHelper.attachmentPath = localPath;
}
function postMessage() {
checkForFancyEffectsReason();
if (ChatBoxHelper.hasAttachment) {
if (chatBoxHelper.hasAttachment) {
// send attachment but don't reset the text
actionsHandler.postMessage("", ChatBoxHelper.attachmentPath,
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, {}, this.customEmojiModel);
actionsHandler.postMessage("", chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, {}, this.customEmojiModel);
currentRoom.markAllMessagesAsRead();
messageSent();
return;
@@ -401,14 +400,12 @@ ToolBar {
actionsHandler.postEdit(inputField.text);
} else {
// send normal message
actionsHandler.postMessage(inputField.text.trim(), ChatBoxHelper.attachmentPath,
ChatBoxHelper.replyEventId, ChatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
actionsHandler.postMessage(inputField.text.trim(), chatBoxHelper.attachmentPath,
chatBoxHelper.replyEventId, chatBoxHelper.editEventId, userAutocompleted, this.customEmojiModel);
}
currentRoom.markAllMessagesAsRead();
inputField.clear();
inputField.text = Qt.binding(function() {
return currentRoom ? currentRoom.cachedInput : "";
});
inputField.text = currentRoom ? currentRoom.cachedInput : "";
messageSent()
}

View File

@@ -127,8 +127,8 @@ Item {
ReplyPane {
id: replyPane
visible: ChatBoxHelper.isReplying || ChatBoxHelper.isEditing
user: ChatBoxHelper.replyUser
visible: chatBoxHelper.isReplying || chatBoxHelper.isEditing
user: chatBoxHelper.replyUser
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: attachmentSeparator.top
@@ -154,7 +154,7 @@ Item {
AttachmentPane {
id: attachmentPane
visible: ChatBoxHelper.hasAttachment
visible: chatBoxHelper.hasAttachment
width: parent.width
height: visible ? implicitHeight : 0
anchors.bottom: chatBarSeparator.top
@@ -248,7 +248,7 @@ Item {
}
Connections {
target: ChatBoxHelper
target: chatBoxHelper
function onShouldClearText() {
root.inputFieldText = "";
@@ -277,7 +277,7 @@ Item {
}
function closeAll() {
ChatBoxHelper.clear();
chatBoxHelper.clear();
chatBar.emojiPaneOpened = false;
}
}

View File

@@ -47,6 +47,7 @@ Popup {
implicitHeight: Math.min(completionListView.contentHeight, Kirigami.Units.gridUnit * 10)
contentItem: ScrollView {
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: completionListView

View File

@@ -12,7 +12,7 @@ import org.kde.neochat 1.0
Loader {
id: root
readonly property bool isEdit: ChatBoxHelper.isEditing
readonly property bool isEdit: chatBoxHelper.isEditing
property var user: null
property string avatarMediaUrl: user ? "image://mxc/" + user.avatarMediaId : ""
@@ -71,6 +71,10 @@ Loader {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 8 - fontMetrics.leading
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
TextArea {
id: textArea
leftPadding: 0
@@ -79,7 +83,7 @@ Loader {
bottomPadding: 0
text: {
const stylesheet = "<style> a{color:"+Kirigami.Theme.linkColor+";}.user-pill{}</style>";
const content = ChatBoxHelper.isReplying ? ChatBoxHelper.replyEventContent : ChatBoxHelper.editContent;
const content = chatBoxHelper.isReplying ? chatBoxHelper.replyEventContent : chatBoxHelper.editContent;
return stylesheet + content;
}
selectByMouse: true
@@ -102,7 +106,7 @@ Loader {
text: i18n("Cancel")
display: AbstractButton.IconOnly
onClicked: {
ChatBoxHelper.clearEditReply();
chatBoxHelper.clear();
root.replyCancelled();
}
ToolTip.text: text

View File

@@ -20,7 +20,7 @@ LoginStep {
Connections {
target: LoginHelper
function onSsoUrlChanged() {
Qt.openUrlExternally(LoginHelper.ssoUrl)
UrlHelper.openUrl(LoginHelper.ssoUrl)
}
function onConnected() {
processed("qrc:/imports/NeoChat/Component/Login/Loading.qml")

View File

@@ -1,71 +1,116 @@
// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1 as Platform
import QtMultimedia 5.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: audioDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
onOpenContextMenu: openFileContext(model, audioDelegate)
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
onDownloadedChanged: audio.play()
hoverComponent: hoverActions
innerObject: Control {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.bubbleMaxWidth
Layout.maximumWidth: audioDelegate.contentMaxWidth
Audio {
id: audio
source: currentRoom.urlToMxcUrl(content.url)
source: model.progressInfo.localPath
autoLoad: false
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
states: [
State {
name: "notDownloaded"
when: !model.progressInfo.completed && !model.progressInfo.active
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: currentRoom.downloadFile(model.eventId)
}
},
State {
name: "downloading"
when: model.progressInfo.active && !model.progressInfo.completed
PropertyChanges {
target: downloadBar
visible: true
}
PropertyChanges {
target: playButton
icon.name: "media-playback-stop"
onClicked: {
currentRoom.cancelFileTransfer(model.eventId)
}
}
},
State {
name: "paused"
when: model.progressInfo.completed && (audio.playbackState === Audio.StoppedState || audio.playbackState === Audio.PausedState)
PropertyChanges {
target: playButton
icon.name: "media-playback-start"
onClicked: {
audio.play()
}
}
},
State {
name: "playing"
when: model.progressInfo.completed && audio.playbackState === Audio.PlayingState
PropertyChanges {
target: playButton
icon.name: "media-playback-pause"
onClicked: audio.pause()
}
}
]
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()
}
}
id: playButton
}
Label {
text: model.display
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
ProgressBar {
id: downloadBar
visible: false
Layout.fillWidth: true
from: 0
to: model.content.info.size
value: model.progressInfo.progress
}
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 {
Slider {
from: 0
to: audio.duration
value: audio.position
onMoved: audio.seek(value)
}
Label {

View File

@@ -10,7 +10,6 @@ import org.kde.neochat 1.0
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.")
@@ -20,7 +19,7 @@ TimelineContainer {
readOnly: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
Layout.maximumWidth: encryptedDelegate.bubbleMaxWidth
Layout.maximumWidth: encryptedDelegate.contentMaxWidth
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
}
}

View File

@@ -4,7 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
@@ -16,11 +15,12 @@ import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: fileDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, fileDelegate)
readonly property bool downloaded: progressInfo && progressInfo.completed
function saveFileAs() {
@@ -30,14 +30,14 @@ TimelineContainer {
}
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
}
innerObject: RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: fileDelegate.bubbleMaxWidth
Layout.maximumWidth: fileDelegate.contentMaxWidth
Layout.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.largeSpacing
@@ -131,14 +131,5 @@ TimelineContainer {
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
}

View File

@@ -4,7 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1
import org.kde.neochat 1.0
@@ -16,11 +15,11 @@ import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: imageDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, imageDelegate)
property var content: model.content
readonly property bool isAnimated: contentType === "image/gif"
@@ -35,8 +34,8 @@ TimelineContainer {
innerObject: Image {
id: img
Layout.maximumWidth: imageDelegate.bubbleMaxWidth
Layout.maximumHeight: imageDelegate.bubbleMaxWidth / imageDelegate.info.w * imageDelegate.info.h
Layout.maximumWidth: imageDelegate.contentMaxWidth
Layout.maximumHeight: imageDelegate.contentMaxWidth / imageDelegate.info.w * imageDelegate.info.h
Layout.preferredWidth: imageDelegate.info.w
Layout.preferredHeight: imageDelegate.info.h
source: model.mediaUrl
@@ -86,11 +85,6 @@ TimelineContainer {
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
@@ -115,8 +109,8 @@ TimelineContainer {
}
function openSavedFile() {
if (Qt.openUrlExternally(progressInfo.localPath)) return;
if (Qt.openUrlExternally(progressInfo.localDir)) return;
if (UrlHelper.openUrl(progressInfo.localPath)) return;
if (UrlHelper.openUrl(progressInfo.localDir)) return;
}
}
}

View File

@@ -13,24 +13,15 @@ import org.kde.neochat 1.0
TimelineContainer {
id: messageDelegate
width: ListView.view.width
property bool isEmote: false
onOpenContextMenu: openMessageContext(model, parent.selectedText, Controller.plainText(label.textDocument))
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: RichLabel {
id: label
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

@@ -11,11 +11,49 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
id: readMarkerDelegate
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
// extraWidth defines how the delegate can grow after the listView gets very wide
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width - Kirigami.Units.largeSpacing * 2 : Math.min(messageListView.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth)
width: delegateMaxWidth
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: Kirigami.Units.largeSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: readMarkerDelegate
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: readMarkerDelegate
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
}
}
]
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}

View File

@@ -15,11 +15,11 @@ MouseArea {
id: replyButton
Layout.fillWidth: true
implicitHeight: replyName.implicitHeight + (loader.item ? loader.item.height : 0) + Kirigami.Units.largeSpacing
implicitWidth: Math.min(bubbleMaxWidth, Math.max((loader.item ? loader.item.width + Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
implicitWidth: Math.min(contentMaxWidth, Math.max((loader.item ? loader.item.width + Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0), replyName.implicitWidth)) + Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
Component.onCompleted: {
parent.Layout.fillWidth = true;
parent.Layout.preferredWidth = Qt.binding(function() { return implicitWidth; })
parent.Layout.maximumWidth = Qt.binding(function() { return bubbleMaxWidth + Kirigami.Units.largeSpacing * 2; })
parent.Layout.maximumWidth = Qt.binding(function() { return contentMaxWidth + Kirigami.Units.largeSpacing * 2; })
}
Rectangle {
id: replyLeftBorder
@@ -79,7 +79,7 @@ MouseArea {
id: replyText
textMessage: reply.display
textFormat: Text.RichText
width: Math.min(implicitWidth, bubbleMaxWidth - Kirigami.Units.largeSpacing * 3)
width: Math.min(implicitWidth, contentMaxWidth - Kirigami.Units.largeSpacing * 3)
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}
}
@@ -94,7 +94,7 @@ MouseArea {
readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId
source: "image://mxc/" + mediaId
width: bubbleMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - replyAvatar.width
width: contentMaxWidth * 0.75 - Kirigami.Units.smallSpacing * 5 - replyAvatar.width
height: reply.content.info.h / reply.content.info.w * width
x: Kirigami.Units.smallSpacing * 3 + replyAvatar.width
}

View File

@@ -22,7 +22,7 @@ TextEdit {
Layout.fillWidth: Config.compactLayout
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.leftMargin: Kirigami.Units.largeSpacing
text: "<style>
table {
@@ -44,6 +44,10 @@ a{
text-decoration: none;
}
" + (!spoilerRevealed ? "
[data-mx-spoiler] a {
color: transparent;
background: " + Kirigami.Theme.textColor + ";
}
[data-mx-spoiler] {
color: transparent;
background: " + Kirigami.Theme.textColor + ";
@@ -58,8 +62,11 @@ a{
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: RoomManager.openResource(link)
onHoveredLinkChanged: if (hoveredLink.length > 0) {
onLinkActivated: {
spoilerRevealed = true
RoomManager.openResource(link)
}
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
applicationWindow().hoverLinkIndicator.text = hoveredLink;
} else {
applicationWindow().hoverLinkIndicator.text = "";

View File

@@ -7,19 +7,53 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
Control {
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
width: ListView.view.width - Kirigami.Units.largeSpacing - x
id: stateDelegate
// extraWidth defines how the delegate can grow after the listView gets very wide
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
width: delegateMaxWidth
// anchors.leftMargin: Kirigami.Units.largeSpacing
// anchors.rightMargin: Kirigami.Units.largeSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: stateDelegate
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: stateDelegate
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
height: sectionDelegate.height + rowLayout.height
SectionDelegate {
id: sectionDelegate
width: parent.width
anchors.top: parent.top
anchors.leftMargin: Kirigami.Units.smallSpacing
anchors.left: parent.left
anchors.right: parent.right
visible: model.showSection
height: visible ? implicitHeight : 0
}
@@ -27,8 +61,11 @@ Control {
RowLayout {
id: rowLayout
height: label.contentHeight
width: parent.width
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
anchors.rightMargin: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: icon

View File

@@ -4,7 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.15 as Kirigami
@@ -13,19 +12,24 @@ import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
QQC2.ItemDelegate {
id: messageDelegate
id: timelineContainer
default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed
property bool isEmote: false
property bool cardBackground: true
readonly property int bubbleMaxWidth: Config.compactLayout && !Config.showAvatarInTimeline ? width - Kirigami.Units.largeSpacing * 4 : (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))
signal openContextMenu
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
// extraWidth defines this as the excess after a certain ListView width, capped to a max value
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth)
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight &&
model.author.isLocalUser &&
!applicationWindow().wideScreen &&
!Config.compactLayout
model.author.isLocalUser && !Config.compactLayout
signal openExternally()
signal replyClicked(string eventID)
@@ -38,7 +42,10 @@ QQC2.ItemDelegate {
topPadding: 0
bottomPadding: 0
width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : 0)
background: null
property Item hoverComponent
// show hover actions
@@ -51,13 +58,39 @@ QQC2.ItemDelegate {
// updates the global hover component to point to this delegate, and update its position
function updateHoverComponent() {
if (hoverComponent) {
hoverComponent.delegate = timelineContainer
hoverComponent.bubble = bubble
hoverComponent.updateFunction = updateHoverComponent;
hoverComponent.event = model
}
}
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : 0)
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: timelineContainer
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: timelineContainer
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
SectionDelegate {
id: sectionDelegate
@@ -111,18 +144,15 @@ QQC2.ItemDelegate {
rightPadding: Config.compactLayout ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
hoverEnabled: true
// state: Config.compactLayout ? "compactLayout" : "default"
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
anchors {
top: avatar.top
leftMargin: Kirigami.Units.largeSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout ? messageDelegate.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
width: Config.compactLayout ? timelineContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize
// as setting anchors to undefined did not work reliably
states: [
@@ -160,7 +190,7 @@ QQC2.ItemDelegate {
Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.preferredWidth: nameLabel.implicitWidth + timeLabel.implicitWidth + Kirigami.Units.largeSpacing * 2
Layout.maximumWidth: bubbleMaxWidth
Layout.maximumWidth: contentMaxWidth
implicitHeight: visible ? nameLabel.implicitHeight : 0
QQC2.Label {
@@ -168,15 +198,13 @@ QQC2.ItemDelegate {
topInset: 0
visible: model.showAuthor && !isEmote
anchors.left: rowLayout.left
anchors.right: timeLabel.left
anchors.rightMargin: Kirigami.Units.smallSpacing
width: Math.min(contentMaxWidth - timeLabel.width, implicitWidth)
text: visible ? author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
wrapMode: Text.Wrap
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
@@ -193,10 +221,18 @@ QQC2.ItemDelegate {
}
QQC2.Label {
id: timeLabel
anchors.right: rowLayout.right
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
anchors.left: nameLabel.right
visible: model.showAuthor && !isEmote
text: visible ? time.toLocaleTimeString(Locale.ShortFormat) : ""
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
HoverHandler {
id: hoverHandler
}
}
}
Loader {
@@ -206,6 +242,7 @@ QQC2.ItemDelegate {
visible: active
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Config.compactLayout ? 0 : Kirigami.Units.smallSpacing
Layout.leftMargin: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing
Connections {
target: replyLoader.item
@@ -218,7 +255,7 @@ QQC2.ItemDelegate {
background: Item {
Rectangle {
visible: messageDelegate.hovered
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
anchors.fill: parent
@@ -258,4 +295,14 @@ QQC2.ItemDelegate {
visible: active
sourceComponent: ReactionDelegate { }
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: timelineContainer.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: timelineContainer.openContextMenu()
}
}

View File

@@ -4,7 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import QtMultimedia 5.15
import Qt.labs.platform 1.1 as Platform
@@ -18,8 +17,6 @@ import NeoChat.Menu.Timeline 1.0
TimelineContainer {
id: videoDelegate
width: ListView.view.width
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
@@ -29,6 +26,8 @@ TimelineContainer {
property bool supportStreaming: true
readonly property int maxWidth: 1000 // TODO messageListView.width
onOpenContextMenu: openFileContext(model, vid)
onDownloadedChanged: {
if (downloaded) {
vid.source = progressInfo.localPath
@@ -43,7 +42,7 @@ TimelineContainer {
innerObject: Video {
id: vid
Layout.maximumWidth: videoDelegate.bubbleMaxWidth
Layout.maximumWidth: videoDelegate.contentMaxWidth
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 15
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
@@ -124,16 +123,6 @@ TimelineContainer {
videoDelegate.downloadAndPlay()
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openFileContext(model, parent)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent)
}
}
function downloadAndPlay() {

View File

@@ -92,7 +92,7 @@ Labs.MenuBar {
Labs.MenuItem {
text: i18nc("menu", "Matrix FAQ")
onTriggered: Qt.openUrlExternally("https://matrix.org/faq/")
onTriggered: UrlHelper.openUrl("https://matrix.org/faq/")
}
}
}

View File

@@ -57,6 +57,11 @@ Loader {
}
}
MenuItem {
text: i18n("Room settings")
onTriggered: ApplicationWindow.window.pageStack.pushDialogLayer('qrc:/imports/NeoChat/RoomSettings/Categories.qml', {room: room})
}
MenuSeparator {}
MenuItem {

View File

@@ -24,13 +24,13 @@ MessageDelegateContextMenu {
icon.name: "document-open"
onTriggered: {
if (file.downloaded) {
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
}
} else {
file.onDownloadedChanged.connect(function() {
if (!Qt.openUrlExternally(progressInfo.localPath)) {
Qt.openUrlExternally(progressInfo.localDir);
if (!UrlHelper.openUrl(progressInfo.localPath)) {
UrlHelper.openUrl(progressInfo.localDir);
}
});
currentRoom.downloadFile(eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId))
@@ -50,7 +50,7 @@ MessageDelegateContextMenu {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: {
ChatBoxHelper.replyToMessage(eventId, message, author);
chatBoxHelper.replyToMessage(eventId, message, author);
}
},
Kirigami.Action {

View File

@@ -20,6 +20,7 @@ Loader {
property string formattedBody: ""
required property string source
property string selectedText: ""
required property string plainMessage
property list<Kirigami.Action> nestedActions
@@ -27,13 +28,13 @@ Loader {
Kirigami.Action {
text: i18n("Edit")
icon.name: "document-edit"
onTriggered: ChatBoxHelper.edit(message, formattedBody, eventId);
onTriggered: chatBoxHelper.edit(message, formattedBody, eventId);
visible: eventType.length > 0 && author.id === Controller.activeConnection.localUserId && (eventType === "emote" || eventType === "message")
},
Kirigami.Action {
text: i18n("Reply")
icon.name: "mail-replied-symbolic"
onTriggered: ChatBoxHelper.replyToMessage(eventId, message, author);
onTriggered: chatBoxHelper.replyToMessage(eventId, message, author);
},
Kirigami.Action {
visible: author.id === currentRoom.localUser.id || currentRoom.canSendState("redact")
@@ -45,7 +46,7 @@ Loader {
Kirigami.Action {
text: i18n("Copy")
icon.name: "edit-copy"
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.message : loadRoot.selectedText)
onTriggered: Clipboard.saveText(loadRoot.selectedText === "" ? loadRoot.plainMessage : loadRoot.selectedText)
},
Kirigami.Action {
text: i18n("View Source")
@@ -111,7 +112,7 @@ Loader {
Instantiator {
model: WebShortcutModel {
id: webshortcutmodel
selectedText: loadRoot.selectedText ? loadRoot.selectedText : loadRoot.message
selectedText: loadRoot.selectedText ? loadRoot.selectedText : loadRoot.plainMessage
onOpenUrl: RoomManager.visitNonMatrix(url)
}
delegate: QQC2.MenuItem {

View File

@@ -22,6 +22,10 @@ Kirigami.Page {
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
TextArea {
id: sourceTextArea
text: sourceText

View File

@@ -4,7 +4,6 @@
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.15 as Kirigami

View File

@@ -15,6 +15,95 @@ import NeoChat.Component 1.0
import NeoChat.Menu 1.0
Kirigami.ScrollablePage {
header: ColumnLayout {
visible: !page.collapsedMode
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
ListView {
id: spaceList
property string activeSpaceId: ''
orientation: Qt.Horizontal
spacing: Kirigami.Units.largeSpacing
clip:true
visible: spaceList.count > 0
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
Layout.fillWidth: true
model: SortFilterSpaceListModel {
id: sortFilterSpaceListModel
sourceModel: RoomListModel {
id: spaceListModel
connection: Controller.activeConnection
}
}
Connections {
target: SpaceHierarchyCache
function onSpaceHierarchyChanged() {
if (spaceList.activeSpaceId !== '') {
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(spaceList.activeSpaceId, false);
}
}
}
header: QQC2.Control {
contentItem: QQC2.RoundButton {
id: homeButton
flat: true
padding: Kirigami.Units.gridUnit / 2
icon.name: "home"
text: i18nc('@action:button', 'Show All Rooms')
display: QQC2.AbstractButton.IconOnly
onClicked: {
sortFilterRoomListModel.activeSpaceRooms = [];
spaceList.activeSpaceId = '';
listView.positionViewAtIndex(0, ListView.Beginning);
}
QQC2.ToolTip {
text: homeButton.text
}
}
}
delegate: QQC2.Control {
required property string avatar
required property var currentRoom
required property int index
required property string id
implicitWidth: ListView.view.headerItem.implicitWidth
implicitHeight: ListView.view.headerItem.implicitHeight
contentItem: Kirigami.Avatar {
id: del
actions.main: Kirigami.Action {
id: enterSpaceAction
onTriggered: {
spaceList.activeSpaceId = id;
sortFilterRoomListModel.activeSpaceRooms = SpaceHierarchyCache.getRoomListForSpace(id, true);
}
}
QQC2.ToolTip {
text: currentRoom.displayName
}
source: avatar !== "" ? "image://mxc/" + avatar : ""
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
}
}
id: page
title: i18n("Rooms")
@@ -68,7 +157,6 @@ Kirigami.ScrollablePage {
}
}
ListView {
id: listView
@@ -100,6 +188,8 @@ Kirigami.ScrollablePage {
}
}
Layout.fillWidth: true
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
@@ -233,6 +323,7 @@ Kirigami.ScrollablePage {
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: createRoomListContextMenu()
}
@@ -247,15 +338,17 @@ Kirigami.ScrollablePage {
trailing: RowLayout {
QQC2.Label {
text: notificationCount
visible: notificationCount > 0
text: notificationCount > 0 ? notificationCount : "●"
visible: unreadCount > 0
padding: Kirigami.Units.smallSpacing
color: highlightCount > 0 ? "white" : Kirigami.Theme.textColor
color: Kirigami.Theme.textColor
Layout.minimumWidth: height
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: notificationCount > 0
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.backgroundColor
color: highlightCount > 0 ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
opacity: highlightCount > 0 ? 1 : 0.3
radius: height / 2
}
}

View File

@@ -23,6 +23,7 @@ Kirigami.ScrollablePage {
/// It's not readonly because of the seperate window view.
property var currentRoom: RoomManager.currentRoom
property bool loading: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
/// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false;
@@ -52,7 +53,26 @@ Kirigami.ScrollablePage {
onCurrentRoomChanged: {
hasScrolledUpBefore = false;
ChatBoxHelper.clearEditReply()
chatBoxHelper.clearEditReply()
}
Connections {
target: messageEventModel
function onRowsInserted() {
markReadIfVisibleTimer.restart()
}
}
Timer {
id: markReadIfVisibleTimer
interval: 1000
onTriggered: {
if (loading || !currentRoom.readMarkerLoaded || !applicationWindow().active) {
restart()
} else {
markReadIfVisible()
}
}
}
ActionsHandler {
@@ -61,6 +81,10 @@ Kirigami.ScrollablePage {
connection: Controller.activeConnection
}
ChatBoxHelper {
id: chatBoxHelper
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: applicationWindow().pageStack.get(0).forceActiveFocus()
@@ -123,7 +147,7 @@ Kirigami.ScrollablePage {
Kirigami.PlaceholderMessage {
id: loadingIndicator
anchors.centerIn: parent
visible: page.currentRoom === null || (messageListView.count === 0 && !page.currentRoom.allHistoryLoaded && !page.currentRoom.isInvite)
visible: loading
text: i18n("Loading…")
QQC2.BusyIndicator {
running: loadingIndicator.visible
@@ -147,9 +171,9 @@ Kirigami.ScrollablePage {
Keys.onPressed: {
if (event.key === Qt.Key_PageDown && (event.modifiers & Qt.ControlModifier)) {
switchRoomUp();
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
switchRoomDown();
} else if (event.key === Qt.Key_PageUp && (event.modifiers & Qt.ControlModifier)) {
switchRoomUp();
} else if (!(event.modifiers & Qt.ControlModifier) && event.key < Qt.Key_Escape) {
event.accepted = true;
chatBox.addText(event.text);
@@ -242,7 +266,7 @@ Kirigami.ScrollablePage {
fileDialog.chosen.connect(function(path) {
if (!path) return
ChatBoxHelper.attachmentPath = path;
chatBoxHelper.attachmentPath = path;
})
fileDialog.open()
@@ -264,7 +288,7 @@ Kirigami.ScrollablePage {
if (!Clipboard.saveImage(localPath)) {
return;
}
ChatBoxHelper.attachmentPath = localPath;
chatBoxHelper.attachmentPath = localPath;
attachDialog.close();
}
}
@@ -339,7 +363,7 @@ Kirigami.ScrollablePage {
DropArea {
id: dropAreaFile
anchors.fill: parent
onDropped: ChatBoxHelper.attachmentPath = drop.urls[0]
onDropped: chatBoxHelper.attachmentPath = drop.urls[0]
}
QQC2.Pane {
@@ -388,7 +412,7 @@ Kirigami.ScrollablePage {
currentRoom.usersTyping.length,
currentRoom.usersTyping.map(user => user.displayName).join(", ")
) : ""
anchors.right: parent.right
anchors.left: parent.left
height: visible ? implicitHeight : 0
Behavior on height {
NumberAnimation {
@@ -408,7 +432,9 @@ Kirigami.ScrollablePage {
Item {
id: hoverActions
property var event: null
property bool showEdit: event && (event.author.id === Controller.activeConnection.localUserId && (event.eventType === "emote" || event.eventType === "message"))
property bool userMsg: event && event.author.id === Controller.activeConnection.localUserId
property bool showEdit: event && (userMsg && (event.eventType === "emote" || event.eventType === "message"))
property var delegate: null
property var bubble: null
property var hovered: bubble && bubble.hovered
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
@@ -424,7 +450,9 @@ Kirigami.ScrollablePage {
interval: 200
onTriggered: hoverActions.visible = hoverActions.visibleDelayed;
}
x: bubble ? (bubble.x + Kirigami.Units.largeSpacing + Math.max(bubble.width - childWidth, 0) - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0)) : 0
property int childOffset: userMsg && Config.showLocalMessagesOnRight && !Config.compactLayout ? (bubble ? bubble.width : 0) - childWidth : Math.max((bubble ? bubble.width : 0) - childWidth, 0)
x: delegate && bubble ? (delegate.x + bubble.x + Kirigami.Units.largeSpacing + childOffset - (Config.compactLayout ? Kirigami.Units.gridUnit * 3 : 0)) : 0
y: bubble ? bubble.mapToItem(parent, 0, 0).y - hoverActions.childHeight + Kirigami.Units.smallSpacing: 0;
visible: false
@@ -462,7 +490,7 @@ Kirigami.ScrollablePage {
icon.name: "document-edit"
onClicked: {
if (hoverActions.showEdit) {
ChatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
chatBoxHelper.edit(hoverActions.event.message, hoverActions.event.formattedBody, hoverActions.event.eventId)
}
chatBox.focusInputField();
}
@@ -472,7 +500,7 @@ Kirigami.ScrollablePage {
QQC2.ToolTip.visible: hovered
icon.name: "mail-replied-symbolic"
onClicked: {
ChatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
chatBoxHelper.replyToMessage(hoverActions.event.eventId, hoverActions.event.message, hoverActions.event.author);
chatBox.focusInputField();
}
}
@@ -492,14 +520,14 @@ Kirigami.ScrollablePage {
onEditLastUserMessage: {
const targetMessage = messageEventModel.getLastLocalUserMessageEventId();
if (targetMessage) {
ChatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], targetMessage["event_id"]);
chatBoxHelper.edit(targetMessage["message"], targetMessage["formattedBody"], targetMessage["event_id"]);
chatBox.focusInputField();
}
}
onReplyPreviousUserMessage: {
const replyResponse = messageEventModel.getLatestMessageFromIndex(0);
if (replyResponse && replyResponse["event_id"]) {
ChatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["message"], replyResponse["sender_id"]);
chatBoxHelper.replyToMessage(replyResponse["event_id"], replyResponse["message"], replyResponse["sender_id"]);
}
}
}
@@ -588,6 +616,14 @@ Kirigami.ScrollablePage {
return index;
}
// Mark all messages as read if all unread messages are visible to the user
function markReadIfVisible() {
let readMarkerRow = eventToIndex(currentRoom.readMarkerEventId)
if (readMarkerRow > 0 && readMarkerRow < firstVisibleIndex()) {
currentRoom.markAllMessagesAsRead()
}
}
/// Open message context dialog for file and videos
function openFileContext(event, file) {
const contextMenu = fileDelegateContextMenu.createObject(page, {
@@ -598,12 +634,13 @@ Kirigami.ScrollablePage {
file: file,
mimeType: event.mimeType,
progressInfo: event.progressInfo,
plainMessage: event.message,
});
contextMenu.open();
}
/// Open context menu for normal message
function openMessageContext(event, selectedText) {
function openMessageContext(event, selectedText, plainMessage) {
const contextMenu = messageDelegateContextMenu.createObject(page, {
selectedText: selectedText,
author: event.author,
@@ -611,7 +648,8 @@ Kirigami.ScrollablePage {
eventId: event.eventId,
formattedBody: event.formattedBody,
source: event.source,
eventType: event.eventType
eventType: event.eventType,
plainMessage: plainMessage,
});
contextMenu.open();
}

View File

@@ -179,7 +179,7 @@ Kirigami.OverlayDrawer {
wrapMode: Text.WordWrap
selectByMouse: true
color: Kirigami.Theme.textColor
onLinkActivated: Qt.openUrlExternally(link)
onLinkActivated: UrlHelper.openUrl(link)
readOnly: true
MouseArea {
anchors.fill: parent
@@ -217,6 +217,9 @@ Kirigami.OverlayDrawer {
Layout.fillWidth: true
Layout.fillHeight: true
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: userListView
clip: true
@@ -296,7 +299,9 @@ Kirigami.OverlayDrawer {
}
onRoomChanged: {
loader.item.userSearchText = ""
if (loader.active) {
loader.item.userSearchText = ""
}
if (room == null) {
close()
}

View File

@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
import NeoChat.Dialog 1.0
Kirigami.ScrollablePage {
id: root
title: i18n("Edit Account")
property var connection
ColumnLayout {
Kirigami.FormLayout {
RowLayout {
Kirigami.Avatar {
id: avatar
source: root.connection && root.connection.localUser.avatarMediaId ? ("image://mxc/" + root.connection.localUser.avatarMediaId) : ""
MouseArea {
id: mouseArea
anchors.fill: parent
property var fileDialog: null;
onClicked: {
if (fileDialog != null) {
return;
}
fileDialog = openFileDialog.createObject(Controls.ApplicationWindow.Overlay)
fileDialog.chosen.connect(function(receivedSource) {
mouseArea.fileDialog = null;
if (!receivedSource) {
return;
}
parent.source = receivedSource;
});
fileDialog.onRejected.connect(function() {
mouseArea.fileDialog = null;
});
fileDialog.open();
}
}
}
Controls.Button {
visible: avatar.source.toString().length !== 0
icon.name: "edit-clear"
onClicked: avatar.source = ""
}
Kirigami.FormData.label: i18n("Avatar:")
}
Controls.TextField {
id: name
text: root.connection ? root.connection.localUser.displayName : ""
Kirigami.FormData.label: i18n("Name:")
}
Controls.TextField {
id: accountLabel
text: root.connection ? root.connection.localUser.accountLabel : ""
Kirigami.FormData.label: i18n("Label:")
}
Controls.TextField {
id: currentPassword
Kirigami.FormData.label: i18n("Current Password:")
enabled: roto.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: newPassword
Kirigami.FormData.label: i18n("New Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
Controls.TextField {
id: confirmPassword
Kirigami.FormData.label: i18n("Confirm new Password:")
enabled: root.connection !== undefined && root.connection.canChangePassword !== false
echoMode: TextInput.Password
}
}
}
footer: RowLayout {
Item {
Layout.fillWidth: true
}
Controls.Button {
text: i18n("Save")
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
onClicked: {
if (!Controller.setAvatar(root.connection, avatar.source)) {
showPassiveNotification("The Avatar could not be set");
}
if (root.connection.localUser.displayName !== name.text) {
root.connection.localUser.rename(name.text);
}
if (root.connection.localUser.accountLabel !== accountLabel.text) {
root.connection.localUser.setAccountLabel(accountLabel.text);
}
if(currentPassword.text !== "" && newPassword.text !== "" && confirmPassword.text !== "") {
if(newPassword.text === confirmPassword.text) {
Controller.changePassword(root.connection, currentPassword.text, newPassword.text);
} else {
showPassiveNotification(i18n("Passwords do not match"));
return;
}
}
root.closeDialog();
}
}
Controls.Button {
text: i18n("Cancel")
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
onClicked: root.closeDialog();
}
}
}

View File

@@ -23,6 +23,7 @@ Kirigami.ScrollablePage {
ListView {
model: AccountRegistry
anchors.fill: parent
delegate: Kirigami.BasicListItem {
text: model.connection.localUser.displayName
labelItem.textFormat: Text.PlainText
@@ -30,31 +31,39 @@ Kirigami.ScrollablePage {
icon: model.connection.localUser.avatarMediaId ? ("image://mxc/" + model.connection.localUser.avatarMediaId) : "im-user"
onClicked: {
Controller.activeConnection = model.connection
pageStack.layers.pop()
Controller.activeConnection = model.connection;
pageStack.layers.pop();
}
trailing: RowLayout {
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
Controls.ToolTip {
text: parent.action.text
}
action: Kirigami.Action {
text: i18n("Edit this account")
iconName: "document-edit"
onTriggered: {
userEditSheet.connection = model.connection
userEditSheet.open()
}
onTriggered: pageSettingStack.pushDialogLayer(Qt.resolvedUrl('./AccountEditorPage.qml'), {
connection: model.connection
}, {
title: i18n('Account editor')
});
}
}
Controls.ToolButton {
display: Controls.AbstractButton.IconOnly
Controls.ToolTip {
text: parent.action.text
}
action: Kirigami.Action {
text: i18n("Logout")
iconName: "im-kick-user"
onTriggered: {
Controller.logout(model.connection, true)
if(Controller.accountCount === 1)
pageStack.layers.pop()
Controller.logout(model.connection, true);
if (Controller.accountCount === 1) {
pageStack.layers.pop();
}
}
}
}
@@ -76,6 +85,7 @@ Kirigami.ScrollablePage {
}
}
}
Connections {
target: Controller
function onConnectionAdded() {
@@ -83,134 +93,21 @@ Kirigami.ScrollablePage {
pageStack.layers.pop()
}
function onPasswordStatus(status) {
if(status == Controller.Success)
showPassiveNotification(i18n("Password changed successfully"))
else if(status == Controller.Wrong)
showPassiveNotification(i18n("Wrong password entered"))
else
showPassiveNotification(i18n("Unknown problem while trying to change password"))
if (status === Controller.Success) {
showPassiveNotification(i18n("Password changed successfully"));
} else if (status === Controller.Wrong) {
showPassiveNotification(i18n("Wrong password entered"));
} else {
showPassiveNotification(i18n("Unknown problem while trying to change password"));
}
}
}
Component {
property Component openFileDialog: Component {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
}
}
Kirigami.OverlaySheet {
id: userEditSheet
property var connection
title: i18n("Edit Account")
Kirigami.FormLayout {
RowLayout {
Kirigami.Avatar {
id: avatar
source: userEditSheet.connection && userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
MouseArea {
id: mouseArea
anchors.fill: parent
property var fileDialog: null;
onClicked: {
if (fileDialog != null) {
return;
}
fileDialog = openFileDialog.createObject(Controls.ApplicationWindow.Overlay)
fileDialog.chosen.connect(function(receivedSource) {
mouseArea.fileDialog = null;
if (!receivedSource) {
return;
}
parent.source = receivedSource;
});
fileDialog.onRejected.connect(function() {
mouseArea.fileDialog = null;
});
fileDialog.open();
}
}
}
Controls.Button {
visible: avatar.source.toString().length !== 0
icon.name: "edit-clear"
onClicked: avatar.source = ""
}
Kirigami.FormData.label: i18n("Avatar:")
}
Controls.TextField {
id: name
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
}
RowLayout {
Controls.Button {
text: i18n("Save")
onClicked: {
if(!Controller.setAvatar(userEditSheet.connection, avatar.source))
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)
} else {
showPassiveNotification(i18n("Passwords do not match"))
return
}
}
userEditSheet.close()
currentPassword.text = ""
newPassword.text = ""
confirmPassword.text = ""
}
}
Controls.Button {
text: i18n("Cancel")
onClicked: {
userEditSheet.close()
avatar.source = userEditSheet.connection.localUser.avatarMediaId ? ("image://mxc/" + userEditSheet.connection.localUser.avatarMediaId) : ""
currentPassword.text = ""
newPassword.text = ""
confirmPassword.text = ""
}
}
}
}
}
}

View File

@@ -245,7 +245,7 @@ Kirigami.ScrollablePage {
QQC2.CheckBox {
text: i18n("Show your messages on the right")
checked: Config.showLocalMessagesOnRight
enabled: !Config.isShowLocalMessagesOnRightImmutable
enabled: !Config.isShowLocalMessagesOnRightImmutable && !Config.compactLayout
onToggled: {
Config.showLocalMessagesOnRight = checked
Config.save()

View File

@@ -17,8 +17,10 @@ Kirigami.ScrollablePage {
id: devices
}
anchors.fill: parent
Kirigami.PlaceholderMessage {
visible: parent.model.count === 0 // We can assume 0 means loading since there is at least one device
visible: parent.count === 0 // We can assume 0 means loading since there is at least one device
anchors.centerIn: parent
text: i18n("Loading…")
Controls.BusyIndicator {

View File

@@ -19,9 +19,10 @@ Kirigami.ScrollablePage {
title: i18nc('@title:window', 'Custom Emojis')
ListView {
anchors.fill: parent
model: CustomEmojiModel {
id: emojiModel
connection: Controller.activeConnection
}
@@ -102,7 +103,6 @@ Kirigami.ScrollablePage {
this.fileDialog = null
})
this.fileDialog.onRejected.connect(() => {
rej()
this.fileDialog = null
})
this.fileDialog.open()

View File

@@ -14,6 +14,7 @@ Kirigami.ScrollablePage {
title: i18nc('@title:window', 'General')
ColumnLayout {
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.CheckBox {
Kirigami.FormData.label: i18n("General settings:")
text: i18n("Close to system tray")
@@ -96,19 +97,16 @@ Kirigami.ScrollablePage {
QQC2.CheckBox {
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
}
text: i18n("Use s/text/replacement syntax to edit your last message")
checked: Config.allowQuickEdit
enabled: !Config.isAllowQuickEditImmutable
onToggled: {
Config.allowQuickEdit = checked
Config.save()
}
// TODO KF5.97 remove this line
Component.onCompleted: this.contentItem.wrap = QQC2.Label.Wrap
}
QQC2.CheckBox {
text: i18n("Send Typing Notifications")
@@ -121,12 +119,16 @@ Kirigami.ScrollablePage {
}
QQC2.CheckBox {
text: i18n("Automatically hide/unhide the room information when resizing the window")
Layout.maximumWidth: parent.width
checked: Config.autoRoomInfoDrawer
enabled: !Config.isAutoRoomInfoDrawerImmutable
onToggled: {
Config.autoRoomInfoDrawer = checked
Config.save()
}
// TODO KF5.97 remove this line
Component.onCompleted: this.contentItem.wrap = QQC2.Label.Wrap
}
}
}

View File

@@ -190,6 +190,10 @@ Kirigami.Page {
Layout.fillHeight: true
enabled: autodetectLanguageCheckbox.checked
Component.onCompleted: background.visible = wideMode
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
clip: true
model: settings.dictionaryModel
@@ -254,6 +258,10 @@ Kirigami.Page {
}
QQC2.ScrollView {
anchors.fill: parent
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
ListView {
model: settings.currentIgnoreList
delegate: Kirigami.BasicListItem {

View File

@@ -44,7 +44,7 @@
<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>
<summary xml:lang="ca-valencia">Un client per a Matrix, el protocol de comunicacions descentralitzat</summary>
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
@@ -102,7 +102,7 @@
<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 NeoChat implementa una gran part del protocol amb l'excepció dels chats encriptats i els chats 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>
@@ -129,7 +129,7 @@
<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">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 en 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -407,7 +407,7 @@ Kirigami.ApplicationWindow {
}
footer: QQC2.Button {
text: i18n("Open")
onClicked: Qt.openUrlExternally(consentSheet.url)
onClicked: UrlHelper.openUrl(consentSheet.url)
}
}

View File

@@ -80,6 +80,7 @@
<file>imports/NeoChat/Settings/Emoticons.qml</file>
<file>imports/NeoChat/Settings/AppearanceSettingsPage.qml</file>
<file>imports/NeoChat/Settings/AccountsPage.qml</file>
<file>imports/NeoChat/Settings/AccountEditorPage.qml</file>
<file>imports/NeoChat/Settings/DevicesPage.qml</file>
<file>imports/NeoChat/Settings/About.qml</file>
<file>imports/NeoChat/Settings/SonnetConfigPage.qml</file>

View File

@@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: BSD-2-Clause
add_executable(neochat
add_library(neochat STATIC
controller.cpp
actionshandler.cpp
emojimodel.cpp
@@ -14,6 +14,8 @@ add_executable(neochat
messageeventmodel.cpp
messagefiltermodel.cpp
roomlistmodel.cpp
sortfilterspacelistmodel.cpp
spacehierarchycache.cpp
roommanager.cpp
neochatroom.cpp
neochatuser.cpp
@@ -21,7 +23,6 @@ add_executable(neochat
publicroomlistmodel.cpp
userdirectorylistmodel.cpp
utils.cpp
main.cpp
notificationsmanager.cpp
sortfilterroomlistmodel.cpp
chatdocumenthandler.cpp
@@ -36,11 +37,22 @@ add_executable(neochat
blurhashimageprovider.cpp
joinrulesevent.cpp
collapsestateproxymodel.cpp
urlhelper.cpp
)
add_executable(neochat-app
main.cpp
../res.qrc
)
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
neochat
)
if(Quotient_VERSION_MINOR GREATER 6)
target_compile_definitions(neochat PRIVATE QUOTIENT_07)
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
else()
target_sources(neochat PRIVATE neochataccountregistry.cpp)
endif()
@@ -56,34 +68,37 @@ if(NOT ANDROID)
else()
target_sources(neochat PRIVATE trayicon.cpp)
endif()
target_link_libraries(neochat PRIVATE KF5::ConfigWidgets KF5::WindowSystem KF5::SonnetCore)
target_compile_definitions(neochat PRIVATE -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PRIVATE -DHAVE_WINDOWSYSTEM)
target_link_libraries(neochat PUBLIC KF5::ConfigWidgets KF5::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
target_compile_definitions(neochat PUBLIC -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)
target_sources(neochat-app PRIVATE ../res_desktop.qrc)
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
target_sources(neochat PRIVATE runner.cpp)
else()
target_sources(neochat PRIVATE ../res_android.qrc)
target_sources(neochat-app 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})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia 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)
target_link_libraries(neochat PUBLIC QCoro5::Coro)
else()
target_link_libraries(neochat PRIVATE QCoro::QCoro)
target_link_libraries(neochat PUBLIC QCoro::QCoro)
endif()
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK)
target_compile_definitions(neochat PRIVATE NEOCHAT_FLATPAK)
target_compile_definitions(neochat PUBLIC NEOCHAT_FLATPAK)
endif()
if(ANDROID)
target_sources(neochat PRIVATE notifyrc.qrc)
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL)
target_sources(neochat-app PRIVATE notifyrc.qrc)
target_link_libraries(neochat PUBLIC Qt::Svg OpenSSL::SSL)
kirigami_package_breeze_icons(ICONS
"arrow-down"
"help-about"
@@ -125,20 +140,22 @@ if(ANDROID)
"preferences-desktop-theme-global"
)
else()
target_link_libraries(neochat PRIVATE Qt5::Widgets KF5::KIOWidgets)
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
endif()
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
if(TARGET KF5::DBusAddons)
target_link_libraries(neochat PRIVATE KF5::DBusAddons)
target_compile_definitions(neochat PRIVATE -DHAVE_KDBUSADDONS)
target_link_libraries(neochat PUBLIC KF5::DBusAddons)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF5::KIOWidgets)
target_compile_definitions(neochat PRIVATE -DHAVE_KIO)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
install(TARGETS neochat ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
install(TARGETS neochat-app ${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)

View File

@@ -131,8 +131,8 @@ void ChatBoxHelper::clear()
setEditContent(QString());
setReplyEventId(QString());
setReplyEventContent(QString());
setAttachmentPath(QString());
setReplyUser(QVariant());
setAttachmentPath(QString());
}
void ChatBoxHelper::edit(const QString &message, const QString &formattedBody, const QString &eventId)
@@ -149,6 +149,7 @@ void ChatBoxHelper::clearEditReply()
setReplyEventId(QString());
setReplyEventContent(QString());
setReplyUser(QVariant());
setAttachmentPath(QString());
Q_EMIT shouldClearText();
}

View File

@@ -5,11 +5,11 @@
#include <QFont>
#include <QObject>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QUrl>
class QTextDocument;
class QQuickTextDocument;
class NeoChatRoom;
class Controller;

View File

@@ -21,14 +21,15 @@
#include <QFileInfo>
#include <QGuiApplication>
#include <QMovie>
#include <QNetworkConfigurationManager>
#include <QNetworkReply>
#include <QPixmap>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QQuickWindow>
#include <QStandardPaths>
#include <QStringBuilder>
#include <QSysInfo>
#include <QTextDocument>
#include <QTimer>
#include <utility>
@@ -52,6 +53,7 @@
#include "roommanager.h"
#include "settings.h"
#include "utils.h"
#include <qt_connection_util.h>
#include <KStandardShortcut>
@@ -123,13 +125,6 @@ Controller::Controller(QObject *parent)
#endif
}
Controller::~Controller()
{
for (auto c : AccountRegistry::instance().accounts()) {
c->saveState();
}
}
Controller &Controller::instance()
{
static Controller _instance;
@@ -701,3 +696,17 @@ bool Controller::hasWindowSystem() const
return false;
#endif
}
QString Controller::plainText(QQuickTextDocument *document) const
{
return document->textDocument()->toPlainText();
}
bool Controller::encryptionSupported() const
{
#ifdef QUOTIENT_07
return Quotient::encryptionSupported();
#else
return false;
#endif
}

View File

@@ -21,6 +21,7 @@ class NeoChatRoom;
class NeoChatUser;
class TrayIcon;
class QQuickWindow;
class QQuickTextDocument;
namespace QKeychain
{
@@ -40,6 +41,7 @@ class Controller : public QObject
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
Q_PROPERTY(bool hasWindowSystem READ hasWindowSystem CONSTANT)
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
Q_PROPERTY(bool encryptionSupported READ encryptionSupported CONSTANT)
public:
static Controller &instance();
@@ -94,10 +96,11 @@ public:
Q_INVOKABLE void setBlur(QQuickItem *item, bool blur);
Q_INVOKABLE void raiseWindow(QWindow *window);
Q_INVOKABLE QString plainText(QQuickTextDocument *document) const;
bool encryptionSupported() const;
private:
explicit Controller(QObject *parent = nullptr);
~Controller() override;
QPointer<Connection> m_connection;
bool m_busy = false;

View File

@@ -6,6 +6,7 @@
#include "controller.h"
#include <QUrl>
#include <qt_connection_util.h>
#include <KLocalizedString>

View File

@@ -69,6 +69,9 @@
#include "roomlistmodel.h"
#include "roommanager.h"
#include "sortfilterroomlistmodel.h"
#include "sortfilterspacelistmodel.h"
#include "spacehierarchycache.h"
#include "urlhelper.h"
#include "userdirectorylistmodel.h"
#include "userlistmodel.h"
#include "webshortcutmodel.h"
@@ -175,6 +178,8 @@ int main(int argc, char *argv[])
Login *login = new Login();
ChatBoxHelper chatBoxHelper;
UrlHelper urlHelper;
SpaceHierarchyCache spaceHierarchyCache;
#ifdef HAVE_COLORSCHEME
ColorSchemer colorScheme;
@@ -190,11 +195,13 @@ int main(int argc, char *argv[])
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "RoomManager", &RoomManager::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "ChatBoxHelper", &chatBoxHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "UrlHelper", &urlHelper);
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "EmojiModel", new EmojiModel(&app));
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CommandModel", new CommandModel(&app));
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Quotient::AccountRegistry::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &spaceHierarchyCache);
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatBoxHelper>("org.kde.neochat", 1, 0, "ChatBoxHelper");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");
qmlRegisterType<KWebShortcutModel>("org.kde.neochat", 1, 0, "WebShortcutModel");
@@ -206,6 +213,7 @@ int main(int argc, char *argv[])
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
qmlRegisterType<SortFilterRoomListModel>("org.kde.neochat", 1, 0, "SortFilterRoomListModel");
qmlRegisterType<SortFilterSpaceListModel>("org.kde.neochat", 1, 0, "SortFilterSpaceListModel");
qmlRegisterType<DevicesModel>("org.kde.neochat", 1, 0, "DevicesModel");
qmlRegisterUncreatableType<RoomMessageEvent>("org.kde.neochat", 1, 0, "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<NeoChatRoomType>("org.kde.neochat", 1, 0, "NeoChatRoomType", "ENUM");
@@ -228,7 +236,9 @@ int main(int argc, char *argv[])
});
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Emoji>();
#endif
QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));

View File

@@ -193,6 +193,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole});
});
connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent);
@@ -714,9 +715,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r);
if (data(i, SpecialMarksRole) != EventStatus::Hidden) {
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) != data(idx, EventTypeRole)
|| data(idx, TimeRole).toDateTime().msecsTo(data(i, TimeRole).toDateTime()) > 600000
|| data(idx, TimeRole).toDateTime().toLocalTime().date().day() != data(i, TimeRole).toDateTime().toLocalTime().date().day();
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
}
}

View File

@@ -6,12 +6,16 @@
#include <cmark.h>
#include <QFileInfo>
#include <QImageReader>
#include <QMetaObject>
#include <QMimeDatabase>
#include <QTextDocument>
#include <functional>
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
#include <QMediaMetaData>
#include <QMediaPlayer>
#endif
#include <qcoro/qcorosignal.h>
#include <qcoro/task.h>
@@ -26,6 +30,7 @@
#include "events/accountdataevents.h"
#include "events/reactionevent.h"
#include "events/roomcanonicalaliasevent.h"
#include "events/roomcreateevent.h"
#include "events/roommessageevent.h"
#include "events/roompowerlevelsevent.h"
#include "events/typingevent.h"
@@ -35,6 +40,8 @@
#include "stickerevent.h"
#include "user.h"
#include "utils.h"
#include <events/eventcontent.h>
#include <qt_connection_util.h>
#include <KLocalizedString>
@@ -76,12 +83,38 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
}
void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
{
doUploadFile(url, body);
}
QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
{
if (url.isEmpty()) {
return;
co_return;
}
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
auto mime = QMimeDatabase().mimeTypeForUrl(url);
QFileInfo fileInfo(url.toLocalFile());
EventContent::TypedBase *content;
if (mime.name().startsWith("image/")) {
QImage image;
content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName());
} else if (mime.name().startsWith("audio/")) {
content = new EventContent::AudioContent(url, fileInfo.size(), mime, fileInfo.fileName());
} else if (mime.name().startsWith("video/")) {
QMediaPlayer player;
player.setSource(url);
co_await qCoro(&player, &QMediaPlayer::mediaStatusChanged);
auto resolution = player.metaData().value(QMediaMetaData::Resolution).toSize();
content = new EventContent::VideoContent(url, fileInfo.size(), mime, resolution, fileInfo.fileName());
} else {
content = new EventContent::FileContent(url, fileInfo.size(), mime, fileInfo.fileName());
}
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, content);
#else
QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false);
#endif
setHasFileUploading(true);
#ifdef QUOTIENT_07
connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
@@ -602,7 +635,7 @@ void NeoChatRoom::removeLocalAlias(const QString &alias)
QString NeoChatRoom::markdownToHTML(const QString &markdown)
{
const auto str = markdown.toUtf8();
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_DEFAULT);
char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_HARDBREAKS);
const std::string html(tmp_buf);
@@ -610,11 +643,9 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
auto result = QString::fromStdString(html).trimmed();
result.replace("<!-- raw HTML omitted -->", "<br />");
result.replace(QRegularExpression("(<br />)*$"), "");
result.replace("<!-- raw HTML omitted -->", "");
result.replace("<p>", "");
result.replace("</p>", "");
result.replace("\n", "<br>");
return result;
}
@@ -840,3 +871,17 @@ void NeoChatRoom::clearInvitationNotification()
{
NotificationsManager::instance().clearInvitationNotification(id());
}
bool NeoChatRoom::isSpace()
{
const auto creationEvent = this->creation();
if (!creationEvent) {
return false;
}
#ifdef QUOTIENT_07
return creationEvent->roomType() == RoomType::Space;
#else
return false;
#endif
}

View File

@@ -72,6 +72,8 @@ public:
/// \see lastEventToString
[[nodiscard]] QString subtitleText();
[[nodiscard]] bool isSpace();
bool isEventHighlighted(const Quotient::RoomEvent *e) const;
[[nodiscard]] QString joinRule() const;
@@ -151,6 +153,7 @@ private:
static QString markdownToHTML(const QString &markdown);
QCoro::Task<void> doDeleteMessagesByUser(const QString &user);
QCoro::Task<void> doUploadFile(QUrl url, QString body = QString());
private Q_SLOTS:
void countChanged();

View File

@@ -408,6 +408,10 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == IdRole) {
return room->id();
}
if (role == IsSpaceRole) {
return room->isSpace();
}
return QVariant();
}
@@ -439,6 +443,8 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[CurrentRoomRole] = "currentRoom";
roles[CategoryVisibleRole] = "categoryVisible";
roles[SubtitleTextRole] = "subtitleText";
roles[IsSpaceRole] = "isSpace";
roles[IdRole] = "id";
return roles;
}

View File

@@ -52,6 +52,7 @@ public:
SubtitleTextRole,
AvatarImageRole,
IdRole,
IsSpaceRole,
};
Q_ENUM(EventRoles)
@@ -99,6 +100,7 @@ private:
QMap<int, bool> m_categoryVisibility;
int m_notificationCount = 0;
QString m_activeSpaceId = "";
void connectRoomSignals(NeoChatRoom *room);
void handleNotifications();

View File

@@ -11,8 +11,13 @@
#include <QDesktopServices>
#include <QStandardPaths>
#include <csapi/joining.h>
#include <qt_connection_util.h>
#include <utility>
#ifndef Q_OS_ANDROID
#include <KIO/OpenUrlJob>
#endif
RoomManager::RoomManager(QObject *parent)
: QObject(parent)
, m_currentRoom(nullptr)
@@ -191,9 +196,19 @@ void RoomManager::joinRoom(Quotient::Connection *account, const QString &roomAli
bool RoomManager::visitNonMatrix(const QUrl &url)
{
#ifdef Q_OS_ANDROID
if (!QDesktopServices::openUrl(url)) {
Q_EMIT warning(i18n("No application for the link"), i18n("Your operating system could not find an application for the link."));
}
#else
auto *job = new KIO::OpenUrlJob(url);
connect(job, &KJob::finished, this, [this](KJob *job) {
if (job->error()) {
Q_EMIT warning(i18n("Could not open URL"), job->errorString());
}
});
job->start();
#endif
return true;
}

View File

@@ -76,6 +76,23 @@ QString SortFilterRoomListModel::filterText() const
bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded";
bool acceptRoom = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::NameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded"
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceRooms.empty())
return acceptRoom;
else
return std::find(m_activeSpaceRooms.begin(),
m_activeSpaceRooms.end(),
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IdRole).toString())
!= m_activeSpaceRooms.end()
&& acceptRoom;
}
void SortFilterRoomListModel::setActiveSpaceRooms(QVector<QString> activeSpaceRooms)
{
this->m_activeSpaceRooms = activeSpaceRooms;
invalidate();
}

View File

@@ -11,6 +11,7 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
Q_PROPERTY(RoomSortOrder roomSortOrder READ roomSortOrder WRITE setRoomSortOrder NOTIFY roomSortOrderChanged)
Q_PROPERTY(QString filterText READ filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged)
Q_PROPERTY(QVector<QString> activeSpaceRooms WRITE setActiveSpaceRooms)
public:
enum RoomSortOrder {
@@ -30,6 +31,8 @@ public:
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
Q_INVOKABLE void setActiveSpaceRooms(QVector<QString> activeSpaceRooms);
protected:
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
@@ -40,4 +43,5 @@ Q_SIGNALS:
private:
RoomSortOrder m_sortOrder = Categories;
QString m_filterText;
QVector<QString> m_activeSpaceRooms;
};

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "sortfilterspacelistmodel.h"
#include "roomlistmodel.h"
SortFilterSpaceListModel::SortFilterSpaceListModel(QObject *parent)
: QSortFilterProxyModel{parent}
{
setSortRole(RoomListModel::IdRole);
sort(0);
invalidateFilter();
}
bool SortFilterSpaceListModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent);
return sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool()
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() != "upgraded";
}
bool SortFilterSpaceListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
const auto idLeft = sourceModel()->data(source_left, RoomListModel::IdRole).toString();
const auto idRight = sourceModel()->data(source_right, RoomListModel::IdRole).toString();
return idLeft < idRight;
}

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#pragma once
#include <QSortFilterProxyModel>
class SortFilterSpaceListModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit SortFilterSpaceListModel(QObject *parent = nullptr);
[[nodiscard]] QString activeSpaceId() const;
[[nodiscard]] bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
Q_SIGNALS:
void activeSpaceIdChanged(QString &activeSpaceId);
protected:
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
};

View File

@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#include "spacehierarchycache.h"
#include "controller.h"
#ifdef QUOTIENT_07
#include "csapi/space_hierarchy.h"
#endif
#include "neochatroom.h"
SpaceHierarchyCache::SpaceHierarchyCache(QObject *parent)
: QObject{parent}
{
cacheSpaceHierarchy();
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
cacheSpaceHierarchy();
});
}
void SpaceHierarchyCache::cacheSpaceHierarchy()
{
#ifdef QUOTIENT_07
auto connection = Controller::instance().activeConnection();
if (!connection) {
return;
}
const auto roomList = connection->allRooms();
for (const auto &room : roomList) {
const auto neoChatRoom = static_cast<NeoChatRoom *>(room);
if (neoChatRoom->isSpace()) {
populateSpaceHierarchy(neoChatRoom->id());
} else {
connect(neoChatRoom, &Room::baseStateLoaded, neoChatRoom, [this, neoChatRoom]() {
if (neoChatRoom->isSpace()) {
populateSpaceHierarchy(neoChatRoom->id());
}
});
}
}
#endif
}
void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId)
{
auto connection = Controller::instance().activeConnection();
if (!connection) {
return;
}
#ifdef QUOTIENT_07
GetSpaceHierarchyJob *job = connection->callApi<GetSpaceHierarchyJob>(spaceId);
connect(job, &BaseJob::success, this, [this, job, spaceId]() {
const auto rooms = job->rooms();
QVector<QString> roomList;
for (unsigned long i = 0; i < rooms.size(); ++i) {
for (const auto &state : rooms[i].childrenState) {
roomList.push_back(state->stateKey());
}
roomList.push_back(rooms.at(i).roomId);
}
m_spaceHierarchy.insert(spaceId, roomList);
Q_EMIT spaceHierarchyChanged();
});
#endif
}
QVector<QString> SpaceHierarchyCache::getRoomListForSpace(const QString &spaceId, bool updateCache)
{
if (updateCache) {
populateSpaceHierarchy(spaceId);
}
return m_spaceHierarchy[spaceId];
}

28
src/spacehierarchycache.h Normal file
View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2022 Snehit Sah <hi@snehit.dev>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
#pragma once
#include <QHash>
#include <QObject>
#include <QString>
#include <QVector>
class SpaceHierarchyCache : public QObject
{
Q_OBJECT
public:
explicit SpaceHierarchyCache(QObject *parent = nullptr);
[[nodiscard]] Q_INVOKABLE QVector<QString> getRoomListForSpace(const QString &spaceId, bool updateCache);
Q_SIGNALS:
void spaceHierarchyChanged();
private:
QVector<QString> m_activeSpaceRooms;
QHash<QString, QVector<QString>> m_spaceHierarchy;
void cacheSpaceHierarchy();
void populateSpaceHierarchy(const QString &spaceId);
};

24
src/urlhelper.cpp Normal file
View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "urlhelper.h"
#include <QtGlobal>
#ifdef Q_OS_ANDROID
#include <QDesktopServices>
#else
#include <KIO/OpenUrlJob>
#endif
// QDesktopServices::openUrl doesn't support XDG activation yet, OpenUrlJob does
// On Android XDG activation is not relevant, so use QDesktopServices::openUrl to avoid the heavy KIO dependency
void UrlHelper::openUrl(const QUrl &url)
{
#ifdef Q_OS_ANDROID
QDesktopServices::openUrl(url);
#else
auto *job = new KIO::OpenUrlJob(url);
job->start();
#endif
}

13
src/urlhelper.h Normal file
View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
class UrlHelper : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE void openUrl(const QUrl &url);
};