Compare commits

...

100 Commits

Author SHA1 Message Date
Tobias Fella
de3d3abc53 Port away from and remove MatrixImageProvider
The functionality was redundant with the custom QNAM extension libQuotient has
2023-12-02 22:30:18 +01:00
James Graham
46fd288a95 Add some clarification around around offical and canonical parents 2023-12-02 13:21:26 +00:00
Joshua Goins
3a1cac6c99 Remove ffmpeg from Android build
This doesn't work on Android right now, and should allow the apk to at least launch.
2023-12-02 03:53:55 +00:00
l10n daemon script
225092a193 GIT_SILENT Sync po/docbooks with svn 2023-12-01 02:13:35 +00:00
Heiko Becker
2fcb17621c GIT_SILENT Update Appstream for new release
(cherry picked from commit 17b14aa556)
2023-11-30 18:35:03 +01:00
l10n daemon script
69e3ee862f GIT_SILENT Sync po/docbooks with svn 2023-11-30 02:08:31 +00:00
James Graham
47c12cb582 MessageEventModel test
Add basic test suite for MessageEventModel
2023-11-29 19:06:43 +00:00
l10n daemon script
94cf75af68 GIT_SILENT Sync po/docbooks with svn 2023-11-29 02:12:57 +00:00
ivan tkachenko
259b9884c7 Set WrapMode on all TextArea components, and fix up some of the Labels
Upstream styles do not provide any wrapping by default, and thus we
should not rely on deceptively convenient override in qqc2-desktop-style.

See https://invent.kde.org/frameworks/qqc2-desktop-style/-/merge_requests/331
2023-11-28 22:26:57 +00:00
l10n daemon script
2980dc49e4 GIT_SILENT Sync po/docbooks with svn 2023-11-28 02:11:26 +00:00
Tobias Fella
5e12f50899 Wrap text in poll delegates
BUG: 477629
2023-11-27 20:44:19 +00:00
James Graham
79940f707f By default only show leave/join state events
As discussed in network/neochat#609
2023-11-27 18:15:48 +00:00
l10n daemon script
e7024a270c GIT_SILENT Sync po/docbooks with svn 2023-11-27 02:12:56 +00:00
Tobias Fella
1a561f6649 Fix linking on windows 2023-11-26 17:40:50 +01:00
Tobias Fella
2cbb6ed65c Enable windows qt6 CI 2023-11-26 15:22:26 +00:00
James Graham
ab4639926a Fix isEdit Check MessageEditComponent
Stop the console being pinged with qrc:/org/kde/neochat/qml/MessageEditComponent.qml:158: TypeError: Cannot read property 'isEditing' of undefined.

Fixes network/neochat#629
2023-11-26 15:21:18 +00:00
Tobias Fella
89b6c54f25 Fix state delegate spacing 2023-11-26 15:06:35 +01:00
James Graham
27c9c62564 ResolveResource
Use ResolveResource rather than calling individual functions like visit user and room
2023-11-26 12:23:28 +00:00
l10n daemon script
bb8ffb02d1 GIT_SILENT Sync po/docbooks with svn 2023-11-26 02:48:22 +00:00
James Graham
201aa82c04 Remove controller::joinRoom
Remove controller::joinRoom in favour of using RoomManager::OpenResource
2023-11-25 15:07:15 +00:00
l10n daemon script
704430db7a GIT_SILENT Sync po/docbooks with svn 2023-11-25 02:08:53 +00:00
Albert Astals Cid
dcbbbd9296 GIT_SILENT Upgrade release service version to 24.01.80. 2023-11-25 00:51:46 +01:00
Tobias Fella
410258c478 Join rooms when visiting them
BUG: 477261
2023-11-24 19:11:15 +01:00
Tobias Fella
6baf2e4888 Fix crash for invalid media uris
BUG: 476698
2023-11-24 18:50:30 +01:00
Tobias Fella
dd6eaac556 Don't crash when sticker pack room doesn't exist
BUG: 476923
2023-11-24 16:43:16 +01:00
l10n daemon script
840903128c GIT_SILENT Sync po/docbooks with svn 2023-11-24 02:10:11 +00:00
l10n daemon script
68f0ca96da GIT_SILENT Sync po/docbooks with svn 2023-11-23 02:13:47 +00:00
l10n daemon script
faf7af06fe GIT_SILENT Sync po/docbooks with svn 2023-11-22 02:09:37 +00:00
l10n daemon script
4ef67c3e0d GIT_SILENT Sync po/docbooks with svn 2023-11-21 02:09:50 +00:00
James Graham
5efd17d370 Loading and End of Timeline Delegates
Add delegate for showing the user a loading indicator and for the beginning of the timeline.

BUG: 455045
BUG: 465285
2023-11-20 17:10:56 +00:00
l10n daemon script
0dbef58ff2 GIT_SILENT Sync po/docbooks with svn 2023-11-20 02:44:00 +00:00
Tobias Fella
6a3df8baf4 Fix initial visual focus in welcomepage 2023-11-19 14:09:08 +01:00
l10n daemon script
2fc973f218 GIT_SILENT Sync po/docbooks with svn 2023-11-19 02:26:37 +00:00
l10n daemon script
7d16999c44 GIT_SILENT Sync po/docbooks with svn 2023-11-18 02:09:37 +00:00
Laurent Montel
f9ba31f2dc Remove unused variables 2023-11-17 10:23:58 +00:00
Laurent Montel
d4ad773ff1 Remove unused "this" argument 2023-11-17 09:36:35 +00:00
Laurent Montel
822a4dc500 Use nullptr 2023-11-17 07:34:46 +00:00
l10n daemon script
bfd1d431c1 GIT_SILENT Sync po/docbooks with svn 2023-11-17 02:12:16 +00:00
Carl Schwan
94bf2481f0 Fix spacing in login screen 2023-11-16 22:43:45 +01:00
Tobias Fella
5c32520c35 Add appium test for opening the user details sheet and fix some accessibility problems 2023-11-16 19:47:13 +01:00
l10n daemon script
de47597f6e GIT_SILENT Sync po/docbooks with svn 2023-11-16 02:21:25 +00:00
l10n daemon script
7780a72888 GIT_SILENT Sync po/docbooks with svn 2023-11-15 02:13:48 +00:00
Carl Schwan
1f8c07fedf Make room timeline non interactible
Currently trying to scroll with this scrollbar is probablematic and it's
unlikely we will find an easy solution to fix it, so make it non interactible.
2023-11-14 19:16:02 +00:00
Tobias Fella
877575b4d3 Fix kirigami name 2023-11-14 19:18:02 +01:00
l10n daemon script
1181df4db2 GIT_SILENT Sync po/docbooks with svn 2023-11-14 02:12:52 +00:00
Joshua Goins
5be2113b32 Add matrix: URL to push notifications 2023-11-13 20:02:07 +00:00
l10n daemon script
7ad7af56e8 GIT_SILENT Sync po/docbooks with svn 2023-11-13 02:12:13 +00:00
Joshua Goins
d3148f8c8b Don't start the entire NeoChat backend when receiving push notifications
This adds a dedicated "set up for push notifications only" function in
Controller, which only sets up the KUnifiedPush connector for receiving
the message and then quitting right afterward. If the user tries to
open a notification, then it will quit and open the main client.
2023-11-12 17:08:15 -05:00
Joshua Goins
100f595026 Add handler for KUnifiedPush messageReceived events 2023-11-12 21:46:33 +00:00
Joshua Goins
15ba6d58e2 Only attempt to setup push notifications on the first syncDone 2023-11-12 15:53:15 -05:00
James Graham
61ad892732 Handle multiple line names
Add function to get the display name for an author on a single line as nothing stops there being linebreaks.

BUG:476731
2023-11-12 18:46:04 +00:00
James Graham
2a3e1dfcd7 Read Marker Hidden
Only show the read marker if there is a non-hidden event earlier in the timeline

fixes network/neochat#615
2023-11-12 16:32:08 +00:00
Joshua Goins
d1dc6fc4ed Fix typo preventing APK from building 2023-11-12 15:36:38 +00:00
James Graham
6dc30a9ca7 Fix hover action position when wide
Fix the positioning of the hover actions when the window is wide by using the content x pos
2023-11-12 14:20:46 +00:00
James Graham
ae0c5ffaef Improve State Text Translatability
Make the state state strings less ambiguous for the purpose of translation.

BUG: 476358
2023-11-12 14:08:36 +00:00
Tobias Fella
fc546d4a43 Add notifications view 2023-11-12 13:16:09 +00:00
Tobias Fella
96e62e3ebe Disable system tray integration by default 2023-11-12 12:46:16 +00:00
Joshua Goins
d979cd2fbc Add Snap Store link in README 2023-11-12 04:01:26 +00:00
l10n daemon script
22298181cb GIT_SILENT Sync po/docbooks with svn 2023-11-12 02:22:14 +00:00
Tobias Fella
1312fde470 Cleanup code 2023-11-12 01:46:23 +01:00
Tobias Fella
7fe2feb1e4 Make NeoChat not shut down when disabling close-to-tray 2023-11-12 01:46:06 +01:00
Ingo Klöcker
67fb5d0824 Add option used on Binary Factory 2023-11-11 22:06:59 +00:00
Ingo Klöcker
359114bd3d Tell Craft that we need Qt 6 and master of the dependencies 2023-11-11 22:06:59 +00:00
Ingo Klöcker
3db9f1198b Add CI template for building APKs 2023-11-11 22:06:59 +00:00
Tobias Fella
2435a6b953 Fix opening account editor 2023-11-11 23:06:26 +01:00
Tobias Fella
e6c8b3fa4b Simplify some android code 2023-11-11 22:59:27 +01:00
Tobias Fella
2d55dca508 Don't allow changing the power level for users with a power level higher or equal to ours
The server won't allow it anyway
2023-11-11 18:20:54 +00:00
Tobias Fella
bbb0bc3092 Improve and fix powerlevels dialog 2023-11-11 17:37:27 +00:00
James Graham
4065aa6a2e Fix linkpreview tooltip
Fix linkpreview tooltip so that the correct text is always shown.

BUG: 467106
2023-11-11 17:29:15 +00:00
Tobias Fella
5942eac5ed Force plaintext in permissions settings 2023-11-11 18:09:03 +01:00
Tobias Fella
85c2b7dada Fix crash on shutdown 2023-11-11 16:03:35 +01:00
Carl Schwan
86ef921cdb Fix alignment user info
Otherwise we end up with two times largeSpacing as right margin for the
user info.
2023-11-11 15:40:15 +01:00
James Graham
aab69c5bae Suggested rooms spaces
Add the ability to set and show suggested rooms for spaces. 

This is just adding the basic functionality, we can do more things with it later like sort/filter the space home for example.
2023-11-11 13:32:19 +00:00
Carl Schwan
624578ec77 Improve welcome page
- Don't use card for welcome message and instead use same layout as
  kwordquiz
- Add separator between login and register button
- Add type annotation in functions
2023-11-11 13:12:56 +00:00
l10n daemon script
197ff984fd GIT_SILENT Sync po/docbooks with svn 2023-11-11 02:19:28 +00:00
James Graham
a26337d5f4 Fix ServerListModel
The server list needs to be populated on connection change as this is instantiated from QML
2023-11-10 20:32:51 +00:00
Joshua Goins
31b4eefadd Move closeDialog signal from Loading to LoginStep
This prevents errors because we try to connect to a non-existent signal
if the step is anything but Loading.
2023-11-10 18:40:37 +00:00
Joshua Goins
b8e592f8ba Fix wrong argument being passed into maximize component creation
It's supposed to refer to the attached property on QQC2.Overlay, not
QQC2.ApplicationWindow
2023-11-10 18:36:49 +00:00
Joshua Goins
555d23863e Port to QEvent::ApplicationPaletteChange
The paletteChanged on QGuiApplication is deprecated in Qt6
2023-11-10 18:30:27 +00:00
Joshua Goins
ffa2d5dc0e Fix push notification registration
Now it should regularly happen after login
2023-11-10 18:08:52 +00:00
Joshua Goins
9987edbaf2 Note that profileTag is intentionally left empty 2023-11-10 18:08:52 +00:00
Joshua Goins
d90298392d Only run setupPushNotification once for new accounts 2023-11-10 18:08:52 +00:00
Joshua Goins
600dbd0603 Remove unnecessary activeConnection() call in setupPushNotifications 2023-11-10 18:08:52 +00:00
Joshua Goins
2179e2cc35 Fix QCoro headers 2023-11-10 18:08:52 +00:00
Joshua Goins
8119ea3ccb Use auto in NeoChatConnection::setupPushNotifications 2023-11-10 18:08:52 +00:00
Joshua Goins
3fc125a798 Move push notification setup to NeoChatConnection 2023-11-10 18:08:52 +00:00
Joshua Goins
99f6778df4 Register for push notifications
This sets it up so the homeserver will give us push notifications, but
we aren't handling them yet.
2023-11-10 18:08:52 +00:00
Joshua Goins
f7bd24db34 Depend on QCoro::Network
For awaiting on QNetworkReply
2023-11-10 18:08:52 +00:00
Joshua Goins
41c2f9c4d5 Re-arrange main.cpp and add no gui option when DBus activated
We don't want the QML engine initialized at all when launching on
notifications, it'll be useless there.
2023-11-10 18:08:52 +00:00
Joshua Goins
56d5f3036b Change name of notify argument to dbus-activated 2023-11-10 18:08:52 +00:00
Tobias Fella
21bb7dce21 Fix background color of UserInfo
Previously, this would change when the window lost focus, which looks strange
2023-11-10 16:16:29 +01:00
l10n daemon script
11081719a7 GIT_SILENT Sync po/docbooks with svn 2023-11-10 02:22:55 +00:00
Joshua Goins
980de7d85d Use go.kde.org for the Matrix link, mention Matrix wiki page
I moved NeoChat up next to Element on go.kde.org, so it's good marketing
;-)
2023-11-09 16:53:36 -05:00
Joshua Goins
7735313b0c Add DBus service for NeoChat and link to KUnifiedPush if found 2023-11-09 21:40:09 +00:00
Joshua Goins
1a3055df86 Add optional KUnifiedPush dependency 2023-11-09 21:40:09 +00:00
Tobias Fella
84cad630cd Refactor and fix ChatBox layouting
BUG: 474616
2023-11-09 20:19:09 +00:00
James Graham
0beb5df08d Hide the search field for room members in a direct chat.
Hide the search field for room members in a direct chat as there are no members to search.
2023-11-09 20:11:38 +00:00
Tobias Fella
4ef44b8e93 Track online status per connection 2023-11-09 19:38:43 +01:00
l10n daemon script
59153be006 GIT_SILENT Sync po/docbooks with svn 2023-11-09 02:12:54 +00:00
l10n daemon script
50be96f762 GIT_SILENT Sync po/docbooks with svn 2023-11-08 09:27:31 +00:00
158 changed files with 17920 additions and 12868 deletions

10
.craft.ini Normal file
View File

@@ -0,0 +1,10 @@
; SPDX-FileCopyrightText: None
; SPDX-License-Identifier: CC0-1.0
[BlueprintSettings]
kde/unreleased/kirigami-addons.version=master
kde/frameworks.version=master
kde/libs.version=master
kde/plasma.version=master
kde/unreleased.version=master
libs/qt.qtMajorVersion=6

View File

@@ -2,9 +2,12 @@
# SPDX-License-Identifier: CC0-1.0
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-qt6.yml
- 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-qt6.yml
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
- project: sysadmin/ci-utilities
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/windows-qt6.yml
- /gitlab-templates/freebsd-qt6.yml
# - /gitlab-templates/flatpak.yml
- /gitlab-templates/craft-android-qt6-apks.yml

View File

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

View File

@@ -45,3 +45,7 @@ License: BSD-2-Clause
Files: autotests/data/*
Copyright: none
License: CC0-1.0
Files: appiumtests/data/*
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
License: CC0-1.0

View File

@@ -9,7 +9,7 @@ cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "01")
set(RELEASE_SERVICE_VERSION_MICRO "75")
set(RELEASE_SERVICE_VERSION_MICRO "80")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
@@ -58,12 +58,12 @@ set_package_properties(Qt6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
set_package_properties(KF6 PROPERTIES
TYPE REQUIRED
PURPOSE "Basic application components"
)
set_package_properties(KF6Kirigami2 PROPERTIES
set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED
PURPOSE "Kirigami application UI framework"
)
@@ -102,8 +102,7 @@ set_package_properties(QuotientQt6 PROPERTIES
PURPOSE "Talk with matrix server"
)
# The android part is just for CI. We do NOT support any builds without E2EE
if (NOT TARGET Olm::Olm AND NOT ANDROID)
if (NOT TARGET Olm::Olm)
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
endif()
@@ -130,7 +129,7 @@ set_package_properties(KQuickImageEditor PROPERTIES
PURPOSE "Add image editing capability to image attachments"
)
find_package(QCoro6 0.4 COMPONENTS Core REQUIRED)
find_package(QCoro6 0.4 COMPONENTS Core Network REQUIRED)
qcoro_enable_coroutines()
@@ -140,6 +139,13 @@ set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
TYPE OPTIONAL
)
find_package(KUnifiedPush QUIET)
set_package_properties(KUnifiedPush PROPERTIES
TYPE OPTIONAL
PURPOSE "Push notification support"
URL "https://invent.kde.org/libraries/kunifiedpush"
)
if(ANDROID)
find_package(Sqlite3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)

View File

@@ -11,6 +11,7 @@ A Qt/QML based Matrix client.
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
## Introduction
@@ -95,7 +96,7 @@ As is the case throughout the KDE ecosystem contributions are welcome from all.
## Contact
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org).
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://go.kde.org/matrix/#/#neochat:kde.org). See [Matrix](https://community.kde.org/Matrix) for more details.
## Acknowledgement

View File

@@ -92,6 +92,7 @@ android {
exclude 'lib/*/*_imageformats_qtga_*'
exclude 'lib/*/*_imageformats_qtiff_*'
exclude 'lib/*/*_qmltooling_*'
exclude 'lib/*/*_multimedia_ffmpeg*' // temporary qt6 android fix
}
aaptOptions {

View File

@@ -21,3 +21,8 @@ add_test(
NAME logintest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
)
add_test(
NAME openuserdetailstest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/openuserdetailstest.py
)

View File

@@ -0,0 +1,3 @@
{
"next_batch": "batch1234"
}

View File

@@ -0,0 +1,50 @@
{
"next_batch": "batch1234",
"rooms": {
"join": {
"!room_id_1234:localhost:1234": {
"state": {
"events": [
{
"type": "m.room.member",
"state_key": "@user:localhost:1234",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_0:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"avatar_url": "",
"displayname": "A Display Name",
"membership": "join",
"reason": "Nothing"
},
"unsigned": {
"age": 1234
}
}
]
},
"timeline": {
"events": [
{
"type": "m.room.message",
"sender": "@user:localhost:1234",
"origin_server_ts": 1432735824653,
"event_id": "$event_id_1234_1:localhost:1234",
"room_id": "!room_id_1234:localhost:1234",
"content": {
"body": "This is a message",
"format": "org.matrix.custom.html",
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
"msgtype": "m.text"
},
"unsigned": {
"age": 1234
}
}
]
}
}
}
}
}

View File

@@ -1,10 +1,12 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import json
from flask import Flask, request, abort
import os
app = Flask(__name__)
@app.route("/_matrix/client/v3/login", methods=["GET"])
def login_get():
result = dict()
@@ -12,6 +14,13 @@ def login_get():
result["flows"][0]["type"] = "m.login.password"
return result
@app.route("/_matrix/client/v3/account/whoami", methods=["GET"])
def whoami():
result = dict()
result["device_id"] = "device_id_1234"
result["user_id"] = "@user:localhost:1234"
return result
@app.route("/_matrix/client/v3/login", methods=["POST"])
def login_post():
data = request.get_json()
@@ -19,15 +28,22 @@ def login_post():
abort(403)
print(data)
result = dict()
result["access_token"] = "token_1234"
result["access_token"] = "token_login"
result["device_id"] = "device_1234"
result["user_id"] = "@user:localhost:1234"
return result
def load_json(name):
parts = __file__.split("/")
parts.pop()
datadir = "/".join(parts)
return json.loads(open(f"{datadir}/data/{name}.json").read())
@app.route("/_matrix/client/r0/sync")
def sync():
result = dict()
result["next_batch"] = "batch1234"
result = load_json("sync_response_no_rooms") if ("login" in request.headers.get("Authorization")) else load_json("sync_response_rooms")
return result
@app.route("/.well-known/matrix/client")
@@ -37,6 +53,18 @@ def well_known():
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
return reply
@app.route("/_matrix/client/v3/profile/<id>")
def profile(id):
reply = dict()
reply["avatar_url"] = "mxc://localhost:1234/asdf1234"
reply["displayname"] = "User123"
return reply
@app.route("/_matrix/client/v3/keys/upload", methods=["POST"])
def upload_keys():
reply = dict()
return reply
if __name__ == "__main__":
app.run(ssl_context='adhoc', port=1234)

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
import os
import subprocess
import sys
import unittest
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
class OpenUserDetailsTest(unittest.TestCase):
mockServerProcess: subprocess.Popen
@classmethod
def setUpClass(cls):
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
options = AppiumOptions()
options.set_capability("app", "neochat --ignore-ssl-errors --test")
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
def setUp(self):
pass
def tearDown(self):
if not self._outcome.result.wasSuccessful():
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
@classmethod
def tearDownClass(self):
self.mockServerProcess.terminate()
self.driver.quit()
def test_open_sheet(self):
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
self.driver.find_element(by=AppiumBy.NAME, value="Empty room (!room_id_1234:localhost:1234)").click()
self.driver.find_element(by=AppiumBy.NAME, value="A Display Name").click()
self.driver.find_element(by=AppiumBy.NAME, value="Account Details")
if __name__ == '__main__':
unittest.main()

View File

@@ -46,3 +46,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test(
messageeventmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME messageeventmodeltest
)

View File

@@ -135,6 +135,21 @@
"unsigned": {
"age": 1234
}
},
{
"content": {
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
"membership": "join"
},
"event_id": "$143273582443PhrSh:example.org",
"origin_server_ts": 1432735824659,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"state_key": "@newline:example.org",
"type": "m.room.member",
"unsigned": {
"age": 12345
}
}
]
},
@@ -373,6 +388,20 @@
"unsigned": {
"age": 1238
}
},
{
"content": {
"body": "A message from someone who thought it was a good idea to put newlines in their display name.",
"msgtype": "m.text"
},
"event_id": "$153456889:example.org",
"origin_server_ts": 14327358246589,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@newline:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1230
}
}
],
"limited": true,

View File

@@ -55,6 +55,8 @@ private Q_SLOTS:
void nullAuthor();
void authorDisplayName();
void nullAuthorDisplayName();
void singleLineSidplayName();
void nullSingleLineDisplayName();
void time();
void nullTime();
void timeString();
@@ -203,6 +205,23 @@ void EventHandlerTest::nullAuthorDisplayName()
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
}
void EventHandlerTest::singleLineSidplayName()
{
auto event = room->messageEvents().at(11).get();
eventHandler.setEvent(event);
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
}
void EventHandlerTest::nullSingleLineDisplayName()
{
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
}
void EventHandlerTest::time()
{
auto event = room->messageEvents().at(0).get();

View File

@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include "enums/delegatetype.h"
#include "models/messageeventmodel.h"
#include "neochatroom.h"
using namespace Quotient;
class TestRoom : public NeoChatRoom
{
public:
using NeoChatRoom::NeoChatRoom;
void update(SyncRoomData &&data, bool fromCache = false)
{
Room::updateData(std::move(data), fromCache);
}
};
class MessageEventModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
MessageEventModel *model = nullptr;
TestRoom *setupTestRoom(const QString &syncFileName);
private Q_SLOTS:
void initTestCase();
void init();
void switchRoom();
void simpleTimeline();
void idToRow();
void cleanup();
};
void MessageEventModelTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
}
void MessageEventModelTest::init()
{
QCOMPARE(model, nullptr);
model = new MessageEventModel;
}
// Make sure that basic rooms can be switched without crashing.
void MessageEventModelTest::switchRoom()
{
TestRoom *firstRoom = new TestRoom(connection, QStringLiteral("#firstRoom:kde.org"), JoinState::Join);
TestRoom *secondRoom = new TestRoom(connection, QStringLiteral("#secondRoom:kde.org"), JoinState::Join);
QSignalSpy spy(model, SIGNAL(roomChanged()));
QCOMPARE(model->room(), nullptr);
model->setRoom(firstRoom);
QCOMPARE(spy.count(), 1);
QCOMPARE(model->room()->id(), QStringLiteral("#firstRoom:kde.org"));
model->setRoom(secondRoom);
QCOMPARE(spy.count(), 2);
QCOMPARE(model->room()->id(), QStringLiteral("#secondRoom:kde.org"));
model->setRoom(nullptr);
QCOMPARE(spy.count(), 3);
QCOMPARE(model->room(), nullptr);
}
void MessageEventModelTest::simpleTimeline()
{
auto room = setupTestRoom(QLatin1String("test-min-sync.json"));
model->setRoom(room);
QCOMPARE(model->rowCount(), 1);
QCOMPARE(model->data(model->index(0)), QStringLiteral("<b>This is an example<br>text message</b>"));
QCOMPARE(model->data(model->index(0), MessageEventModel::DelegateTypeRole), DelegateType::Message);
QCOMPARE(model->data(model->index(0), MessageEventModel::PlainText), QStringLiteral("This is an example\ntext message"));
QCOMPARE(model->data(model->index(0), MessageEventModel::EventIdRole), QStringLiteral("$153456789:example.org"));
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
QCOMPARE(model->data(model->index(-1)), QVariant());
QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)");
QCOMPARE(model->data(model->index(1)), QVariant());
}
void MessageEventModelTest::idToRow()
{
auto room = setupTestRoom(QLatin1String("test-min-sync.json"));
model->setRoom(room);
QCOMPARE(model->eventIdToRow(QStringLiteral("$153456789:example.org")), 0);
}
void MessageEventModelTest::cleanup()
{
delete model;
model = nullptr;
QCOMPARE(model, nullptr);
}
TestRoom *MessageEventModelTest::setupTestRoom(const QString &syncFileName)
{
auto room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + syncFileName);
testSyncFile.open(QIODevice::ReadOnly);
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testSyncJson.object());
room->update(std::move(roomData));
}
return room;
}
QTEST_GUILESS_MAIN(MessageEventModelTest)
#include "messageeventmodeltest.moc"

View File

@@ -343,6 +343,7 @@ to provide a convergent experience across multiple platforms.</p>
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="23.08.4" date="2023-12-07"/>
<release version="23.08.3" date="2023-11-09"/>
<release version="23.08.2" date="2023-10-12"/>
<release version="23.08.0" date="2023-08-24">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,6 @@ add_library(neochat STATIC
models/customemojimodel.h
clipboard.cpp
clipboard.h
matriximageprovider.cpp
matriximageprovider.h
models/messageeventmodel.cpp
models/messageeventmodel.h
models/messagefiltermodel.cpp
@@ -139,6 +137,10 @@ add_library(neochat STATIC
chatbarcache.h
colorschemer.cpp
colorschemer.h
models/notificationsmodel.cpp
models/notificationsmodel.h
models/timelinemodel.cpp
models/timelinemodel.h
)
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
@@ -173,7 +175,6 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/TypingPane.qml
qml/QuickSwitcher.qml
qml/HoverActions.qml
qml/ChatBox.qml
qml/ChatBar.qml
qml/AttachmentPane.qml
qml/ReplyPane.qml
@@ -290,11 +291,19 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
qml/Security.qml
qml/QrCodeMaximizeComponent.qml
qml/SelectSpacesDialog.qml
qml/AttachDialog.qml
qml/NotificationsView.qml
qml/LoadingDelegate.qml
qml/TimelineEndDelegate.qml
RESOURCES
qml/confetti.png
qml/glowdot.png
)
if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
ecm_qt_declare_logging_category(neochat
HEADER "messageeventmodel_logging.h"
IDENTIFIER "MessageEvent"
@@ -365,7 +374,7 @@ target_link_libraries(neochat PUBLIC
Qt::Network
Qt::QuickControls2
KF6::I18n
KF6::Kirigami2
KF6::Kirigami
KF6::Notifications
KF6::ConfigCore
KF6::ConfigGui
@@ -376,6 +385,7 @@ target_link_libraries(neochat PUBLIC
QuotientQt6
cmark::cmark
QCoro::Core
QCoro::Network
)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -464,7 +474,7 @@ if(ANDROID)
"gps"
"system-users-symbolic"
)
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR/android})
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
@@ -483,9 +493,18 @@ if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID)
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
endif()
endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
endif()

View File

@@ -177,7 +177,7 @@ uint8_t *decode(const char *blurhash, int width, int height, int punch, int nCha
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
return NULL;
return nullptr;
}
return pixelArray;
}

View File

@@ -132,11 +132,7 @@ int ChatDocumentHandler::completionStartIndex() const
return 0;
}
#if !defined(Q_OS_ANDROID)
const long long cursor = cursorPosition();
#else
const auto cursor = cursorPosition();
#endif
const qsizetype cursor = cursorPosition();
const auto &text = getText();
auto start = std::min(cursor, text.size()) - 1;

View File

@@ -158,7 +158,6 @@ private:
QPointer<NeoChatRoom> m_room;
QPointer<ChatBarCache> m_chatBarCache;
bool completionVisible = false;
QColor m_mentionColor;
QColor m_errorColor;
@@ -172,7 +171,5 @@ private:
SyntaxHighlighter *m_highlighter = nullptr;
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
CompletionModel *m_completionModel = nullptr;
};

View File

@@ -42,6 +42,8 @@
#include "trayicon_sni.h"
#endif
bool testMode = false;
using namespace Quotient;
Controller::Controller(QObject *parent)
@@ -56,9 +58,19 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif
QTimer::singleShot(0, this, [this] {
invokeLogin();
});
if (!testMode) {
QTimer::singleShot(0, this, [this] {
invokeLogin();
});
} else {
auto c = new NeoChatConnection(this);
c->assumeIdentity(QStringLiteral("@user:localhost:1234"), QStringLiteral("token_1234"));
connect(c, &Connection::connected, this, [c, this]() {
m_accountRegistry.add(c);
c->syncLoop();
Q_EMIT initiated();
});
}
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
@@ -93,12 +105,30 @@ Controller::Controller(QObject *parent)
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
if (m_accountRegistry.size() > oldAccountCount) {
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
connect(connection, &NeoChatConnection::syncDone, this, [connection]() {
connect(connection, &NeoChatConnection::syncDone, this, [this, connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
connection->setupPushNotifications(m_endpoint);
});
}
oldAccountCount = m_accountRegistry.size();
});
#ifdef HAVE_KUNIFIEDPUSH
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
connect(connector, &KUnifiedPush::Connector::endpointChanged, this, [this](const QString &endpoint) {
m_endpoint = endpoint;
for (auto &quotientConnection : m_accountRegistry) {
auto connection = dynamic_cast<NeoChatConnection *>(quotientConnection);
connection->setupPushNotifications(endpoint);
}
});
connector->registerClient(i18n("Receiving push notifications"));
m_endpoint = connector->endpoint();
#endif
}
Controller &Controller::instance()
@@ -300,9 +330,6 @@ void Controller::setQuitOnLastWindowClosed()
m_trayIcon = nullptr;
}
}
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
#else
return;
#endif
}
@@ -326,20 +353,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
m_connection = connection;
if (connection != nullptr) {
NeoChatConfig::self()->setActiveConnection(connection->userId());
connect(connection, &NeoChatConnection::networkError, this, [this]() {
if (!m_isOnline) {
return;
}
m_isOnline = false;
Q_EMIT isOnlineChanged(false);
});
connect(connection, &NeoChatConnection::syncDone, this, [this] {
if (m_isOnline) {
return;
}
m_isOnline = true;
Q_EMIT isOnlineChanged(true);
});
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
@@ -357,29 +370,25 @@ void Controller::saveWindowGeometry()
WindowController::instance().saveGeometry();
}
bool Controller::isOnline() const
{
return m_isOnline;
}
// TODO: Remove in favor of RoomManager::joinRoom
void Controller::joinRoom(const QString &alias)
{
if (!alias.contains(":"_ls)) {
Q_EMIT errorOccured(i18n("The room id you are trying to join is not valid"));
return;
}
const auto knownServer = alias.mid(alias.indexOf(":"_ls) + 1);
RoomManager::instance().joinRoom(m_connection, alias, QStringList{knownServer});
}
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
{
// HACK: Workaround bug QTBUG 93281
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
}
void Controller::listenForNotifications()
{
#ifdef HAVE_KUNIFIEDPUSH
auto connector = new KUnifiedPush::Connector(QStringLiteral("org.kde.neochat"));
connect(connector, &KUnifiedPush::Connector::messageReceived, [](const QByteArray &data) {
NotificationsManager::instance().postPushNotification(data);
});
connector->registerClient(i18n("Receiving push notifications"));
#endif
}
void Controller::setApplicationProxy()
{
NeoChatConfig *cfg = NeoChatConfig::self();
@@ -425,3 +434,8 @@ AccountRegistry &Controller::accounts()
}
#include "moc_controller.cpp"
void Controller::setTestMode(bool test)
{
testMode = test;
}

View File

@@ -12,6 +12,10 @@
#include <Quotient/jobs/basejob.h>
#include <Quotient/settings.h>
#ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h>
#endif
class NeoChatRoom;
class TrayIcon;
class QQuickTextDocument;
@@ -51,11 +55,6 @@ class Controller : public QObject
*/
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
/**
* @brief Whether NeoChat is running as a flatpak.
*
@@ -101,15 +100,8 @@ public:
*/
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
/**
* @brief Join a room.
*/
Q_INVOKABLE void joinRoom(const QString &alias);
[[nodiscard]] bool supportSystemTray() const;
bool isOnline() const;
/**
* @brief Sets the QNetworkProxy for the application.
*
@@ -126,8 +118,16 @@ public:
*/
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
/**
* @brief Start listening for notifications in dbus-activated mode.
* These notifications will quit the application when closed.
*/
static void listenForNotifications();
Quotient::AccountRegistry &accounts();
static void setTestMode(bool testMode);
private:
explicit Controller(QObject *parent = nullptr);
@@ -138,11 +138,11 @@ private:
void loadSettings();
void saveSettings() const;
bool m_isOnline = true;
QMap<Quotient::Room *, int> m_notificationCounts;
Quotient::AccountRegistry m_accountRegistry;
QStringList m_accountsLoading;
QString m_endpoint;
private Q_SLOTS:
void invokeLogin();
@@ -164,7 +164,6 @@ Q_SIGNALS:
void activeConnectionChanged();
void passwordStatus(Controller::PasswordStatus status);
void userConsentRequired(QUrl url);
void isOnlineChanged(bool isOnline);
void accountsLoadingChanged();
public Q_SLOTS:

View File

@@ -40,6 +40,8 @@ public:
Poll, /**< The initial event for a poll. */
Location, /**< A location event. */
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
Loading, /**< A delegate to tell the user more messages are being loaded. */
TimelineEnd, /**< A delegate to inform that all messages are loaded. */
Other, /**< Anything that cannot be classified as another type. */
};
Q_ENUM(Type);

View File

@@ -169,6 +169,28 @@ QString EventHandler::getAuthorDisplayName(bool isPending) const
}
}
QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
{
if (m_room == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
return {};
}
if (m_event == nullptr) {
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
return {};
}
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
auto displayName = m_room->htmlSafeMemberName(author->id());
displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
displayName.replace(QStringLiteral("<br />"), QStringLiteral(" "));
displayName.replace(u'\n', QStringLiteral(" "));
displayName.replace(u'\u2028', QStringLiteral(" "));
return displayName;
}
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
{
if (m_event == nullptr) {
@@ -663,12 +685,12 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
QVariantMap mediaInfo;
// Get the mxc URL for the media.
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
mediaInfo["source"_ls] = QUrl();
} else {
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
if (source.isValid()) {
mediaInfo["source"_ls] = source;
} else {
mediaInfo["source"_ls] = QUrl();

View File

@@ -111,6 +111,17 @@ public:
*/
QString getAuthorDisplayName(bool isPending = false) const;
/**
* @brief Get the display name of the event author but with any newlines removed.
*
* Turns out you can put newlines in your display name so we need to handle that
* primarily for the room list subtitle.
*
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
*/
QString singleLineAuthorDisplayname(bool isPending = false) const;
/**
* @brief Return a QDateTime object for the event timestamp.
*/

View File

@@ -5,6 +5,7 @@
#include <QIcon>
#include <QNetworkDiskCache>
#include <QNetworkProxyFactory>
#include <QNetworkReply>
#include <QObject>
#include <QQmlApplicationEngine>
#include <QQmlContext>
@@ -40,7 +41,6 @@
#include "colorschemer.h"
#include "controller.h"
#include "logger.h"
#include "matriximageprovider.h"
#include "neochatconfig.h"
#include "roommanager.h"
#include "windowcontroller.h"
@@ -171,6 +171,40 @@ int main(int argc, char *argv[])
colorScheme.apply(NeoChatConfig::self()->colorScheme());
}
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
QCommandLineOption testOption("test"_ls, i18n("Only used for autotests"));
testOption.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(testOption);
#ifdef HAVE_KUNIFIEDPUSH
QCommandLineOption dbusActivatedOption(QStringLiteral("dbus-activated"), i18n("Internal usage only."));
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
parser.addOption(dbusActivatedOption);
#endif
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
Controller::setTestMode(parser.isSet("test"_ls));
#ifdef HAVE_KUNIFIEDPUSH
if (parser.isSet(dbusActivatedOption)) {
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
Controller::listenForNotifications();
return QCoreApplication::exec();
}
#endif
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
#endif
qml_register_types_org_kde_neochat();
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
@@ -180,7 +214,6 @@ int main(int argc, char *argv[])
QQmlApplicationEngine engine;
#ifdef HAVE_KDBUSADDONS
KDBusService service(KDBusService::Unique);
service.connect(&service,
&KDBusService::activateRequested,
&RoomManager::instance(),
@@ -199,7 +232,7 @@ int main(int argc, char *argv[])
auto args = arguments;
args.removeFirst();
for (const auto &arg : args) {
RoomManager::instance().openResource(arg);
RoomManager::instance().resolveResource(arg);
}
});
#endif
@@ -208,22 +241,12 @@ int main(int argc, char *argv[])
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
QCommandLineParser parser;
parser.setApplicationDescription(i18n("Client for the matrix communication protocol"));
parser.addPositionalArgument(QStringLiteral("urls"), i18n("Supports matrix: url scheme"));
parser.addOption(QCommandLineOption("ignore-ssl-errors"_ls, i18n("Ignore all SSL Errors, e.g., unsigned certificates.")));
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
if (parser.isSet("ignore-ssl-errors"_ls)) {
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
reply->ignoreSslErrors();
});
}
engine.addImageProvider(QLatin1String("mxc"), MatrixImageProvider::create(&engine, &engine));
engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider);
engine.load(QUrl(QStringLiteral("qrc:/org/kde/neochat/qml/main.qml")));

View File

@@ -1,127 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#include "matriximageprovider.h"
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QThread>
#include <KLocalizedString>
#include "controller.h"
#include "neochatconnection.h"
#include <Quotient/connection.h>
using namespace Quotient;
ThumbnailResponse::ThumbnailResponse(QString id, QSize size, NeoChatConnection *connection)
: mediaId(std::move(id))
, requestedSize(size)
, localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
mediaId,
QString::number(requestedSize.width()),
QString::number(requestedSize.height())))
, m_connection(connection)
, errorStr("Image request hasn't started"_ls)
{
if (requestedSize.isEmpty()) {
requestedSize.setHeight(100);
requestedSize.setWidth(100);
}
if (mediaId.count(QLatin1Char('/')) != 1) {
if (mediaId.startsWith(QLatin1Char('/'))) {
mediaId = mediaId.mid(1);
} else {
errorStr = i18n("Media id '%1' doesn't follow server/mediaId pattern", mediaId);
Q_EMIT finished();
return;
}
}
mediaId = mediaId.split(QLatin1Char('?'))[0];
QImage cachedImage;
if (cachedImage.load(localFile)) {
image = cachedImage;
errorStr.clear();
Q_EMIT finished();
return;
}
if (!m_connection) {
qWarning() << "Current connection is null";
return;
}
// Execute a request on the main thread asynchronously
moveToThread(m_connection->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
}
void ThumbnailResponse::startRequest()
{
if (!m_connection) {
return;
}
// Runs in the main thread, not QML thread
Q_ASSERT(QThread::currentThread() == m_connection->thread());
job = m_connection->getThumbnail(mediaId, requestedSize);
// Connect to any possible outcome including abandonment
// to make sure the QML thread is not left stuck forever.
connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult);
}
void ThumbnailResponse::prepareResult()
{
Q_ASSERT(QThread::currentThread() == job->thread());
Q_ASSERT(job->error() != BaseJob::Pending);
{
QWriteLocker _(&lock);
if (job->error() == BaseJob::Success) {
image = job->thumbnail();
QString localPath = QFileInfo(localFile).absolutePath();
QDir dir;
if (!dir.exists(localPath)) {
dir.mkpath(localPath);
}
image.save(localFile);
errorStr.clear();
} else if (job->error() == BaseJob::Abandoned) {
errorStr = i18n("Image request has been cancelled");
// qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
} else {
errorStr = job->errorString();
qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr;
}
job = nullptr;
}
Q_EMIT finished();
}
QQuickTextureFactory *ThumbnailResponse::textureFactory() const
{
QReadLocker _(&lock);
return QQuickTextureFactory::textureFactoryForImage(image);
}
QString ThumbnailResponse::errorString() const
{
QReadLocker _(&lock);
return errorStr;
}
QQuickImageResponse *MatrixImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
return new ThumbnailResponse(id, requestedSize, m_connection);
}
#include "moc_matriximageprovider.cpp"

View File

@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QQuickAsyncImageProvider>
#include <Quotient/jobs/mediathumbnailjob.h>
#include <QReadWriteLock>
class NeoChatConnection;
/**
* @class ThumbnailResponse
*
* A QQuickImageResponse for an mxc image.
*
* @sa QQuickImageResponse
*/
class ThumbnailResponse : public QQuickImageResponse
{
Q_OBJECT
public:
explicit ThumbnailResponse(QString mediaId, QSize requestedSize, NeoChatConnection *m_connection);
~ThumbnailResponse() override = default;
private Q_SLOTS:
void startRequest();
void prepareResult();
private:
QString mediaId;
QSize requestedSize;
const QString localFile;
Quotient::MediaThumbnailJob *job = nullptr;
NeoChatConnection *m_connection;
QImage image;
QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory *textureFactory() const override;
QString errorString() const override;
};
/**
* @class MatrixImageProvider
*
* A QQuickAsyncImageProvider for mxc images.
*
* @sa QQuickAsyncImageProvider
*/
class MatrixImageProvider : public QQuickAsyncImageProvider
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(NeoChatConnection *connection MEMBER m_connection)
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider instance;
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
return &instance;
}
/**
* @brief Return a job to provide the image with the given ID.
*
* @sa QQuickAsyncImageProvider::requestImageResponse
*/
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
private:
NeoChatConnection *m_connection = nullptr;
MatrixImageProvider() = default;
};

View File

@@ -235,7 +235,7 @@ QList<ActionsModel::Action> actions{
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Controller::instance().joinRoom(text);
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
false,
@@ -290,7 +290,7 @@ QList<ActionsModel::Action> actions{
return QString();
}
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
Controller::instance().joinRoom(text);
RoomManager::instance().resolveResource(text, "join"_ls);
return QString();
},
false,

View File

@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
fetchEmojis();
});
});
CustomEmojiModel::fetchEmojis();
}
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const

View File

@@ -102,6 +102,9 @@ void ImagePacksModel::reloadImages()
}
auto packs = rooms[roomId].toObject();
const auto &stickerRoom = m_room->connection()->room(roomId);
if (!stickerRoom) {
continue;
}
for (const auto &packKey : packs.keys()) {
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
const auto packContent = pack->content();

View File

@@ -45,10 +45,6 @@ LocationsModel::LocationsModel(QObject *parent)
});
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
});
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
@@ -135,4 +131,12 @@ QRectF LocationsModel::boundingBox() const
return bbox;
}
bool LocationsModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
return QObject::event(event);
}
#include "moc_locationsmodel.cpp"

View File

@@ -46,6 +46,9 @@ Q_SIGNALS:
void roomChanged();
void boundingBoxChanged();
protected:
bool event(QEvent *event) override;
private:
QPointer<NeoChatRoom> m_room;

View File

@@ -71,9 +71,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
MessageEventModel::MessageEventModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
});
}
NeoChatRoom *MessageEventModel::room() const
@@ -95,6 +92,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
}
m_currentRoom = room;
Q_EMIT roomChanged();
if (room) {
m_lastReadEventIndex = QPersistentModelIndex(QModelIndex());
room->setDisplayed();
@@ -392,13 +390,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
return 0;
}
const auto firstIt = m_currentRoom->messageEvents().crbegin();
if (firstIt != m_currentRoom->messageEvents().crend()) {
const auto &firstEvt = **firstIt;
return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0);
} else {
return m_currentRoom->timelineSize();
}
return int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0);
}
bool MessageEventModel::canFetchMore(const QModelIndex &parent) const
@@ -425,7 +417,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
const auto row = idx.row();
if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) {
if (!m_currentRoom || row < 0 || row >= rowCount()) {
return {};
};
@@ -440,6 +432,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
const KFormat format;
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
}
case SpecialMarksRole:
// Check if all the earlier events in the timeline are hidden. If so hide this.
for (auto r = row - 1; r >= 0; --r) {
const auto specialMark = index(r).data(SpecialMarksRole);
if (!(specialMark == EventStatus::Hidden || specialMark == EventStatus::Replaced)) {
return EventStatus::Normal;
}
}
return EventStatus::Hidden;
}
return {};
}
@@ -459,7 +460,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
}
return eventHandler.getRichBody();
}
@@ -726,4 +726,12 @@ void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *eve
}
}
bool MessageEventModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyAuthor, ReadMarkersRole});
}
return QObject::event(event);
}
#include "moc_messageeventmodel.cpp"

View File

@@ -118,6 +118,9 @@ public:
*/
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
int refreshEvent(const QString &eventId);
void refreshRow(int row);

View File

@@ -8,14 +8,15 @@
#include "enums/delegatetype.h"
#include "messageeventmodel.h"
#include "neochatconfig.h"
#include "timelinemodel.h"
using namespace Quotient;
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
: QSortFilterProxyModel(parent)
{
Q_ASSERT(sourceMessageModel);
setSourceModel(sourceMessageModel);
Q_ASSERT(sourceModel);
setSourceModel(sourceModel);
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
invalidateFilter();
@@ -117,35 +118,37 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
chunks += QString();
int count = 1;
auto part = parts.takeFirst();
chunks.last() += part;
while (!parts.isEmpty() && parts.first() == part) {
parts.removeFirst();
count++;
}
if (count > 1 && uniqueAuthors.length() == 1) {
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
}
chunks.last() += i18ncp("%1: What's being done; %2: How often it is done.", " %1", " %1 %2 times", part, count);
}
chunks.removeDuplicates();
QString text = QStringLiteral("<style>a {text-decoration: none;}</style>"); // There can be links in the event text so make sure all are styled.
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
text += userText;
text += chunks.takeFirst();
QString chunksText;
chunksText += chunks.takeFirst();
if (chunks.size() > 0) {
while (chunks.size() > 1) {
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
text += chunks.takeFirst();
chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", ");
chunksText += chunks.takeFirst();
}
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
text += chunks.takeFirst();
chunksText +=
uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
chunksText += chunks.takeFirst();
}
return text;
return i18nc(
"userText (%1) is either a Matrix username if a single user sent all the states or n users if they were sent by multiple users."
"chunksText (%2) is a list of comma separated actions for each of the state events in the group.",
"<style>a {text-decoration: none;}</style>%1 %2",
userText,
chunksText);
} else {
return {};
}

View File

@@ -7,6 +7,7 @@
#include <QSortFilterProxyModel>
#include "messageeventmodel.h"
#include "timelinemodel.h"
/**
* @class MessageFilterModel
@@ -36,7 +37,7 @@ public:
LastRole, // Keep this last
};
explicit MessageFilterModel(QObject *parent = nullptr, MessageEventModel *sourceMessageModel = nullptr);
explicit MessageFilterModel(QObject *parent = nullptr, TimelineModel *sourceModel = nullptr);
/**
* @brief Custom filter function to remove hidden messages.

View File

@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "notificationsmodel.h"
#include <Quotient/connection.h>
#include <Quotient/events/event.h>
#include <Quotient/uri.h>
#include "eventhandler.h"
#include "neochatroom.h"
using namespace Quotient;
NotificationsModel::NotificationsModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int NotificationsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_notifications.count();
}
QVariant NotificationsModel::data(const QModelIndex &index, int role) const
{
auto row = index.row();
if (row < 0 || row >= m_notifications.count()) {
return {};
}
if (role == TextRole) {
return m_notifications[row].text;
}
if (role == RoomIdRole) {
return m_notifications[row].roomId;
}
if (role == AuthorName) {
return m_notifications[row].authorName;
}
if (role == AuthorAvatar) {
return m_notifications[row].authorAvatar;
}
if (role == RoomRole) {
return QVariant::fromValue(m_connection->room(m_notifications[row].roomId));
}
if (role == EventIdRole) {
return m_notifications[row].eventId;
}
if (role == RoomDisplayNameRole) {
return m_notifications[row].roomDisplayName;
}
if (role == UriRole) {
return Uri(m_notifications[row].roomId.toLatin1(), m_notifications[row].eventId.toLatin1()).toUrl();
}
return {};
}
QHash<int, QByteArray> NotificationsModel::roleNames() const
{
return {
{TextRole, "text"},
{RoomIdRole, "roomId"},
{AuthorName, "authorName"},
{AuthorAvatar, "authorAvatar"},
{RoomRole, "room"},
{EventIdRole, "eventId"},
{RoomDisplayNameRole, "roomDisplayName"},
{UriRole, "uri"},
};
}
NeoChatConnection *NotificationsModel::connection() const
{
return m_connection;
}
void NotificationsModel::setConnection(NeoChatConnection *connection)
{
if (m_connection) {
// disconnect things...
}
if (!connection) {
return;
}
m_connection = connection;
Q_EMIT connectionChanged();
connect(connection, &Connection::syncDone, this, [=]() {
loadData();
});
loadData();
}
void NotificationsModel::loadData()
{
Q_ASSERT(m_connection);
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
return;
}
m_job = m_connection->callApi<GetNotificationsJob>(m_nextToken);
Q_EMIT loadingChanged();
connect(m_job, &BaseJob::finished, this, [this]() {
m_nextToken = m_job->nextToken();
Q_EMIT nextTokenChanged();
for (const auto &notification : m_job->notifications()) {
if (std::any_of(notification.actions.constBegin(), notification.actions.constEnd(), [](const QVariant &it) {
if (it.canConvert<QVariantMap>()) {
auto map = it.toMap();
if (map["set_tweak"_ls] == "highlight"_ls) {
return true;
}
}
return false;
})) {
const auto &authorId = notification.event->fullJson()["sender"_ls].toString();
const auto &room = m_connection->room(notification.roomId);
if (!room) {
continue;
}
auto u = room->memberAvatarUrl(authorId);
auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u);
const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl();
const auto &roomEvent = eventCast<const RoomEvent>(notification.event.get());
EventHandler eventHandler;
eventHandler.setRoom(dynamic_cast<NeoChatRoom *>(room));
eventHandler.setEvent(roomEvent);
beginInsertRows({}, m_notifications.length(), m_notifications.length());
m_notifications += Notification{
.roomId = notification.roomId,
.text = room->htmlSafeMemberName(authorId) + (roomEvent->is<StateEvent>() ? QStringLiteral(" ") : QStringLiteral(": "))
+ eventHandler.getPlainBody(true),
.authorName = room->htmlSafeMemberName(authorId),
.authorAvatar = authorAvatar,
.eventId = roomEvent->id(),
.roomDisplayName = room->displayName(),
};
endInsertRows();
}
}
m_job = nullptr;
Q_EMIT loadingChanged();
});
}
bool NotificationsModel::canFetchMore(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return !m_nextToken.isEmpty();
}
void NotificationsModel::fetchMore(const QModelIndex &parent)
{
Q_UNUSED(parent);
loadData();
}
bool NotificationsModel::loading() const
{
return m_job;
}
QString NotificationsModel::nextToken() const
{
return m_nextToken;
}

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include "neochatconnection.h"
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QVariant>
#include <Quotient/csapi/notifications.h>
class NotificationsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(QString nextToken READ nextToken NOTIFY nextTokenChanged)
public:
enum Roles {
TextRole = Qt::DisplayRole,
RoomIdRole,
AuthorName,
AuthorAvatar,
RoomRole,
EventIdRole,
RoomDisplayNameRole,
UriRole,
};
Q_ENUM(Roles);
struct Notification {
QString roomId;
QString text;
QString authorName;
QUrl authorAvatar;
QString eventId;
QString roomDisplayName;
};
NotificationsModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
NeoChatConnection *connection() const;
void setConnection(NeoChatConnection *connection);
bool loading() const;
QString nextToken() const;
Q_SIGNALS:
void connectionChanged();
void loadingChanged();
void nextTokenChanged();
private:
QPointer<NeoChatConnection> m_connection;
void loadData();
QList<Notification> m_notifications;
QString m_nextToken;
QPointer<Quotient::GetNotificationsJob> m_job;
};

View File

@@ -21,9 +21,6 @@ using namespace Quotient;
SearchModel::SearchModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
});
}
QString SearchModel::searchText() const
@@ -219,6 +216,14 @@ bool SearchModel::searching() const
return m_searching;
}
bool SearchModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole});
}
return QObject::event(event);
}
void SearchModel::setSearching(bool searching)
{
m_searching = searching;

View File

@@ -132,6 +132,9 @@ Q_SIGNALS:
void roomChanged();
void searchingChanged();
protected:
bool event(QEvent *event) override;
private:
void setSearching(bool searching);

View File

@@ -16,41 +16,6 @@
ServerListModel::ServerListModel(QObject *parent)
: QAbstractListModel(parent)
{
const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
QString domain = m_connection->domain();
// Add the user's homeserver
m_servers.append(Server{
domain,
true,
false,
false,
});
// Add matrix.org
m_servers.append(Server{
QStringLiteral("matrix.org"),
false,
false,
false,
});
// Add each of the saved custom servers
for (const auto &i : serverGroup.keyList()) {
m_servers.append(Server{
serverGroup.readEntry(i, QString()),
false,
false,
true,
});
}
// Add add server delegate entry
m_servers.append(Server{
QString(),
false,
true,
false,
});
}
QVariant ServerListModel::data(const QModelIndex &index, int role) const
@@ -165,6 +130,53 @@ void ServerListModel::setConnection(NeoChatConnection *connection)
}
m_connection = connection;
Q_EMIT connectionChanged();
initialize();
}
void ServerListModel::initialize()
{
if (m_connection == nullptr) {
return;
}
beginResetModel();
const auto stateConfig = KSharedConfig::openStateConfig();
const KConfigGroup serverGroup = stateConfig->group(QStringLiteral("Servers"));
QString domain = m_connection->domain();
// Add the user's homeserver
m_servers.append(Server{
domain,
true,
false,
false,
});
// Add matrix.org
m_servers.append(Server{
QStringLiteral("matrix.org"),
false,
false,
false,
});
// Add each of the saved custom servers
for (const auto &i : serverGroup.keyList()) {
m_servers.append(Server{
serverGroup.readEntry(i, QString()),
false,
false,
true,
});
}
// Add add server delegate entry
m_servers.append(Server{
QString(),
false,
true,
false,
});
beginResetModel();
}
#include "moc_serverlistmodel.cpp"

View File

@@ -111,4 +111,6 @@ private:
QList<Server> m_servers;
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
NeoChatConnection *m_connection = nullptr;
void initialize();
};

View File

@@ -86,6 +86,7 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
SpaceTreeItem *parentItem = getItem(parent);
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
parentItem->setChildStates(std::move(children[0].childrenState));
children.erase(children.begin());
}
@@ -112,6 +113,13 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
m_replacedRooms += successorId;
}
}
if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
insertChildren(job->rooms(), index(insertRow, 0, parent));
});
}
parentItem->insertChild(insertRow,
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
parentItem,
@@ -123,14 +131,8 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
children[i].avatarUrl,
children[i].guestCanJoin,
children[i].worldReadable,
children[i].roomType == QLatin1String("m.space")));
if (children[i].childrenState.size() > 0) {
auto job = m_space->connection()->callApi<Quotient::GetSpaceHierarchyJob>(children[i].roomId, Quotient::none, Quotient::none, 1);
m_currentJobs.append(job);
connect(job, &Quotient::BaseJob::success, this, [this, parent, insertRow, job]() {
insertChildren(job->rooms(), index(insertRow, 0, parent));
});
}
children[i].roomType == QLatin1String("m.space"),
std::move(children[i].childrenState)));
}
}
endInsertRows();
@@ -194,6 +196,9 @@ QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
if (role == IsSpaceRole) {
return child->isSpace();
}
if (role == IsSuggestedRole) {
return child->isSuggested();
}
if (role == CanAddChildrenRole) {
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
return room->canSendState(QLatin1String("m.space.child"));
@@ -313,6 +318,7 @@ QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
roles[IsJoinedRole] = "isJoined";
roles[AliasRole] = "alias";
roles[IsSpaceRole] = "isSpace";
roles[IsSuggestedRole] = "isSuggested";
roles[CanAddChildrenRole] = "canAddChildren";
roles[ParentDisplayNameRole] = "parentDisplayName";
roles[CanSetParentRole] = "canSetParent";

View File

@@ -44,6 +44,7 @@ public:
WorldReadableRole,
IsJoinedRole,
IsSpaceRole,
IsSuggestedRole,
CanAddChildrenRole,
ParentDisplayNameRole,
CanSetParentRole,

View File

@@ -15,7 +15,8 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
const QUrl &avatarUrl,
bool allowGuests,
bool worldReadable,
bool isSpace)
bool isSpace,
Quotient::StateEvents childStates)
: m_connection(connection)
, m_parentItem(parent)
, m_id(id)
@@ -27,6 +28,7 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
, m_allowGuests(allowGuests)
, m_worldReadable(worldReadable)
, m_isSpace(isSpace)
, m_childStates(std::move(childStates))
{
}
@@ -74,7 +76,7 @@ int SpaceTreeItem::row() const
return 0;
}
SpaceTreeItem *SpaceTreeItem::parentItem()
SpaceTreeItem *SpaceTreeItem::parentItem() const
{
return m_parentItem;
}
@@ -138,3 +140,34 @@ bool SpaceTreeItem::isSpace() const
{
return m_isSpace;
}
QJsonObject SpaceTreeItem::childStateContent(const SpaceTreeItem *child) const
{
if (child == nullptr) {
return {};
}
if (child->parentItem() != this) {
return {};
}
for (const auto &childState : m_childStates) {
if (childState->stateKey() == child->id()) {
return childState->contentJson();
}
}
return {};
}
void SpaceTreeItem::setChildStates(Quotient::StateEvents childStates)
{
m_childStates.clear();
m_childStates = std::move(childStates);
}
bool SpaceTreeItem::isSuggested() const
{
if (m_parentItem == nullptr) {
return false;
}
const auto childStateContent = m_parentItem->childStateContent(this);
return childStateContent.value(QLatin1String("suggested")).toBool();
}

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <Quotient/csapi/space_hierarchy.h>
#include <Quotient/events/stateevent.h>
class NeoChatConnection;
@@ -30,7 +31,8 @@ public:
const QUrl &avatarUrl = {},
bool allowGuests = {},
bool worldReadable = {},
bool isSpace = {});
bool isSpace = {},
Quotient::StateEvents childStates = {});
~SpaceTreeItem();
/**
@@ -60,7 +62,7 @@ public:
/**
* @brief Return this item's parent.
*/
SpaceTreeItem *parentItem();
SpaceTreeItem *parentItem() const;
/**
* @brief Return the row number for this child relative to the parent.
@@ -123,6 +125,23 @@ public:
*/
bool isSpace() const;
/**
* @brief Return the m.space.child state event content for the given child.
*/
QJsonObject childStateContent(const SpaceTreeItem *child) const;
/**
* @brief Set the list of m.space.child events.
*
* Overwrites existing states. Calling with no input will clear the existing states.
*/
void setChildStates(Quotient::StateEvents childStates = {});
/**
* @brief Whether the room is suggested in the parent space.
*/
bool isSuggested() const;
private:
NeoChatConnection *m_connection;
QList<SpaceTreeItem *> m_children;
@@ -137,4 +156,5 @@ private:
bool m_allowGuests;
bool m_worldReadable;
bool m_isSpace;
Quotient::StateEvents m_childStates;
};

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "timelinemodel.h"
#include "delegatetype.h"
TimelineModel::TimelineModel(QObject *parent)
: QConcatenateTablesProxyModel(parent)
{
m_messageEventModel = new MessageEventModel(this);
addSourceModel(m_messageEventModel);
m_timelineEndModel = new TimelineEndModel(this);
addSourceModel(m_timelineEndModel);
}
NeoChatRoom *TimelineModel::room() const
{
return m_messageEventModel->room();
}
void TimelineModel::setRoom(NeoChatRoom *room)
{
// Both models do their own null checking so just pass along.
m_messageEventModel->setRoom(room);
m_timelineEndModel->setRoom(room);
}
MessageEventModel *TimelineModel::messageEventModel() const
{
return m_messageEventModel;
}
QHash<int, QByteArray> TimelineModel::roleNames() const
{
return m_messageEventModel->roleNames();
}
TimelineEndModel::TimelineEndModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void TimelineEndModel::setRoom(NeoChatRoom *room)
{
if (room == m_room) {
return;
}
beginResetModel();
if (m_room != nullptr) {
m_room->disconnect(this);
}
m_room = room;
if (m_room != nullptr) {
connect(m_room, &Quotient::Room::eventsHistoryJobChanged, this, [this]() {
if (m_room->allHistoryLoaded()) {
// HACK: We have to do it this way because DelegateChooser doesn't update dynamically.
beginRemoveRows({}, 0, 0);
endRemoveRows();
beginInsertRows({}, 0, 0);
endInsertRows();
}
});
}
endResetModel();
}
QVariant TimelineEndModel::data(const QModelIndex &idx, int role) const
{
Q_UNUSED(idx)
if (m_room == nullptr) {
return {};
}
if (role == DelegateTypeRole) {
return m_room->allHistoryLoaded() ? DelegateType::TimelineEnd : DelegateType::Loading;
}
return {};
}
int TimelineEndModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QHash<int, QByteArray> TimelineEndModel::roleNames() const
{
return {{DelegateTypeRole, "delegateType"}};
}

112
src/models/timelinemodel.h Normal file
View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QAbstractListModel>
#include <QConcatenateTablesProxyModel>
#include <QQmlEngine>
#include "messageeventmodel.h"
#include "neochatroom.h"
/**
* @class TimelineEndModel
*
* A model to provide a single delegate to mark the end of the timeline.
*
* The delegate will either be a loading delegate if more events are being loaded
* or a timeline end delegate if all history is loaded.
*/
class TimelineEndModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
/**
* @brief Defines the model roles.
*/
enum Roles {
DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */
};
Q_ENUM(Roles)
explicit TimelineEndModel(QObject *parent = nullptr);
/**
* @brief Set the room for the timeline.
*/
void setRoom(NeoChatRoom *room);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
/**
* @brief 1, the answer is always 1.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/**
* @brief Returns a map with DelegateTypeRole it's the only one.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
private:
NeoChatRoom *m_room = nullptr;
};
/**
* @class TimelineModel
*
* A model to visualise a room timeline.
*
* This model combines a MessageEventModel with a TimelineEndModel.
*
* @sa MessageEventModel, TimelineEndModel
*/
class TimelineModel : public QConcatenateTablesProxyModel
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The current room that the model is getting its messages from.
*/
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/**
* @brief The MessageEventModel for the timeline.
*/
Q_PROPERTY(MessageEventModel *messageEventModel READ messageEventModel CONSTANT)
public:
TimelineModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
MessageEventModel *messageEventModel() const;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractProxyModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void roomChanged();
private:
MessageEventModel *m_messageEventModel = nullptr;
TimelineEndModel *m_timelineEndModel = nullptr;
};

View File

@@ -141,12 +141,10 @@ QVariant UserDirectoryListModel::data(const QModelIndex &index, int role) const
return QStringLiteral("Unknown User");
}
if (role == AvatarRole) {
auto avatarUrl = user.avatarUrl;
if (avatarUrl.isEmpty()) {
return QString();
if (user.avatarUrl.isEmpty() || user.avatarUrl.scheme() != QStringLiteral("mxc")) {
return QUrl();
}
return avatarUrl.url().remove(0, 6);
return m_connection->makeMediaUrl(user.avatarUrl);
}
if (role == UserIDRole) {
return user.userId;

View File

@@ -16,9 +16,6 @@ UserListModel::UserListModel(QObject *parent)
: QAbstractListModel(parent)
, m_currentRoom(nullptr)
{
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this]() {
refreshAllUsers();
});
}
void UserListModel::setRoom(NeoChatRoom *room)
@@ -121,6 +118,14 @@ int UserListModel::rowCount(const QModelIndex &parent) const
return m_users.count();
}
bool UserListModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
refreshAllUsers();
}
return QObject::event(event);
}
void UserListModel::userAdded(Quotient::User *user)
{
auto pos = findUserPos(user);

View File

@@ -86,6 +86,9 @@ Q_SIGNALS:
void roomChanged();
void usersRefreshed();
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
void userAdded(Quotient::User *user);
void userRemoved(Quotient::User *user);

View File

@@ -82,22 +82,22 @@
</entry>
<entry name="ShowRename" type="bool">
<label>Show rename events in the timeline</label>
<default>true</default>
<default>false</default>
</entry>
<entry name="ShowAvatarUpdate" type="bool">
<label>Show avatar update events in the timeline</label>
<default>true</default>
<default>false</default>
</entry>
<entry name="ShowDeletedMessages" type="bool">
<label>Show deleted messages in the timeline</label>
<default>true</default>
<default>false</default>
</entry>
<entry name="ShowLinkPreview" type="bool">
<label>Show preview of the links in the chat messages</label>
</entry>
<entry name="SystemTray" type="bool">
<label>Close NeoChat to system tray</label>
<default>true</default>
<default>false</default>
</entry>
<entry name="MinimizeToSystemTrayOnStartup" type="bool">
<label>Minimize to system tray on startup</label>

View File

@@ -21,7 +21,14 @@
#include <Quotient/settings.h>
#include <Quotient/user.h>
#ifdef HAVE_KUNIFIEDPUSH
#include <QCoroNetwork>
#include <Quotient/csapi/pusher.h>
#include <Quotient/networkaccessmanager.h>
#endif
using namespace Quotient;
using namespace Qt::StringLiterals;
NeoChatConnection::NeoChatConnection(QObject *parent)
: Connection(parent)
@@ -31,6 +38,12 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
Q_EMIT labelChanged();
}
});
connect(this, &NeoChatConnection::syncDone, this, [this] {
setIsOnline(true);
});
connect(this, &NeoChatConnection::networkError, this, [this]() {
setIsOnline(false);
});
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
@@ -181,7 +194,7 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic, co
}
});
}
connect(job, &CreateRoomJob::failure, this, [this, job] {
connect(job, &CreateRoomJob::failure, this, [job] {
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
@@ -213,7 +226,7 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
}
});
}
connect(job, &CreateRoomJob::failure, this, [this, job] {
connect(job, &CreateRoomJob::failure, this, [job] {
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
});
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
@@ -235,6 +248,39 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
requestDirectChat(user);
}
QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
{
#ifdef HAVE_KUNIFIEDPUSH
QUrl gatewayEndpoint(endpoint);
gatewayEndpoint.setPath(QStringLiteral("/_matrix/push/v1/notify"));
QNetworkRequest checkGateway(gatewayEndpoint);
auto reply = co_await NetworkAccessManager::instance()->get(checkGateway);
// We want to check if this UnifiedPush server has a Matrix gateway
// This is because Matrix does not natively support UnifiedPush
const auto &replyJson = QJsonDocument::fromJson(reply->readAll()).object();
if (replyJson["unifiedpush"_L1]["gateway"_L1].toString() == QStringLiteral("matrix")) {
callApi<PostPusherJob>(endpoint,
QStringLiteral("http"),
QStringLiteral("org.kde.neochat"),
QStringLiteral("NeoChat"),
deviceId(),
QString(), // profileTag is intentionally left empty for now, it's optional
QStringLiteral("en-US"),
PostPusherJob::PusherData{QUrl::fromUserInput(gatewayEndpoint.toString()), QStringLiteral(" ")},
false);
qInfo() << "Registered for push notifications";
} else {
qWarning() << "There's no gateway, not setting up push notifications.";
}
#else
co_return;
#endif
}
QString NeoChatConnection::deviceKey() const
{
return edKeyForUserDevice(userId(), deviceId());
@@ -252,4 +298,18 @@ QString NeoChatConnection::encryptionKey() const
return query.value(0).toString();
}
bool NeoChatConnection::isOnline() const
{
return m_isOnline;
}
void NeoChatConnection::setIsOnline(bool isOnline)
{
if (isOnline == m_isOnline) {
return;
}
m_isOnline = isOnline;
Q_EMIT isOnlineChanged();
}
#include "moc_neochatconnection.cpp"

View File

@@ -6,6 +6,7 @@
#include <QObject>
#include <QQmlEngine>
#include <QCoroTask>
#include <Quotient/connection.h>
class NeoChatConnection : public Quotient::Connection
@@ -26,6 +27,11 @@ class NeoChatConnection : public Quotient::Connection
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
public:
NeoChatConnection(QObject *parent = nullptr);
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
@@ -70,9 +76,20 @@ public:
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
// note: this is intentionally a copied QString because
// the reference could be destroyed before the task is finished
QCoro::Task<void> setupPushNotifications(QString endpoint);
QString deviceKey() const;
QString encryptionKey() const;
bool isOnline() const;
Q_SIGNALS:
void labelChanged();
void isOnlineChanged();
private:
bool m_isOnline = true;
void setIsOnline(bool isOnline);
};

View File

@@ -355,7 +355,7 @@ QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines
body = eventHandler.getPlainBody(stripNewlines);
}
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
return eventHandler.singleLineAuthorDisplayname() + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
}
return {};
}
@@ -452,17 +452,18 @@ QVariantMap NeoChatRoom::getUser(User *user) const
};
}
QString NeoChatRoom::avatarMediaId() const
QUrl NeoChatRoom::avatarMediaId() const
{
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
return avatar;
return connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(avatar)));
}
// Use the first (excluding self) user's avatar for direct chats
const auto dcUsers = directChatUsers();
for (const auto u : dcUsers) {
if (u != localUser()) {
return u->avatarMediaId(this);
const auto &avatar = u->avatarMediaId(this);
return avatar.isEmpty() ? QUrl() : connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(avatar)));
}
}
@@ -1304,7 +1305,7 @@ bool NeoChatRoom::isSpace()
return creationEvent->roomType() == RoomType::Space;
}
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical)
void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested)
{
if (!isSpace()) {
return;
@@ -1312,7 +1313,7 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
if (!canSendEvent("m.space.child"_ls)) {
return;
}
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}});
setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}});
if (setChildParent) {
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
@@ -1354,6 +1355,30 @@ void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent)
}
}
bool NeoChatRoom::isSuggested(const QString &childId)
{
if (!currentState().contains("m.space.child"_ls, childId)) {
return false;
}
const auto childEvent = currentState().get("m.space.child"_ls, childId);
return childEvent->contentPart<bool>("suggested"_ls);
}
void NeoChatRoom::toggleChildSuggested(const QString &childId)
{
if (!isSpace()) {
return;
}
if (!canSendEvent("m.space.child"_ls)) {
return;
}
if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) {
auto content = childEvent->contentJson();
content.insert("suggested"_ls, !childEvent->contentPart<bool>("suggested"_ls));
setState("m.space.child"_ls, childId, content);
}
}
PushNotificationState::State NeoChatRoom::pushNotificationState() const
{
return m_currentPushNotificationState;

View File

@@ -110,7 +110,7 @@ class NeoChatRoom : public Quotient::Room
/**
* @brief The avatar image to be used for the room.
*/
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(QUrl avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
/**
* @brief Get a user object for the other person in a direct chat.
@@ -501,7 +501,7 @@ public:
*/
[[nodiscard]] QString subtitleText();
[[nodiscard]] QString avatarMediaId() const;
[[nodiscard]] QUrl avatarMediaId() const;
Quotient::User *directChatRemoteUser() const;
@@ -562,7 +562,7 @@ public:
* Will fail if the user doesn't have the required privileges or this room is
* not a space.
*/
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false);
Q_INVOKABLE void addChild(const QString &childId, bool setChildParent = false, bool canonical = false, bool suggested = false);
/**
* @brief Remove the given room as a child.
@@ -572,6 +572,19 @@ public:
*/
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
/**
* @brief Whether the given child is a suggested room in the space.
*/
Q_INVOKABLE bool isSuggested(const QString &childId);
/**
* @brief Toggle whether the given child is a suggested room in the space.
*
* Will fail if the user doesn't have the required privileges, this room is
* not a space or the given room is not a child of this space.
*/
Q_INVOKABLE void toggleChildSuggested(const QString &childId);
bool isInvite() const;
Q_INVOKABLE void clearInvitationNotification();

View File

@@ -16,6 +16,10 @@
#include <Quotient/csapi/pushrules.h>
#include <Quotient/user.h>
#ifdef HAVE_KIO
#include <KIO/ApplicationLauncherJob>
#endif
#include "controller.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
@@ -292,6 +296,56 @@ void NotificationsManager::clearInvitationNotification(const QString &roomId)
}
}
void NotificationsManager::postPushNotification(const QByteArray &message)
{
const auto json = QJsonDocument::fromJson(message).object();
const auto type = json["notification"_ls]["type"_ls].toString();
// the only two types of push notifications we support right now
if (type == QStringLiteral("m.room.message") || type == QStringLiteral("m.room.encrypted")) {
auto notification = new KNotification("message"_ls);
const auto sender = json["notification"_ls]["sender_display_name"_ls].toString();
const auto roomName = json["notification"_ls]["room_name"_ls].toString();
const auto roomId = json["notification"_ls]["room_id"_ls].toString();
if (roomName.isEmpty() || sender == roomName) {
notification->setTitle(sender);
} else {
notification->setTitle(i18n("%1 (%2)", sender, roomName));
}
if (type == QStringLiteral("m.room.message")) {
const auto text = json["notification"_ls]["content"_ls]["body"_ls].toString();
notification->setText(text.toHtmlEscaped());
} else if (type == QStringLiteral("m.room.encrypted")) {
notification->setText(i18n("Encrypted Message"));
}
#ifdef HAVE_KIO
auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, this, [=]() {
QString properId = roomId;
properId = properId.replace(QStringLiteral("#"), QString());
properId = properId.replace(QStringLiteral("!"), QString());
auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.neochat")));
job->setUrls({QUrl::fromUserInput(QStringLiteral("matrix:r/%1").arg(properId))});
job->start();
});
#endif
connect(notification, &KNotification::closed, qGuiApp, &QGuiApplication::quit);
notification->sendEvent();
m_notifications.insert(roomId, notification);
} else {
qWarning() << "Skipping unsupported push notification" << type;
}
}
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
{
// Handle avatars that are lopsided in one dimension

View File

@@ -83,6 +83,11 @@ public:
*/
void clearInvitationNotification(const QString &roomId);
/**
* @brief Display a native notification for the given push notification.
*/
void postPushNotification(const QByteArray &message);
/**
* @brief Handle the notifications for the given connection.
*/

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