Compare commits

...

92 Commits

Author SHA1 Message Date
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
141 changed files with 16687 additions and 11544 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

@@ -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

@@ -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();

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

@@ -139,6 +139,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 +177,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 +293,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 +376,7 @@ target_link_libraries(neochat PUBLIC
Qt::Network
Qt::QuickControls2
KF6::I18n
KF6::Kirigami2
KF6::Kirigami
KF6::Notifications
KF6::ConfigCore
KF6::ConfigGui
@@ -376,6 +387,7 @@ target_link_libraries(neochat PUBLIC
QuotientQt6
cmark::cmark
QCoro::Core
QCoro::Network
)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
@@ -464,7 +476,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 +495,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

@@ -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,15 +241,6 @@ 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();

View File

@@ -62,9 +62,9 @@ class MatrixImageProvider : public QQuickAsyncImageProvider
public:
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
{
static MatrixImageProvider instance;
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
return &instance;
static MatrixImageProvider *instance = new MatrixImageProvider;
engine->setObjectOwnership(instance, QQmlEngine::CppOwnership);
return instance;
}
/**

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
@@ -392,13 +389,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 +416,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 +431,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 +459,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 +725,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

@@ -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 {};
}
@@ -1304,7 +1304,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 +1312,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 +1354,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

@@ -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.
*/

View File

@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: none
# SPDX-License-Identifier: CC0-1.0
[D-BUS Service]
Name=org.kde.neochat
Exec=@CMAKE_INSTALL_PREFIX@/bin/neochat --dbus-activated

View File

@@ -88,7 +88,7 @@ FormCard.FormCardPage {
id: openFileDialog
OpenFileDialog {
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
parentWindow: root.Window.window
onAccepted: destroy()

66
src/qml/AttachDialog.qml Normal file
View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtCore
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
QQC2.Popup {
id: root
padding: 16
signal chosen(string path)
contentItem: RowLayout {
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
icon.name: 'mail-attachment'
text: i18n("Choose local file")
onClicked: {
root.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect(path => root.chosen(path))
fileDialog.open()
}
}
Kirigami.Separator {}
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
icon.name: 'insert-image'
text: i18n("Clipboard image")
onClicked: {
const path = StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + "/screenshots/" + (new Date()).getTime() + ".png"
if (!Clipboard.saveImage(path)) {
return;
}
root.chosen(path)
root.close();
}
}
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
}

View File

@@ -139,20 +139,17 @@ QQC2.Control {
RowLayout {
Layout.maximumWidth: root.maxContentWidth
visible: root.showAuthor
QQC2.Label {
QQC2.AbstractButton {
Layout.fillWidth: true
text: root.author.displayName
color: root.author.color
textFormat: Text.PlainText
font.weight: Font.Bold
elide: Text.ElideRight
TapHandler {
onTapped: RoomManager.visitUser(root.author.object, "mention")
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
contentItem: QQC2.Label {
text: root.author.displayName
color: root.author.color
textFormat: Text.PlainText
font.weight: Font.Bold
elide: Text.ElideRight
}
Accessible.name: contentItem.text
onClicked: RoomManager.resolveResource(root.author.id, "mention")
}
QQC2.Label {
text: root.timeString

View File

@@ -2,35 +2,25 @@
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import QtQuick.Window
import Qt.labs.platform as Platform
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.neochat.config
/**
* @brief The component which handles the message sending.
* @brief A component for typing and sending chat messages.
*
* The ChatBox deals with laying out the visual elements with the ChatBar handling
* the core functionality of displaying the current message composition before sending.
* This is designed to go to the bottom of the timeline and provides all the functionality
* required for the user to send messages to the room.
*
* This includes support for the following message types:
* - text
* - media (video, image, file)
* - emojis/stickers
* - location
*
* In addition, when replying, this component supports showing the message that is being
* In addition when replying this component supports showing the message that is being
* replied to.
*
* @note There is no edit functionality here this, is handled inline by the timeline
* text delegate.
*
* @sa ChatBox
* @sa ChatBar
*/
QQC2.Control {
id: root
@@ -39,17 +29,13 @@ QQC2.Control {
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
onActiveFocusChanged: textField.forceActiveFocus()
onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache
/**
* @brief The QQC2.TextArea object.
*
* @sa QQC2.TextArea
*/
property alias textField: textField
property NeoChatConnection connection
/**
* @brief The ActionsHandler object to use.
*
@@ -64,31 +50,22 @@ QQC2.Control {
* Each of these will be visualised in the ChatBar so new actions can be added
* by appending to this list.
*/
property list<Kirigami.Action> actions : [
property list<Kirigami.Action> actions: [
Kirigami.Action {
id: attachmentAction
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
// Matrix does not allow sending attachments in replies
visible: _private.chatBarCache.isReplying && _private.chatBarCache.attachmentPath.length === 0
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
displayHint: Kirigami.DisplayHint.IconOnly
onTriggered: {
if (Clipboard.hasImage) {
attachDialog.open()
} else {
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect((path) => {
if (!path) {
return;
}
_private.chatBarCache.attachmentPath = path;
})
fileDialog.open()
}
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(applicationWindow().overlay)
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path)
dialog.open()
}
tooltip: text
@@ -105,10 +82,10 @@ QQC2.Control {
checkable: true
onTriggered: {
if (emojiDialog.item.visible) {
emojiDialog.item.close()
if (emojiDialog.visible) {
emojiDialog.close()
} else {
emojiDialog.item.open()
emojiDialog.open()
}
}
tooltip: text
@@ -121,7 +98,7 @@ QQC2.Control {
displayHint: QQC2.AbstractButton.IconOnly
onTriggered: {
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
}
tooltip: text
},
@@ -136,7 +113,7 @@ QQC2.Control {
checkable: true
onTriggered: {
root.postMessage()
_private.postMessage()
}
tooltip: text
@@ -148,294 +125,308 @@ QQC2.Control {
*/
signal messageSent()
leftPadding: 0
rightPadding: 0
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
anchors.left: parent.left
anchors.right:parent.right
anchors.top: parent.top
}
}
leftPadding: rightPadding
rightPadding: (root.width - chatBarSizeHelper.currentWidth) / 2
topPadding: 0
bottomPadding: 0
contentItem: QQC2.ScrollView {
id: chatBarScrollView
property var textFieldHeight: textField.height
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
Behavior on implicitHeight {
NumberAnimation {
id: chatBarHeightAnimation
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutCubic
}
contentItem: ColumnLayout {
spacing: 0
Item { // Required to adjust for the top separator
Layout.preferredHeight: 1
Layout.fillWidth: true
}
Loader {
id: paneLoader
QQC2.TextArea {
id: textField
Layout.fillWidth: true
Layout.margins: Kirigami.Units.largeSpacing
x: Math.round((root.width - chatBarSizeHelper.currentWidth) / 2) - (root.width > chatBarSizeHelper.currentWidth + Kirigami.Units.largeSpacing * 2.5 ? Kirigami.Units.largeSpacing * 1.5 : 0)
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
bottomPadding: Kirigami.Units.largeSpacing
leftPadding: LayoutMirroring.enabled ? actionsRow.width : Kirigami.Units.largeSpacing
rightPadding: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : actionsRow.width + x * 2 + Kirigami.Units.largeSpacing * 2
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : _private.chatBarCache.attachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: Text.Wrap
Accessible.description: placeholderText
// opt-out of whatever spell checker a styled TextArea might come with
Kirigami.SpellCheck.enabled: false
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
root.currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
_private.chatBarCache.text = text
}
onCursorRectangleChanged: chatBarScrollView.ensureVisible(cursorRectangle)
onSelectedTextChanged: {
if (selectedText.length > 0) {
quickFormatBar.selectionStart = selectionStart
quickFormatBar.selectionEnd = selectionEnd
quickFormatBar.open()
}
}
QuickFormatBar {
id: quickFormatBar
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd)
} else {
remove(cursorPosition, cursorPosition + 1)
}
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible) {
quickFormatBar.close()
}
}
Keys.onEnterPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
root.postMessage();
}
}
Keys.onReturnPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
root.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
event.accepted = root.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
root.currentRoom.replyLastMessage();
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
root.currentRoom.editLastMessage();
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace) {
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible && selectedText.length > 0) {
quickFormatBar.close()
}
}
}
Keys.onShortcutOverride: event => {
// Accept the event only when there was something to cancel. Otherwise, let the event go to the RoomPage.
if (cancelButton.visible && event.key === Qt.Key_Escape) {
cancelButton.action.trigger();
event.accepted = true;
}
}
Loader {
id: paneLoader
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.right: parent.right
anchors.rightMargin: root.width > chatBarSizeHelper.currentWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing)
active: visible
visible: _private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0
sourceComponent: _private.chatBarCache.isReplying ? replyPane : attachmentPane
}
Component {
id: replyPane
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarSource
text: _private.chatBarCache.relationMessage
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: _private.chatBarCache.attachmentPath
onAttachmentCancelled: {
_private.chatBarCache.attachmentPath = "";
root.forceActiveFocus()
}
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
active: visible
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
sourceComponent: root.currentRoom.mainCache.replyId.length > 0 ? replyPane : attachmentPane
}
RowLayout {
QQC2.ScrollView {
id: chatBarScrollView
/**
* Because of the paneLoader we have to manage the scroll
* position manually or it doesn't keep the cursor visible properly all the time.
*/
function ensureVisible(r) {
// Find the child that is the Flickable created by ScrollView.
let flickable = undefined;
for (var index in children) {
if (children[index] instanceof Flickable) {
flickable = children[index];
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
Behavior on implicitHeight {
NumberAnimation {
id: chatBarHeightAnimation
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutCubic
}
}
QQC2.TextArea {
id: textField
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18n("Set an attachment caption…") : i18n("Send a message…")
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
Accessible.description: placeholderText
Kirigami.SpellCheck.enabled: false
Timer {
id: repeatTimer
interval: 5000
}
onTextChanged: {
if (!repeatTimer.running && Config.typingNotifications) {
var textExists = text.length > 0
root.currentRoom.sendTypingNotification(textExists)
textExists ? repeatTimer.start() : repeatTimer.stop()
}
_private.chatBarCache.text = text
}
onSelectedTextChanged: {
if (selectedText.length > 0) {
quickFormatBar.selectionStart = selectionStart
quickFormatBar.selectionEnd = selectionEnd
quickFormatBar.open()
}
}
QuickFormatBar {
id: quickFormatBar
x: textField.cursorRectangle.x
y: textField.cursorRectangle.y - height
onFormattingSelected: root.formatText(format, selectionStart, selectionEnd)
}
Keys.onDeletePressed: {
if (selectedText.length > 0) {
remove(selectionStart, selectionEnd)
} else {
remove(cursorPosition, cursorPosition + 1)
}
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible) {
quickFormatBar.close()
}
}
Keys.onEnterPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
_private.postMessage();
}
}
Keys.onReturnPressed: event => {
if (completionMenu.visible) {
completionMenu.complete()
} else if (event.modifiers & Qt.ShiftModifier || Kirigami.Settings.isMobile) {
textField.insert(cursorPosition, "\n")
} else {
_private.postMessage();
}
}
Keys.onTabPressed: {
if (completionMenu.visible) {
completionMenu.complete()
}
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
event.accepted = _private.pasteImage();
} else if (event.key === Qt.Key_Up && event.modifiers & Qt.ControlModifier) {
root.currentRoom.replyLastMessage();
} else if (event.key === Qt.Key_Up && textField.text.length === 0) {
root.currentRoom.editLastMessage();
} else if (event.key === Qt.Key_Up && completionMenu.visible) {
completionMenu.decrementIndex()
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
completionMenu.incrementIndex()
} else if (event.key === Qt.Key_Backspace) {
if (textField.text == selectedText || textField.text.length <= 1) {
root.currentRoom.sendTypingNotification(false)
repeatTimer.stop()
}
if (quickFormatBar.visible && selectedText.length > 0) {
quickFormatBar.close()
}
}
}
Keys.onShortcutOverride: event => {
if (completionMenu.visible) {
completionMenu.close()
} else if ((_private.chatBarCache.isReplying || _private.chatBarCache.attachmentPath.length > 0) && event.key === Qt.Key_Escape) {
_private.chatBarCache.attachmentPath = ""
_private.chatBarCache.replyId = ""
_private.chatBarCache.threadId = ""
event.accepted = true;
}
}
background: MouseArea {
acceptedButtons: Qt.NoButton
cursorShape: Qt.IBeamCursor
z: 1
}
}
}
RowLayout {
id: actionsRow
spacing: 0
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
if (flickable) {
if (flickable.contentX >= r.x) {
flickable.contentX = r.x;
} else if (flickable.contentX + width <= r.x + r.width) {
flickable.contentX = r.x + r.width - width;
} if (flickable.contentY >= r.y) {
flickable.contentY = r.y;
} else if (flickable.contentY + height <= r.y + r.height) {
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: modelData.tooltip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
PieProgressBar {
visible: modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
}
}
}
}
}
QQC2.ToolButton {
id: cancelButton
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
visible: _private.chatBarCache.isReplying
display: QQC2.AbstractButton.IconOnly
action: Kirigami.Action {
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onTriggered: {
parentWidth: root.width
}
Component {
id: replyPane
ReplyPane {
userName: _private.chatBarCache.relationUser.displayName
userColor: _private.chatBarCache.relationUser.color
userAvatar: _private.chatBarCache.relationUser.avatarSource
text: _private.chatBarCache.relationMessage
onCancel: {
_private.chatBarCache.replyId = "";
_private.chatBarCache.attachmentPath = "";
}
}
}
Component {
id: attachmentPane
AttachmentPane {
attachmentPath: _private.chatBarCache.attachmentPath
onAttachmentCancelled: {
_private.chatBarCache.attachmentPath = "";
root.forceActiveFocus()
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
}
RowLayout {
id: actionsRow
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: layoutDirection === Qt.RightToLeft ? requiredMargin : 0
anchors.rightMargin: layoutDirection === Qt.RightToLeft ? 0 : requiredMargin
anchors.bottomMargin: Kirigami.Units.smallSpacing
spacing: 0
property var requiredMargin: (root.width - chatBarSizeHelper.currentWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBarSizeHelper.currentWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0)
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
QtObject {
id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: modelData.tooltip
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
function postMessage() {
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
repeatTimer.stop()
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
messageSent()
}
PieProgressBar {
visible: modelData.isBusy
progress: root.currentRoom.fileUploadingProgress
}
function formatText(format, selectionStart, selectionEnd) {
let index = textField.cursorPosition;
/*
* There cannot be white space at the beginning or end of the string for the
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
*/
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
if (innerText.charAt(innerText.length - 1) === " ") {
let trimmedRightString = innerText.replace(/\s*$/,"");
let trimDifference = innerText.length - trimmedRightString.length;
selectionEnd -= trimDifference;
}
if (innerText.charAt(0) === " ") {
let trimmedLeftString = innerText.replace(/^\s*/,"");
let trimDifference = innerText.length - trimmedLeftString.length;
selectionStart = selectionStart + trimDifference;
}
let startText = textField.text.substr(0, selectionStart);
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
let endText = textField.text.substr(selectionEnd);
textField.text = "";
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
/*
* Put the cursor where it was when the popup was opened accounting for the
* new markup.
*
* The exception is for a hyperlink where it is placed ready to start typing
* the url.
*/
if (format.extra !== "") {
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
} else if (index == selectionStart) {
textField.cursorPosition = index;
} else {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
}
Loader {
id: emojiDialog
active: !Kirigami.Settings.isMobile
sourceComponent: EmojiDialog {
x: root.width - width
y: -implicitHeight // - Kirigami.Units.smallSpacing
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) emojiAction.checked = false
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
CompletionMenu {
id: completionMenu
height: implicitHeight
y: -height - 5
z: 1
chatDocumentHandler: documentHandler
connection: root.connection
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
function pasteImage() {
let localPath = Clipboard.saveImage();
if (localPath.length === 0) {
return false;
}
_private.chatBarCache.attachmentPath = localPath;
return true;
}
}
@@ -452,21 +443,59 @@ QQC2.Control {
}
}
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
Component {
id: openFileDialog
parentWidth: root.width
OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
function forceActiveFocus() {
textField.forceActiveFocus();
// set the cursor to the end of the text
textField.cursorPosition = textField.length;
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
Component {
id: locationChooser
LocationChooser {}
}
CompletionMenu {
id: completionMenu
chatDocumentHandler: documentHandler
connection: root.connection
x: 1
y: -height
width: parent.width - 1
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
}
}
}
EmojiDialog {
id: emojiDialog
x: root.width - width
y: -implicitHeight
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom
onChosen: emoji => insertText(emoji)
onClosed: if (emojiAction.checked) emojiAction.checked = false
}
function insertText(text) {
@@ -475,139 +504,4 @@ QQC2.Control {
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
textField.cursorPosition = initialCursorPosition + text.length
}
function pasteImage() {
let localPath = Clipboard.saveImage();
if (localPath.length === 0) {
return false;
}
_private.chatBarCache.attachmentPath = localPath;
return true;
}
function postMessage() {
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
repeatTimer.stop()
root.currentRoom.markAllMessagesAsRead();
textField.clear();
_private.chatBarCache.replyId = "";
messageSent()
}
function formatText(format, selectionStart, selectionEnd) {
let index = textField.cursorPosition;
/*
* There cannot be white space at the beginning or end of the string for the
* formatting to work so move the sectionStart and sectionEnd markers past any whitespace.
*/
let innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
if (innerText.charAt(innerText.length - 1) === " ") {
let trimmedRightString = innerText.replace(/\s*$/,"");
let trimDifference = innerText.length - trimmedRightString.length;
selectionEnd -= trimDifference;
}
if (innerText.charAt(0) === " ") {
let trimmedLeftString = innerText.replace(/^\s*/,"");
let trimDifference = innerText.length - trimmedLeftString.length;
selectionStart = selectionStart + trimDifference;
}
let startText = textField.text.substr(0, selectionStart);
// Needs updating with the new selectionStart and selectionEnd with white space trimmed.
innerText = textField.text.substr(selectionStart, selectionEnd - selectionStart);
let endText = textField.text.substr(selectionEnd);
textField.text = "";
textField.text = startText + format.start + innerText + format.end + format.extra + endText;
/*
* Put the cursor where it was when the popup was opened accounting for the
* new markup.
*
* The exception is for a hyperlink where it is placed ready to start typing
* the url.
*/
if (format.extra !== "") {
textField.cursorPosition = selectionEnd + format.start.length + format.end.length;
} else if (index == selectionStart) {
textField.cursorPosition = index;
} else {
textField.cursorPosition = index + format.start.length + format.end.length;
}
}
Component {
id: locationChooserComponent
LocationChooser {}
}
QQC2.Popup {
anchors.centerIn: parent
id: attachDialog
padding: 16
contentItem: RowLayout {
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
icon.name: 'mail-attachment'
text: i18n("Choose local file")
onClicked: {
attachDialog.close()
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
fileDialog.chosen.connect(function (path) {
if (!path) {
return;
}
_private.chatBarCache.attachmentPath = path;
})
fileDialog.open()
}
}
Kirigami.Separator {
}
QQC2.ToolButton {
Layout.preferredWidth: 160
Layout.fillHeight: true
padding: 16
icon.name: 'insert-image'
text: i18n("Clipboard image")
onClicked: {
const localPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png"
if (!Clipboard.saveImage(localPath)) {
return;
}
_private.chatBarCache.attachmentPath = localPath;
attachDialog.close();
}
}
}
}
Component {
id: openFileDialog
OpenFileDialog {
parentWindow: Window.window
}
}
QtObject {
id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
}
}

View File

@@ -1,99 +0,0 @@
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.de>
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component for typing and sending chat messages.
*
* This is designed to go to the bottom of the timeline and provides all the functionality
* required for the user to send messages to the room.
*
* This includes support for the following message types:
* - text
* - media (video, image, file)
* - emojis/stickers
* - location
*
* In addition when replying this component supports showing the message that is being
* replied to.
*
* @note The main role of this component is to layout the elements. The main functionality
* is handled by ChatBar
*
* @sa ChatBar
*/
ColumnLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
/**
* @brief The ActionsHandler object to use.
*
* This is expected to have the correct room set otherwise messages will be sent
* to the wrong room.
*/
required property ActionsHandler actionsHandler
/**
* @brief A message has been sent from the chat bar.
*/
signal messageSent()
/**
* @brief Insert the given text into the ChatBar.
*
* The text is inserted at the current cursor location.
*/
function insertText(text) {
chatBar.insertText(text)
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Kirigami.Separator {
Layout.fillWidth: true
}
ChatBar {
id: chatBar
connection: root.connection
visible: root.currentRoom.canSendEvent("m.room.message")
Layout.fillWidth: true
Layout.minimumHeight: Math.max(Kirigami.Units.gridUnit * 2, Math.round(implicitHeight) + Kirigami.Units.largeSpacing)
// lineSpacing is height+leading, so subtract leading once since leading only exists between lines.
Layout.maximumHeight: chatBarFontMetrics.lineSpacing * 8 - chatBarFontMetrics.leading + textField.topPadding + textField.bottomPadding
Layout.preferredHeight: Math.round(implicitHeight)
currentRoom: root.currentRoom
actionsHandler: root.actionsHandler
FontMetrics {
id: chatBarFontMetrics
font: chatBar.textField.font
}
onMessageSent: {
root.messageSent();
}
}
onActiveFocusChanged: chatBar.forceActiveFocus()
}

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