Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259b9884c7 | ||
|
|
2980dc49e4 | ||
|
|
5e12f50899 | ||
|
|
79940f707f | ||
|
|
e7024a270c | ||
|
|
1a561f6649 | ||
|
|
2cbb6ed65c | ||
|
|
ab4639926a | ||
|
|
89b6c54f25 | ||
|
|
27c9c62564 | ||
|
|
bb8ffb02d1 | ||
|
|
201aa82c04 | ||
|
|
704430db7a | ||
|
|
dcbbbd9296 | ||
|
|
410258c478 | ||
|
|
6baf2e4888 | ||
|
|
dd6eaac556 | ||
|
|
840903128c | ||
|
|
68f0ca96da | ||
|
|
faf7af06fe | ||
|
|
4ef67c3e0d | ||
|
|
5efd17d370 | ||
|
|
0dbef58ff2 | ||
|
|
6a3df8baf4 | ||
|
|
2fc973f218 | ||
|
|
7d16999c44 | ||
|
|
f9ba31f2dc | ||
|
|
d4ad773ff1 | ||
|
|
822a4dc500 | ||
|
|
bfd1d431c1 | ||
|
|
94bf2481f0 | ||
|
|
5c32520c35 | ||
|
|
de47597f6e | ||
|
|
7780a72888 | ||
|
|
1f8c07fedf | ||
|
|
877575b4d3 | ||
|
|
1181df4db2 | ||
|
|
5be2113b32 | ||
|
|
7ad7af56e8 | ||
|
|
d3148f8c8b | ||
|
|
100f595026 | ||
|
|
15ba6d58e2 | ||
|
|
61ad892732 | ||
|
|
2a3e1dfcd7 | ||
|
|
d1dc6fc4ed | ||
|
|
6dc30a9ca7 | ||
|
|
ae0c5ffaef | ||
|
|
fc546d4a43 | ||
|
|
96e62e3ebe | ||
|
|
d979cd2fbc | ||
|
|
22298181cb | ||
|
|
1312fde470 | ||
|
|
7fe2feb1e4 | ||
|
|
67fb5d0824 | ||
|
|
359114bd3d | ||
|
|
3db9f1198b | ||
|
|
2435a6b953 | ||
|
|
e6c8b3fa4b | ||
|
|
2d55dca508 | ||
|
|
bbb0bc3092 | ||
|
|
4065aa6a2e | ||
|
|
5942eac5ed | ||
|
|
85c2b7dada | ||
|
|
86ef921cdb | ||
|
|
aab69c5bae | ||
|
|
624578ec77 | ||
|
|
197ff984fd | ||
|
|
a26337d5f4 | ||
|
|
31b4eefadd | ||
|
|
b8e592f8ba | ||
|
|
555d23863e | ||
|
|
ffa2d5dc0e | ||
|
|
9987edbaf2 | ||
|
|
d90298392d | ||
|
|
600dbd0603 | ||
|
|
2179e2cc35 | ||
|
|
8119ea3ccb | ||
|
|
3fc125a798 | ||
|
|
99f6778df4 | ||
|
|
f7bd24db34 | ||
|
|
41c2f9c4d5 | ||
|
|
56d5f3036b | ||
|
|
21bb7dce21 | ||
|
|
11081719a7 | ||
|
|
980de7d85d | ||
|
|
7735313b0c | ||
|
|
1a3055df86 | ||
|
|
84cad630cd | ||
|
|
0beb5df08d | ||
|
|
4ef44b8e93 | ||
|
|
59153be006 | ||
|
|
50be96f762 |
10
.craft.ini
Normal file
10
.craft.ini
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
3
appiumtests/data/sync_response_no_rooms.json
Normal file
3
appiumtests/data/sync_response_no_rooms.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"next_batch": "batch1234"
|
||||
}
|
||||
50
appiumtests/data/sync_response_rooms.json
Normal file
50
appiumtests/data/sync_response_rooms.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
48
appiumtests/openuserdetailstest.py
Executable file
48
appiumtests/openuserdetailstest.py
Executable 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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
633
po/ar/neochat.po
633
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
624
po/az/neochat.po
624
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
616
po/ca/neochat.po
616
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
657
po/cs/neochat.po
657
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
615
po/da/neochat.po
615
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
614
po/de/neochat.po
614
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
628
po/el/neochat.po
628
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
632
po/eo/neochat.po
632
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
635
po/es/neochat.po
635
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
639
po/eu/neochat.po
639
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
621
po/fi/neochat.po
621
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
647
po/fr/neochat.po
647
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
621
po/hu/neochat.po
621
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
626
po/ia/neochat.po
626
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
621
po/id/neochat.po
621
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
623
po/ie/neochat.po
623
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
658
po/it/neochat.po
658
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
595
po/ja/neochat.po
595
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
619
po/ka/neochat.po
619
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
616
po/ko/neochat.po
616
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
596
po/lt/neochat.po
596
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
623
po/nl/neochat.po
623
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
606
po/nn/neochat.po
606
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
613
po/pa/neochat.po
613
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
642
po/pl/neochat.po
642
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
618
po/pt/neochat.po
618
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
627
po/ru/neochat.po
627
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
624
po/sk/neochat.po
624
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
627
po/sl/neochat.po
627
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
621
po/sv/neochat.po
621
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
619
po/ta/neochat.po
619
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
647
po/tr/neochat.po
647
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
624
po/uk/neochat.po
624
po/uk/neochat.po
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
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 "ientConnection : 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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
46
src/main.cpp
46
src/main.cpp
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
||||
fetchEmojis();
|
||||
});
|
||||
});
|
||||
CustomEmojiModel::fetchEmojis();
|
||||
}
|
||||
|
||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -46,6 +46,9 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void boundingBoxChanged();
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
166
src/models/notificationsmodel.cpp
Normal file
166
src/models/notificationsmodel.cpp
Normal 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 ¬ification : 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;
|
||||
}
|
||||
68
src/models/notificationsmodel.h
Normal file
68
src/models/notificationsmodel.h
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -132,6 +132,9 @@ Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void searchingChanged();
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void setSearching(bool searching);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -111,4 +111,6 @@ private:
|
||||
QList<Server> m_servers;
|
||||
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
|
||||
NeoChatConnection *m_connection = nullptr;
|
||||
|
||||
void initialize();
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
WorldReadableRole,
|
||||
IsJoinedRole,
|
||||
IsSpaceRole,
|
||||
IsSuggestedRole,
|
||||
CanAddChildrenRole,
|
||||
ParentDisplayNameRole,
|
||||
CanSetParentRole,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
95
src/models/timelinemodel.cpp
Normal file
95
src/models/timelinemodel.cpp
Normal 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
112
src/models/timelinemodel.h
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
5
src/org.kde.neochat.service.in
Normal file
5
src/org.kde.neochat.service.in
Normal 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
|
||||
@@ -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
66
src/qml/AttachDialog.qml
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user