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
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
- project: sysadmin/ci-utilities
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
file:
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
- /gitlab-templates/reuse-lint.yml
|
||||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
- /gitlab-templates/android-qt6.yml
|
||||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
- /gitlab-templates/linux-qt6.yml
|
||||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.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/qqc2-desktop-style': '@latest-kf6'
|
||||||
'frameworks/kio': '@latest-kf6'
|
'frameworks/kio': '@latest-kf6'
|
||||||
'frameworks/kwindowsystem': '@latest-kf6'
|
'frameworks/kwindowsystem': '@latest-kf6'
|
||||||
|
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||||
- 'on': ['Linux', 'FreeBSD']
|
- 'on': ['Linux', 'FreeBSD']
|
||||||
'require':
|
'require':
|
||||||
'frameworks/kdbusaddons': '@latest-kf6'
|
'frameworks/kdbusaddons': '@latest-kf6'
|
||||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
|
||||||
|
|
||||||
- 'on': ['Linux']
|
- 'on': ['Linux']
|
||||||
'require':
|
'require':
|
||||||
|
|||||||
@@ -45,3 +45,7 @@ License: BSD-2-Clause
|
|||||||
Files: autotests/data/*
|
Files: autotests/data/*
|
||||||
Copyright: none
|
Copyright: none
|
||||||
License: CC0-1.0
|
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.
|
# KDE Applications version, managed by release script.
|
||||||
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "01")
|
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}")
|
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||||
|
|
||||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||||
@@ -58,12 +58,12 @@ set_package_properties(Qt6 PROPERTIES
|
|||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
PURPOSE "Basic application components"
|
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
|
set_package_properties(KF6 PROPERTIES
|
||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
PURPOSE "Basic application components"
|
PURPOSE "Basic application components"
|
||||||
)
|
)
|
||||||
set_package_properties(KF6Kirigami2 PROPERTIES
|
set_package_properties(KF6Kirigami PROPERTIES
|
||||||
TYPE REQUIRED
|
TYPE REQUIRED
|
||||||
PURPOSE "Kirigami application UI framework"
|
PURPOSE "Kirigami application UI framework"
|
||||||
)
|
)
|
||||||
@@ -102,8 +102,7 @@ set_package_properties(QuotientQt6 PROPERTIES
|
|||||||
PURPOSE "Talk with matrix server"
|
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)
|
||||||
if (NOT TARGET Olm::Olm AND NOT ANDROID)
|
|
||||||
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
|
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
|||||||
PURPOSE "Add image editing capability to image attachments"
|
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()
|
qcoro_enable_coroutines()
|
||||||
|
|
||||||
@@ -140,6 +139,13 @@ set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
|
|||||||
TYPE OPTIONAL
|
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)
|
if(ANDROID)
|
||||||
find_package(Sqlite3)
|
find_package(Sqlite3)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
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://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://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
|
## Introduction
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ As is the case throughout the KDE ecosystem contributions are welcome from all.
|
|||||||
|
|
||||||
## Contact
|
## 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
|
## Acknowledgement
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ add_test(
|
|||||||
NAME logintest
|
NAME logintest
|
||||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
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-License-Identifier: MIT
|
||||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
|
||||||
|
import json
|
||||||
from flask import Flask, request, abort
|
from flask import Flask, request, abort
|
||||||
|
import os
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/_matrix/client/v3/login", methods=["GET"])
|
@app.route("/_matrix/client/v3/login", methods=["GET"])
|
||||||
def login_get():
|
def login_get():
|
||||||
result = dict()
|
result = dict()
|
||||||
@@ -12,6 +14,13 @@ def login_get():
|
|||||||
result["flows"][0]["type"] = "m.login.password"
|
result["flows"][0]["type"] = "m.login.password"
|
||||||
return result
|
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"])
|
@app.route("/_matrix/client/v3/login", methods=["POST"])
|
||||||
def login_post():
|
def login_post():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -19,15 +28,22 @@ def login_post():
|
|||||||
abort(403)
|
abort(403)
|
||||||
print(data)
|
print(data)
|
||||||
result = dict()
|
result = dict()
|
||||||
result["access_token"] = "token_1234"
|
result["access_token"] = "token_login"
|
||||||
result["device_id"] = "device_1234"
|
result["device_id"] = "device_1234"
|
||||||
result["user_id"] = "@user:localhost:1234"
|
result["user_id"] = "@user:localhost:1234"
|
||||||
return result
|
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")
|
@app.route("/_matrix/client/r0/sync")
|
||||||
def 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
|
return result
|
||||||
|
|
||||||
@app.route("/.well-known/matrix/client")
|
@app.route("/.well-known/matrix/client")
|
||||||
@@ -37,6 +53,18 @@ def well_known():
|
|||||||
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
|
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
|
||||||
return reply
|
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__":
|
if __name__ == "__main__":
|
||||||
app.run(ssl_context='adhoc', port=1234)
|
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": {
|
"unsigned": {
|
||||||
"age": 1234
|
"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": {
|
"unsigned": {
|
||||||
"age": 1238
|
"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,
|
"limited": true,
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ private Q_SLOTS:
|
|||||||
void nullAuthor();
|
void nullAuthor();
|
||||||
void authorDisplayName();
|
void authorDisplayName();
|
||||||
void nullAuthorDisplayName();
|
void nullAuthorDisplayName();
|
||||||
|
void singleLineSidplayName();
|
||||||
|
void nullSingleLineDisplayName();
|
||||||
void time();
|
void time();
|
||||||
void nullTime();
|
void nullTime();
|
||||||
void timeString();
|
void timeString();
|
||||||
@@ -203,6 +205,23 @@ void EventHandlerTest::nullAuthorDisplayName()
|
|||||||
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
|
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()
|
void EventHandlerTest::time()
|
||||||
{
|
{
|
||||||
auto event = room->messageEvents().at(0).get();
|
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
|
chatbarcache.h
|
||||||
colorschemer.cpp
|
colorschemer.cpp
|
||||||
colorschemer.h
|
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
|
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/TypingPane.qml
|
||||||
qml/QuickSwitcher.qml
|
qml/QuickSwitcher.qml
|
||||||
qml/HoverActions.qml
|
qml/HoverActions.qml
|
||||||
qml/ChatBox.qml
|
|
||||||
qml/ChatBar.qml
|
qml/ChatBar.qml
|
||||||
qml/AttachmentPane.qml
|
qml/AttachmentPane.qml
|
||||||
qml/ReplyPane.qml
|
qml/ReplyPane.qml
|
||||||
@@ -290,11 +293,19 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
|||||||
qml/Security.qml
|
qml/Security.qml
|
||||||
qml/QrCodeMaximizeComponent.qml
|
qml/QrCodeMaximizeComponent.qml
|
||||||
qml/SelectSpacesDialog.qml
|
qml/SelectSpacesDialog.qml
|
||||||
|
qml/AttachDialog.qml
|
||||||
|
qml/NotificationsView.qml
|
||||||
|
qml/LoadingDelegate.qml
|
||||||
|
qml/TimelineEndDelegate.qml
|
||||||
RESOURCES
|
RESOURCES
|
||||||
qml/confetti.png
|
qml/confetti.png
|
||||||
qml/glowdot.png
|
qml/glowdot.png
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
|
||||||
|
endif()
|
||||||
|
|
||||||
ecm_qt_declare_logging_category(neochat
|
ecm_qt_declare_logging_category(neochat
|
||||||
HEADER "messageeventmodel_logging.h"
|
HEADER "messageeventmodel_logging.h"
|
||||||
IDENTIFIER "MessageEvent"
|
IDENTIFIER "MessageEvent"
|
||||||
@@ -365,7 +376,7 @@ target_link_libraries(neochat PUBLIC
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::QuickControls2
|
Qt::QuickControls2
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
KF6::Kirigami2
|
KF6::Kirigami
|
||||||
KF6::Notifications
|
KF6::Notifications
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
KF6::ConfigGui
|
KF6::ConfigGui
|
||||||
@@ -376,6 +387,7 @@ target_link_libraries(neochat PUBLIC
|
|||||||
QuotientQt6
|
QuotientQt6
|
||||||
cmark::cmark
|
cmark::cmark
|
||||||
QCoro::Core
|
QCoro::Core
|
||||||
|
QCoro::Network
|
||||||
)
|
)
|
||||||
|
|
||||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||||
@@ -464,7 +476,7 @@ if(ANDROID)
|
|||||||
"gps"
|
"gps"
|
||||||
"system-users-symbolic"
|
"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()
|
else()
|
||||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
|
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
|
||||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||||
@@ -483,9 +495,18 @@ if (TARGET KF6::KIOWidgets)
|
|||||||
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
||||||
endif()
|
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})
|
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||||
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
||||||
endif()
|
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);
|
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
|
||||||
|
|
||||||
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
|
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
|
||||||
return NULL;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return pixelArray;
|
return pixelArray;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,11 +132,7 @@ int ChatDocumentHandler::completionStartIndex() const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID)
|
const qsizetype cursor = cursorPosition();
|
||||||
const long long cursor = cursorPosition();
|
|
||||||
#else
|
|
||||||
const auto cursor = cursorPosition();
|
|
||||||
#endif
|
|
||||||
const auto &text = getText();
|
const auto &text = getText();
|
||||||
|
|
||||||
auto start = std::min(cursor, text.size()) - 1;
|
auto start = std::min(cursor, text.size()) - 1;
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ private:
|
|||||||
|
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
QPointer<ChatBarCache> m_chatBarCache;
|
QPointer<ChatBarCache> m_chatBarCache;
|
||||||
bool completionVisible = false;
|
|
||||||
|
|
||||||
QColor m_mentionColor;
|
QColor m_mentionColor;
|
||||||
QColor m_errorColor;
|
QColor m_errorColor;
|
||||||
@@ -172,7 +171,5 @@ private:
|
|||||||
|
|
||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
|
|
||||||
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
|
|
||||||
|
|
||||||
CompletionModel *m_completionModel = nullptr;
|
CompletionModel *m_completionModel = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
#include "trayicon_sni.h"
|
#include "trayicon_sni.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
bool testMode = false;
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
Controller::Controller(QObject *parent)
|
Controller::Controller(QObject *parent)
|
||||||
@@ -56,9 +58,19 @@ Controller::Controller(QObject *parent)
|
|||||||
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
|
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this] {
|
if (!testMode) {
|
||||||
invokeLogin();
|
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] {
|
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
|
||||||
delete m_trayIcon;
|
delete m_trayIcon;
|
||||||
@@ -93,12 +105,30 @@ Controller::Controller(QObject *parent)
|
|||||||
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
connect(&m_accountRegistry, &AccountRegistry::accountCountChanged, this, [this]() {
|
||||||
if (m_accountRegistry.size() > oldAccountCount) {
|
if (m_accountRegistry.size() > oldAccountCount) {
|
||||||
auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry.accounts()[m_accountRegistry.size() - 1]);
|
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);
|
NotificationsManager::instance().handleNotifications(connection);
|
||||||
});
|
});
|
||||||
|
connectSingleShot(connection, &NeoChatConnection::syncDone, this, [this, connection] {
|
||||||
|
connection->setupPushNotifications(m_endpoint);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
oldAccountCount = m_accountRegistry.size();
|
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()
|
Controller &Controller::instance()
|
||||||
@@ -300,9 +330,6 @@ void Controller::setQuitOnLastWindowClosed()
|
|||||||
m_trayIcon = nullptr;
|
m_trayIcon = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QGuiApplication::setQuitOnLastWindowClosed(!NeoChatConfig::self()->systemTray());
|
|
||||||
#else
|
|
||||||
return;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,20 +353,6 @@ void Controller::setActiveConnection(NeoChatConnection *connection)
|
|||||||
m_connection = connection;
|
m_connection = connection;
|
||||||
if (connection != nullptr) {
|
if (connection != nullptr) {
|
||||||
NeoChatConfig::self()->setActiveConnection(connection->userId());
|
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) {
|
connect(connection, &NeoChatConnection::requestFailed, this, [](BaseJob *job) {
|
||||||
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_ls].toString() == "M_TOO_LARGE"_ls) {
|
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."));
|
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();
|
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)
|
void Controller::forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item)
|
||||||
{
|
{
|
||||||
// HACK: Workaround bug QTBUG 93281
|
// HACK: Workaround bug QTBUG 93281
|
||||||
connect(textDocument->textDocument(), SIGNAL(imagesLoaded()), item, SLOT(updateWholeDocument()));
|
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()
|
void Controller::setApplicationProxy()
|
||||||
{
|
{
|
||||||
NeoChatConfig *cfg = NeoChatConfig::self();
|
NeoChatConfig *cfg = NeoChatConfig::self();
|
||||||
@@ -425,3 +434,8 @@ AccountRegistry &Controller::accounts()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "moc_controller.cpp"
|
#include "moc_controller.cpp"
|
||||||
|
|
||||||
|
void Controller::setTestMode(bool test)
|
||||||
|
{
|
||||||
|
testMode = test;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
#include <Quotient/jobs/basejob.h>
|
#include <Quotient/jobs/basejob.h>
|
||||||
#include <Quotient/settings.h>
|
#include <Quotient/settings.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_KUNIFIEDPUSH
|
||||||
|
#include <kunifiedpush/connector.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
class TrayIcon;
|
class TrayIcon;
|
||||||
class QQuickTextDocument;
|
class QQuickTextDocument;
|
||||||
@@ -51,11 +55,6 @@ class Controller : public QObject
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(bool supportSystemTray READ supportSystemTray CONSTANT)
|
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.
|
* @brief Whether NeoChat is running as a flatpak.
|
||||||
*
|
*
|
||||||
@@ -101,15 +100,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
bool saveAccessTokenToKeyChain(const Quotient::AccountSettings &account, const QByteArray &accessToken);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Join a room.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void joinRoom(const QString &alias);
|
|
||||||
|
|
||||||
[[nodiscard]] bool supportSystemTray() const;
|
[[nodiscard]] bool supportSystemTray() const;
|
||||||
|
|
||||||
bool isOnline() const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sets the QNetworkProxy for the application.
|
* @brief Sets the QNetworkProxy for the application.
|
||||||
*
|
*
|
||||||
@@ -126,8 +118,16 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void forceRefreshTextDocument(QQuickTextDocument *textDocument, QQuickItem *item);
|
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();
|
Quotient::AccountRegistry &accounts();
|
||||||
|
|
||||||
|
static void setTestMode(bool testMode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit Controller(QObject *parent = nullptr);
|
explicit Controller(QObject *parent = nullptr);
|
||||||
|
|
||||||
@@ -138,11 +138,11 @@ private:
|
|||||||
|
|
||||||
void loadSettings();
|
void loadSettings();
|
||||||
void saveSettings() const;
|
void saveSettings() const;
|
||||||
bool m_isOnline = true;
|
|
||||||
QMap<Quotient::Room *, int> m_notificationCounts;
|
QMap<Quotient::Room *, int> m_notificationCounts;
|
||||||
|
|
||||||
Quotient::AccountRegistry m_accountRegistry;
|
Quotient::AccountRegistry m_accountRegistry;
|
||||||
QStringList m_accountsLoading;
|
QStringList m_accountsLoading;
|
||||||
|
QString m_endpoint;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void invokeLogin();
|
void invokeLogin();
|
||||||
@@ -164,7 +164,6 @@ Q_SIGNALS:
|
|||||||
void activeConnectionChanged();
|
void activeConnectionChanged();
|
||||||
void passwordStatus(Controller::PasswordStatus status);
|
void passwordStatus(Controller::PasswordStatus status);
|
||||||
void userConsentRequired(QUrl url);
|
void userConsentRequired(QUrl url);
|
||||||
void isOnlineChanged(bool isOnline);
|
|
||||||
void accountsLoadingChanged();
|
void accountsLoadingChanged();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public:
|
|||||||
Poll, /**< The initial event for a poll. */
|
Poll, /**< The initial event for a poll. */
|
||||||
Location, /**< A location event. */
|
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). */
|
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. */
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
};
|
};
|
||||||
Q_ENUM(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
|
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
|
||||||
{
|
{
|
||||||
if (m_event == nullptr) {
|
if (m_event == nullptr) {
|
||||||
@@ -663,12 +685,12 @@ QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo
|
|||||||
QVariantMap mediaInfo;
|
QVariantMap mediaInfo;
|
||||||
|
|
||||||
// Get the mxc URL for the media.
|
// 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();
|
mediaInfo["source"_ls] = QUrl();
|
||||||
} else {
|
} else {
|
||||||
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
||||||
|
|
||||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
if (source.isValid()) {
|
||||||
mediaInfo["source"_ls] = source;
|
mediaInfo["source"_ls] = source;
|
||||||
} else {
|
} else {
|
||||||
mediaInfo["source"_ls] = QUrl();
|
mediaInfo["source"_ls] = QUrl();
|
||||||
|
|||||||
@@ -111,6 +111,17 @@ public:
|
|||||||
*/
|
*/
|
||||||
QString getAuthorDisplayName(bool isPending = false) const;
|
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.
|
* @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());
|
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();
|
qml_register_types_org_kde_neochat();
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
|
qmlRegisterSingletonInstance("org.kde.neochat.config", 1, 0, "Config", NeoChatConfig::self());
|
||||||
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
|
qmlRegisterSingletonInstance("org.kde.neochat.accounts", 1, 0, "AccountRegistry", &Controller::instance().accounts());
|
||||||
@@ -180,7 +214,6 @@ int main(int argc, char *argv[])
|
|||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
#ifdef HAVE_KDBUSADDONS
|
#ifdef HAVE_KDBUSADDONS
|
||||||
KDBusService service(KDBusService::Unique);
|
|
||||||
service.connect(&service,
|
service.connect(&service,
|
||||||
&KDBusService::activateRequested,
|
&KDBusService::activateRequested,
|
||||||
&RoomManager::instance(),
|
&RoomManager::instance(),
|
||||||
@@ -199,7 +232,7 @@ int main(int argc, char *argv[])
|
|||||||
auto args = arguments;
|
auto args = arguments;
|
||||||
args.removeFirst();
|
args.removeFirst();
|
||||||
for (const auto &arg : args) {
|
for (const auto &arg : args) {
|
||||||
RoomManager::instance().openResource(arg);
|
RoomManager::instance().resolveResource(arg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
@@ -208,15 +241,6 @@ int main(int argc, char *argv[])
|
|||||||
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
|
QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QCoreApplication::quit);
|
||||||
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
|
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)) {
|
if (parser.isSet("ignore-ssl-errors"_ls)) {
|
||||||
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
|
QObject::connect(NetworkAccessManager::instance(), &QNetworkAccessManager::sslErrors, NetworkAccessManager::instance(), [](QNetworkReply *reply) {
|
||||||
reply->ignoreSslErrors();
|
reply->ignoreSslErrors();
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ class MatrixImageProvider : public QQuickAsyncImageProvider
|
|||||||
public:
|
public:
|
||||||
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
|
static MatrixImageProvider *create(QQmlEngine *engine, QJSEngine *)
|
||||||
{
|
{
|
||||||
static MatrixImageProvider instance;
|
static MatrixImageProvider *instance = new MatrixImageProvider;
|
||||||
engine->setObjectOwnership(&instance, QQmlEngine::CppOwnership);
|
engine->setObjectOwnership(instance, QQmlEngine::CppOwnership);
|
||||||
return &instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ QList<ActionsModel::Action> actions{
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
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();
|
return QString();
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@@ -290,7 +290,7 @@ QList<ActionsModel::Action> actions{
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
Q_EMIT room->showMessage(NeoChatRoom::Info, i18nc("Joining room <roomname>.", "Joining room %1.", text));
|
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();
|
return QString();
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
|||||||
fetchEmojis();
|
fetchEmojis();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
CustomEmojiModel::fetchEmojis();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ void ImagePacksModel::reloadImages()
|
|||||||
}
|
}
|
||||||
auto packs = rooms[roomId].toObject();
|
auto packs = rooms[roomId].toObject();
|
||||||
const auto &stickerRoom = m_room->connection()->room(roomId);
|
const auto &stickerRoom = m_room->connection()->room(roomId);
|
||||||
|
if (!stickerRoom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const auto &packKey : packs.keys()) {
|
for (const auto &packKey : packs.keys()) {
|
||||||
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
||||||
const auto packContent = pack->content();
|
const auto packContent = pack->content();
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ LocationsModel::LocationsModel(QObject *parent)
|
|||||||
});
|
});
|
||||||
|
|
||||||
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
|
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)
|
void LocationsModel::addLocation(const RoomMessageEvent *event)
|
||||||
@@ -135,4 +131,12 @@ QRectF LocationsModel::boundingBox() const
|
|||||||
return bbox;
|
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"
|
#include "moc_locationsmodel.cpp"
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ Q_SIGNALS:
|
|||||||
void roomChanged();
|
void roomChanged();
|
||||||
void boundingBoxChanged();
|
void boundingBoxChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPointer<NeoChatRoom> m_room;
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
MessageEventModel::MessageEventModel(QObject *parent)
|
MessageEventModel::MessageEventModel(QObject *parent)
|
||||||
: QAbstractListModel(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
|
NeoChatRoom *MessageEventModel::room() const
|
||||||
@@ -392,13 +389,7 @@ int MessageEventModel::rowCount(const QModelIndex &parent) const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto firstIt = m_currentRoom->messageEvents().crbegin();
|
return int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0);
|
||||||
if (firstIt != m_currentRoom->messageEvents().crend()) {
|
|
||||||
const auto &firstEvt = **firstIt;
|
|
||||||
return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0);
|
|
||||||
} else {
|
|
||||||
return m_currentRoom->timelineSize();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MessageEventModel::canFetchMore(const QModelIndex &parent) const
|
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();
|
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 {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -440,6 +431,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
const KFormat format;
|
const KFormat format;
|
||||||
return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat);
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -459,7 +459,6 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>")
|
||||||
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventHandler.getRichBody();
|
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"
|
#include "moc_messageeventmodel.cpp"
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
int refreshEvent(const QString &eventId);
|
int refreshEvent(const QString &eventId);
|
||||||
void refreshRow(int row);
|
void refreshRow(int row);
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
#include "enums/delegatetype.h"
|
#include "enums/delegatetype.h"
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
|
#include "timelinemodel.h"
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
MessageFilterModel::MessageFilterModel(QObject *parent, MessageEventModel *sourceMessageModel)
|
MessageFilterModel::MessageFilterModel(QObject *parent, TimelineModel *sourceModel)
|
||||||
: QSortFilterProxyModel(parent)
|
: QSortFilterProxyModel(parent)
|
||||||
{
|
{
|
||||||
Q_ASSERT(sourceMessageModel);
|
Q_ASSERT(sourceModel);
|
||||||
setSourceModel(sourceMessageModel);
|
setSourceModel(sourceModel);
|
||||||
|
|
||||||
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
connect(NeoChatConfig::self(), &NeoChatConfig::ShowStateEventChanged, this, [this] {
|
||||||
invalidateFilter();
|
invalidateFilter();
|
||||||
@@ -117,35 +118,37 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
|
|||||||
chunks += QString();
|
chunks += QString();
|
||||||
int count = 1;
|
int count = 1;
|
||||||
auto part = parts.takeFirst();
|
auto part = parts.takeFirst();
|
||||||
chunks.last() += part;
|
|
||||||
while (!parts.isEmpty() && parts.first() == part) {
|
while (!parts.isEmpty() && parts.first() == part) {
|
||||||
parts.removeFirst();
|
parts.removeFirst();
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (count > 1 && uniqueAuthors.length() == 1) {
|
chunks.last() += i18ncp("%1: What's being done; %2: How often it is done.", " %1", " %1 %2 times", part, count);
|
||||||
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
chunks.removeDuplicates();
|
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.
|
// 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())
|
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> ")
|
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
|
||||||
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
|
.arg(uniqueAuthors[0].toMap()[QStringLiteral("id")].toString(),
|
||||||
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
|
uniqueAuthors[0].toMap()[QStringLiteral("color")].toString(),
|
||||||
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
|
uniqueAuthors[0].toMap()[QStringLiteral("displayName")].toString().toHtmlEscaped());
|
||||||
text += userText;
|
|
||||||
text += chunks.takeFirst();
|
|
||||||
|
|
||||||
|
QString chunksText;
|
||||||
|
chunksText += chunks.takeFirst();
|
||||||
if (chunks.size() > 0) {
|
if (chunks.size() > 0) {
|
||||||
while (chunks.size() > 1) {
|
while (chunks.size() > 1) {
|
||||||
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
chunksText += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
||||||
text += chunks.takeFirst();
|
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 ");
|
chunksText +=
|
||||||
text += chunks.takeFirst();
|
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 {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
|
#include "timelinemodel.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class MessageFilterModel
|
* @class MessageFilterModel
|
||||||
@@ -36,7 +37,7 @@ public:
|
|||||||
LastRole, // Keep this last
|
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.
|
* @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)
|
SearchModel::SearchModel(QObject *parent)
|
||||||
: QAbstractListModel(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
|
QString SearchModel::searchText() const
|
||||||
@@ -219,6 +216,14 @@ bool SearchModel::searching() const
|
|||||||
return m_searching;
|
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)
|
void SearchModel::setSearching(bool searching)
|
||||||
{
|
{
|
||||||
m_searching = searching;
|
m_searching = searching;
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ Q_SIGNALS:
|
|||||||
void roomChanged();
|
void roomChanged();
|
||||||
void searchingChanged();
|
void searchingChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setSearching(bool searching);
|
void setSearching(bool searching);
|
||||||
|
|
||||||
|
|||||||
@@ -16,41 +16,6 @@
|
|||||||
ServerListModel::ServerListModel(QObject *parent)
|
ServerListModel::ServerListModel(QObject *parent)
|
||||||
: QAbstractListModel(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
|
QVariant ServerListModel::data(const QModelIndex &index, int role) const
|
||||||
@@ -165,6 +130,53 @@ void ServerListModel::setConnection(NeoChatConnection *connection)
|
|||||||
}
|
}
|
||||||
m_connection = connection;
|
m_connection = connection;
|
||||||
Q_EMIT connectionChanged();
|
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"
|
#include "moc_serverlistmodel.cpp"
|
||||||
|
|||||||
@@ -111,4 +111,6 @@ private:
|
|||||||
QList<Server> m_servers;
|
QList<Server> m_servers;
|
||||||
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
|
QPointer<Quotient::QueryPublicRoomsJob> m_checkServerJob = nullptr;
|
||||||
NeoChatConnection *m_connection = nullptr;
|
NeoChatConnection *m_connection = nullptr;
|
||||||
|
|
||||||
|
void initialize();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
|||||||
SpaceTreeItem *parentItem = getItem(parent);
|
SpaceTreeItem *parentItem = getItem(parent);
|
||||||
|
|
||||||
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
|
if (children[0].roomId == m_space->id() || children[0].roomId == parentItem->id()) {
|
||||||
|
parentItem->setChildStates(std::move(children[0].childrenState));
|
||||||
children.erase(children.begin());
|
children.erase(children.begin());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +113,13 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
|||||||
m_replacedRooms += successorId;
|
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,
|
parentItem->insertChild(insertRow,
|
||||||
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
|
new SpaceTreeItem(dynamic_cast<NeoChatConnection *>(m_space->connection()),
|
||||||
parentItem,
|
parentItem,
|
||||||
@@ -123,14 +131,8 @@ void SpaceChildrenModel::insertChildren(std::vector<Quotient::GetSpaceHierarchyJ
|
|||||||
children[i].avatarUrl,
|
children[i].avatarUrl,
|
||||||
children[i].guestCanJoin,
|
children[i].guestCanJoin,
|
||||||
children[i].worldReadable,
|
children[i].worldReadable,
|
||||||
children[i].roomType == QLatin1String("m.space")));
|
children[i].roomType == QLatin1String("m.space"),
|
||||||
if (children[i].childrenState.size() > 0) {
|
std::move(children[i].childrenState)));
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
@@ -194,6 +196,9 @@ QVariant SpaceChildrenModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == IsSpaceRole) {
|
if (role == IsSpaceRole) {
|
||||||
return child->isSpace();
|
return child->isSpace();
|
||||||
}
|
}
|
||||||
|
if (role == IsSuggestedRole) {
|
||||||
|
return child->isSuggested();
|
||||||
|
}
|
||||||
if (role == CanAddChildrenRole) {
|
if (role == CanAddChildrenRole) {
|
||||||
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
if (const auto room = static_cast<NeoChatRoom *>(m_space->connection()->room(child->id()))) {
|
||||||
return room->canSendState(QLatin1String("m.space.child"));
|
return room->canSendState(QLatin1String("m.space.child"));
|
||||||
@@ -313,6 +318,7 @@ QHash<int, QByteArray> SpaceChildrenModel::roleNames() const
|
|||||||
roles[IsJoinedRole] = "isJoined";
|
roles[IsJoinedRole] = "isJoined";
|
||||||
roles[AliasRole] = "alias";
|
roles[AliasRole] = "alias";
|
||||||
roles[IsSpaceRole] = "isSpace";
|
roles[IsSpaceRole] = "isSpace";
|
||||||
|
roles[IsSuggestedRole] = "isSuggested";
|
||||||
roles[CanAddChildrenRole] = "canAddChildren";
|
roles[CanAddChildrenRole] = "canAddChildren";
|
||||||
roles[ParentDisplayNameRole] = "parentDisplayName";
|
roles[ParentDisplayNameRole] = "parentDisplayName";
|
||||||
roles[CanSetParentRole] = "canSetParent";
|
roles[CanSetParentRole] = "canSetParent";
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public:
|
|||||||
WorldReadableRole,
|
WorldReadableRole,
|
||||||
IsJoinedRole,
|
IsJoinedRole,
|
||||||
IsSpaceRole,
|
IsSpaceRole,
|
||||||
|
IsSuggestedRole,
|
||||||
CanAddChildrenRole,
|
CanAddChildrenRole,
|
||||||
ParentDisplayNameRole,
|
ParentDisplayNameRole,
|
||||||
CanSetParentRole,
|
CanSetParentRole,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
|
|||||||
const QUrl &avatarUrl,
|
const QUrl &avatarUrl,
|
||||||
bool allowGuests,
|
bool allowGuests,
|
||||||
bool worldReadable,
|
bool worldReadable,
|
||||||
bool isSpace)
|
bool isSpace,
|
||||||
|
Quotient::StateEvents childStates)
|
||||||
: m_connection(connection)
|
: m_connection(connection)
|
||||||
, m_parentItem(parent)
|
, m_parentItem(parent)
|
||||||
, m_id(id)
|
, m_id(id)
|
||||||
@@ -27,6 +28,7 @@ SpaceTreeItem::SpaceTreeItem(NeoChatConnection *connection,
|
|||||||
, m_allowGuests(allowGuests)
|
, m_allowGuests(allowGuests)
|
||||||
, m_worldReadable(worldReadable)
|
, m_worldReadable(worldReadable)
|
||||||
, m_isSpace(isSpace)
|
, m_isSpace(isSpace)
|
||||||
|
, m_childStates(std::move(childStates))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ int SpaceTreeItem::row() const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
SpaceTreeItem *SpaceTreeItem::parentItem()
|
SpaceTreeItem *SpaceTreeItem::parentItem() const
|
||||||
{
|
{
|
||||||
return m_parentItem;
|
return m_parentItem;
|
||||||
}
|
}
|
||||||
@@ -138,3 +140,34 @@ bool SpaceTreeItem::isSpace() const
|
|||||||
{
|
{
|
||||||
return m_isSpace;
|
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
|
// 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/csapi/space_hierarchy.h>
|
||||||
|
#include <Quotient/events/stateevent.h>
|
||||||
|
|
||||||
class NeoChatConnection;
|
class NeoChatConnection;
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ public:
|
|||||||
const QUrl &avatarUrl = {},
|
const QUrl &avatarUrl = {},
|
||||||
bool allowGuests = {},
|
bool allowGuests = {},
|
||||||
bool worldReadable = {},
|
bool worldReadable = {},
|
||||||
bool isSpace = {});
|
bool isSpace = {},
|
||||||
|
Quotient::StateEvents childStates = {});
|
||||||
~SpaceTreeItem();
|
~SpaceTreeItem();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +62,7 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief Return this item's parent.
|
* @brief Return this item's parent.
|
||||||
*/
|
*/
|
||||||
SpaceTreeItem *parentItem();
|
SpaceTreeItem *parentItem() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return the row number for this child relative to the parent.
|
* @brief Return the row number for this child relative to the parent.
|
||||||
@@ -123,6 +125,23 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isSpace() const;
|
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:
|
private:
|
||||||
NeoChatConnection *m_connection;
|
NeoChatConnection *m_connection;
|
||||||
QList<SpaceTreeItem *> m_children;
|
QList<SpaceTreeItem *> m_children;
|
||||||
@@ -137,4 +156,5 @@ private:
|
|||||||
bool m_allowGuests;
|
bool m_allowGuests;
|
||||||
bool m_worldReadable;
|
bool m_worldReadable;
|
||||||
bool m_isSpace;
|
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)
|
: QAbstractListModel(parent)
|
||||||
, m_currentRoom(nullptr)
|
, m_currentRoom(nullptr)
|
||||||
{
|
{
|
||||||
connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this]() {
|
|
||||||
refreshAllUsers();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserListModel::setRoom(NeoChatRoom *room)
|
void UserListModel::setRoom(NeoChatRoom *room)
|
||||||
@@ -121,6 +118,14 @@ int UserListModel::rowCount(const QModelIndex &parent) const
|
|||||||
return m_users.count();
|
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)
|
void UserListModel::userAdded(Quotient::User *user)
|
||||||
{
|
{
|
||||||
auto pos = findUserPos(user);
|
auto pos = findUserPos(user);
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ Q_SIGNALS:
|
|||||||
void roomChanged();
|
void roomChanged();
|
||||||
void usersRefreshed();
|
void usersRefreshed();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *event) override;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void userAdded(Quotient::User *user);
|
void userAdded(Quotient::User *user);
|
||||||
void userRemoved(Quotient::User *user);
|
void userRemoved(Quotient::User *user);
|
||||||
|
|||||||
@@ -82,22 +82,22 @@
|
|||||||
</entry>
|
</entry>
|
||||||
<entry name="ShowRename" type="bool">
|
<entry name="ShowRename" type="bool">
|
||||||
<label>Show rename events in the timeline</label>
|
<label>Show rename events in the timeline</label>
|
||||||
<default>true</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="ShowAvatarUpdate" type="bool">
|
<entry name="ShowAvatarUpdate" type="bool">
|
||||||
<label>Show avatar update events in the timeline</label>
|
<label>Show avatar update events in the timeline</label>
|
||||||
<default>true</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="ShowDeletedMessages" type="bool">
|
<entry name="ShowDeletedMessages" type="bool">
|
||||||
<label>Show deleted messages in the timeline</label>
|
<label>Show deleted messages in the timeline</label>
|
||||||
<default>true</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="ShowLinkPreview" type="bool">
|
<entry name="ShowLinkPreview" type="bool">
|
||||||
<label>Show preview of the links in the chat messages</label>
|
<label>Show preview of the links in the chat messages</label>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="SystemTray" type="bool">
|
<entry name="SystemTray" type="bool">
|
||||||
<label>Close NeoChat to system tray</label>
|
<label>Close NeoChat to system tray</label>
|
||||||
<default>true</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="MinimizeToSystemTrayOnStartup" type="bool">
|
<entry name="MinimizeToSystemTrayOnStartup" type="bool">
|
||||||
<label>Minimize to system tray on startup</label>
|
<label>Minimize to system tray on startup</label>
|
||||||
|
|||||||
@@ -21,7 +21,14 @@
|
|||||||
#include <Quotient/settings.h>
|
#include <Quotient/settings.h>
|
||||||
#include <Quotient/user.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 Quotient;
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
NeoChatConnection::NeoChatConnection(QObject *parent)
|
NeoChatConnection::NeoChatConnection(QObject *parent)
|
||||||
: Connection(parent)
|
: Connection(parent)
|
||||||
@@ -31,6 +38,12 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
|
|||||||
Q_EMIT labelChanged();
|
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)
|
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()));
|
Q_EMIT Controller::instance().errorOccured(i18n("Room creation failed: %1", job->errorString()));
|
||||||
});
|
});
|
||||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
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()));
|
Q_EMIT Controller::instance().errorOccured(i18n("Space creation failed: %1", job->errorString()));
|
||||||
});
|
});
|
||||||
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
connectSingleShot(this, &Connection::newRoom, this, [](Room *room) {
|
||||||
@@ -235,6 +248,39 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
|
|||||||
requestDirectChat(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
|
QString NeoChatConnection::deviceKey() const
|
||||||
{
|
{
|
||||||
return edKeyForUserDevice(userId(), deviceId());
|
return edKeyForUserDevice(userId(), deviceId());
|
||||||
@@ -252,4 +298,18 @@ QString NeoChatConnection::encryptionKey() const
|
|||||||
return query.value(0).toString();
|
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"
|
#include "moc_neochatconnection.cpp"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <QCoroTask>
|
||||||
#include <Quotient/connection.h>
|
#include <Quotient/connection.h>
|
||||||
|
|
||||||
class NeoChatConnection : public Quotient::Connection
|
class NeoChatConnection : public Quotient::Connection
|
||||||
@@ -26,6 +27,11 @@ class NeoChatConnection : public Quotient::Connection
|
|||||||
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
|
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
|
||||||
Q_PROPERTY(QString encryptionKey READ encryptionKey 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:
|
public:
|
||||||
NeoChatConnection(QObject *parent = nullptr);
|
NeoChatConnection(QObject *parent = nullptr);
|
||||||
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
|
NeoChatConnection(const QUrl &server, QObject *parent = nullptr);
|
||||||
@@ -70,9 +76,20 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
|
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 deviceKey() const;
|
||||||
QString encryptionKey() const;
|
QString encryptionKey() const;
|
||||||
|
|
||||||
|
bool isOnline() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void labelChanged();
|
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);
|
body = eventHandler.getPlainBody(stripNewlines);
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
return eventHandler.singleLineAuthorDisplayname() + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -1304,7 +1304,7 @@ bool NeoChatRoom::isSpace()
|
|||||||
return creationEvent->roomType() == RoomType::Space;
|
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()) {
|
if (!isSpace()) {
|
||||||
return;
|
return;
|
||||||
@@ -1312,7 +1312,7 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
|
|||||||
if (!canSendEvent("m.space.child"_ls)) {
|
if (!canSendEvent("m.space.child"_ls)) {
|
||||||
return;
|
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 (setChildParent) {
|
||||||
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
|
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
|
PushNotificationState::State NeoChatRoom::pushNotificationState() const
|
||||||
{
|
{
|
||||||
return m_currentPushNotificationState;
|
return m_currentPushNotificationState;
|
||||||
|
|||||||
@@ -562,7 +562,7 @@ public:
|
|||||||
* Will fail if the user doesn't have the required privileges or this room is
|
* Will fail if the user doesn't have the required privileges or this room is
|
||||||
* not a space.
|
* 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.
|
* @brief Remove the given room as a child.
|
||||||
@@ -572,6 +572,19 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void removeChild(const QString &childId, bool unsetChildParent = false);
|
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;
|
bool isInvite() const;
|
||||||
|
|
||||||
Q_INVOKABLE void clearInvitationNotification();
|
Q_INVOKABLE void clearInvitationNotification();
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
#include <Quotient/csapi/pushrules.h>
|
#include <Quotient/csapi/pushrules.h>
|
||||||
#include <Quotient/user.h>
|
#include <Quotient/user.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_KIO
|
||||||
|
#include <KIO/ApplicationLauncherJob>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "neochatconnection.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)
|
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
|
||||||
{
|
{
|
||||||
// Handle avatars that are lopsided in one dimension
|
// Handle avatars that are lopsided in one dimension
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
void clearInvitationNotification(const QString &roomId);
|
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.
|
* @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
|
id: openFileDialog
|
||||||
|
|
||||||
OpenFileDialog {
|
OpenFileDialog {
|
||||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||||
parentWindow: root.Window.window
|
parentWindow: root.Window.window
|
||||||
|
|
||||||
onAccepted: destroy()
|
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 {
|
RowLayout {
|
||||||
Layout.maximumWidth: root.maxContentWidth
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
visible: root.showAuthor
|
visible: root.showAuthor
|
||||||
QQC2.Label {
|
QQC2.AbstractButton {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: root.author.displayName
|
contentItem: QQC2.Label {
|
||||||
color: root.author.color
|
text: root.author.displayName
|
||||||
textFormat: Text.PlainText
|
color: root.author.color
|
||||||
font.weight: Font.Bold
|
textFormat: Text.PlainText
|
||||||
elide: Text.ElideRight
|
font.weight: Font.Bold
|
||||||
|
elide: Text.ElideRight
|
||||||
TapHandler {
|
|
||||||
onTapped: RoomManager.visitUser(root.author.object, "mention")
|
|
||||||
}
|
|
||||||
HoverHandler {
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
}
|
}
|
||||||
|
Accessible.name: contentItem.text
|
||||||
|
onClicked: RoomManager.resolveResource(root.author.id, "mention")
|
||||||
}
|
}
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
text: root.timeString
|
text: root.timeString
|
||||||
|
|||||||
@@ -2,35 +2,25 @@
|
|||||||
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
// SPDX-FileCopyrightText: 2020 Noah Davis <noahadvs@gmail.com>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtCore
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Window
|
import QtQuick.Layouts
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
import org.kde.neochat.config
|
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
|
* This is designed to go to the bottom of the timeline and provides all the functionality
|
||||||
* the core functionality of displaying the current message composition before sending.
|
* required for the user to send messages to the room.
|
||||||
*
|
*
|
||||||
* This includes support for the following message types:
|
* In addition when replying this component supports showing the message that is being
|
||||||
* - text
|
|
||||||
* - media (video, image, file)
|
|
||||||
* - emojis/stickers
|
|
||||||
* - location
|
|
||||||
*
|
|
||||||
* In addition, when replying, this component supports showing the message that is being
|
|
||||||
* replied to.
|
* replied to.
|
||||||
*
|
*
|
||||||
* @note There is no edit functionality here this, is handled inline by the timeline
|
* @sa ChatBar
|
||||||
* text delegate.
|
|
||||||
*
|
|
||||||
* @sa ChatBox
|
|
||||||
*/
|
*/
|
||||||
QQC2.Control {
|
QQC2.Control {
|
||||||
id: root
|
id: root
|
||||||
@@ -39,17 +29,13 @@ QQC2.Control {
|
|||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
required property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
|
|
||||||
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
|
onActiveFocusChanged: textField.forceActiveFocus()
|
||||||
|
|
||||||
onCurrentRoomChanged: _private.chatBarCache = currentRoom.mainCache
|
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.
|
* @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
|
* Each of these will be visualised in the ChatBar so new actions can be added
|
||||||
* by appending to this list.
|
* by appending to this list.
|
||||||
*/
|
*/
|
||||||
property list<Kirigami.Action> actions : [
|
property list<Kirigami.Action> actions: [
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: attachmentAction
|
id: attachmentAction
|
||||||
|
|
||||||
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||||
|
|
||||||
// Matrix does not allow sending attachments in replies
|
// 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"
|
icon.name: "mail-attachment"
|
||||||
text: i18n("Attach an image or file")
|
text: i18n("Attach an image or file")
|
||||||
displayHint: Kirigami.DisplayHint.IconOnly
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (Clipboard.hasImage) {
|
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(applicationWindow().overlay)
|
||||||
attachDialog.open()
|
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path)
|
||||||
} else {
|
dialog.open()
|
||||||
var fileDialog = openFileDialog.createObject(QQC2.ApplicationWindow.overlay)
|
|
||||||
fileDialog.chosen.connect((path) => {
|
|
||||||
if (!path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_private.chatBarCache.attachmentPath = path;
|
|
||||||
})
|
|
||||||
fileDialog.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -105,10 +82,10 @@ QQC2.Control {
|
|||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (emojiDialog.item.visible) {
|
if (emojiDialog.visible) {
|
||||||
emojiDialog.item.close()
|
emojiDialog.close()
|
||||||
} else {
|
} else {
|
||||||
emojiDialog.item.open()
|
emojiDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -121,7 +98,7 @@ QQC2.Control {
|
|||||||
displayHint: QQC2.AbstractButton.IconOnly
|
displayHint: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
locationChooserComponent.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
|
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {room: root.currentRoom}).open()
|
||||||
}
|
}
|
||||||
tooltip: text
|
tooltip: text
|
||||||
},
|
},
|
||||||
@@ -136,7 +113,7 @@ QQC2.Control {
|
|||||||
checkable: true
|
checkable: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.postMessage()
|
_private.postMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip: text
|
tooltip: text
|
||||||
@@ -148,294 +125,308 @@ QQC2.Control {
|
|||||||
*/
|
*/
|
||||||
signal messageSent()
|
signal messageSent()
|
||||||
|
|
||||||
leftPadding: 0
|
spacing: 0
|
||||||
rightPadding: 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
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
|
|
||||||
contentItem: QQC2.ScrollView {
|
contentItem: ColumnLayout {
|
||||||
id: chatBarScrollView
|
spacing: 0
|
||||||
|
Item { // Required to adjust for the top separator
|
||||||
property var textFieldHeight: textField.height
|
Layout.preferredHeight: 1
|
||||||
|
Layout.fillWidth: true
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loader {
|
||||||
|
id: paneLoader
|
||||||
|
|
||||||
QQC2.TextArea {
|
Layout.fillWidth: true
|
||||||
id: textField
|
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)
|
active: visible
|
||||||
topPadding: Kirigami.Units.largeSpacing + (paneLoader.visible ? paneLoader.height : 0)
|
visible: root.currentRoom.mainCache.replyId.length > 0 || root.currentRoom.mainCache.attachmentPath.length > 0
|
||||||
bottomPadding: Kirigami.Units.largeSpacing
|
sourceComponent: root.currentRoom.mainCache.replyId.length > 0 ? replyPane : attachmentPane
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
RowLayout {
|
||||||
|
QQC2.ScrollView {
|
||||||
|
id: chatBarScrollView
|
||||||
|
|
||||||
/**
|
Layout.fillWidth: true
|
||||||
* Because of the paneLoader we have to manage the scroll
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
||||||
* position manually or it doesn't keep the cursor visible properly all the time.
|
|
||||||
*/
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
function ensureVisible(r) {
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
// Find the child that is the Flickable created by ScrollView.
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
|
||||||
let flickable = undefined;
|
|
||||||
for (var index in children) {
|
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
||||||
if (children[index] instanceof Flickable) {
|
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||||
flickable = children[index];
|
|
||||||
|
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) {
|
Repeater {
|
||||||
if (flickable.contentX >= r.x) {
|
model: root.actions
|
||||||
flickable.contentX = r.x;
|
delegate: QQC2.ToolButton {
|
||||||
} else if (flickable.contentX + width <= r.x + r.width) {
|
Layout.alignment: Qt.AlignVCenter
|
||||||
flickable.contentX = r.x + r.width - width;
|
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||||
} if (flickable.contentY >= r.y) {
|
onClicked: modelData.trigger()
|
||||||
flickable.contentY = r.y;
|
|
||||||
} else if (flickable.contentY + height <= r.y + r.height) {
|
QQC2.ToolTip.visible: hovered
|
||||||
flickable.contentY = r.y + r.height - height + textField.bottomPadding;
|
QQC2.ToolTip.text: modelData.tooltip
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
|
||||||
|
PieProgressBar {
|
||||||
|
visible: modelData.isBusy
|
||||||
|
progress: root.currentRoom.fileUploadingProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ToolButton {
|
DelegateSizeHelper {
|
||||||
id: cancelButton
|
id: chatBarSizeHelper
|
||||||
anchors.top: parent.top
|
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||||
anchors.right: parent.right
|
endBreakpoint: Kirigami.Units.gridUnit * 66
|
||||||
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)
|
startPercentWidth: 100
|
||||||
|
endPercentWidth: Config.compactLayout ? 100 : 85
|
||||||
|
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
|
||||||
|
|
||||||
visible: _private.chatBarCache.isReplying
|
parentWidth: root.width
|
||||||
display: QQC2.AbstractButton.IconOnly
|
}
|
||||||
action: Kirigami.Action {
|
|
||||||
text: i18nc("@action:button", "Cancel reply")
|
Component {
|
||||||
icon.name: "dialog-close"
|
id: replyPane
|
||||||
onTriggered: {
|
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.replyId = "";
|
||||||
_private.chatBarCache.attachmentPath = "";
|
_private.chatBarCache.attachmentPath = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: attachmentPane
|
||||||
|
AttachmentPane {
|
||||||
|
attachmentPath: _private.chatBarCache.attachmentPath
|
||||||
|
|
||||||
|
onAttachmentCancelled: {
|
||||||
|
_private.chatBarCache.attachmentPath = "";
|
||||||
root.forceActiveFocus()
|
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 {
|
QtObject {
|
||||||
model: root.actions
|
id: _private
|
||||||
delegate: QQC2.ToolButton {
|
property ChatBarCache chatBarCache
|
||||||
Layout.alignment: Qt.AlignVCenter
|
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
|
||||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
|
||||||
onClicked: modelData.trigger()
|
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
function postMessage() {
|
||||||
QQC2.ToolTip.text: modelData.tooltip
|
root.actionsHandler.handleMessageEvent(_private.chatBarCache);
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
repeatTimer.stop()
|
||||||
|
root.currentRoom.markAllMessagesAsRead();
|
||||||
|
textField.clear();
|
||||||
|
_private.chatBarCache.replyId = "";
|
||||||
|
messageSent()
|
||||||
|
}
|
||||||
|
|
||||||
PieProgressBar {
|
function formatText(format, selectionStart, selectionEnd) {
|
||||||
visible: modelData.isBusy
|
let index = textField.cursorPosition;
|
||||||
progress: root.currentRoom.fileUploadingProgress
|
|
||||||
}
|
/*
|
||||||
|
* 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 {
|
function pasteImage() {
|
||||||
id: emojiDialog
|
let localPath = Clipboard.saveImage();
|
||||||
active: !Kirigami.Settings.isMobile
|
if (localPath.length === 0) {
|
||||||
sourceComponent: EmojiDialog {
|
return false;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
_private.chatBarCache.attachmentPath = localPath;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,21 +443,59 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateSizeHelper {
|
Component {
|
||||||
id: chatBarSizeHelper
|
id: openFileDialog
|
||||||
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
|
|
||||||
|
|
||||||
parentWidth: root.width
|
OpenFileDialog {
|
||||||
|
parentWindow: Window.window
|
||||||
|
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceActiveFocus() {
|
Component {
|
||||||
textField.forceActiveFocus();
|
id: attachDialog
|
||||||
// set the cursor to the end of the text
|
|
||||||
textField.cursorPosition = textField.length;
|
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) {
|
function insertText(text) {
|
||||||
@@ -475,139 +504,4 @@ QQC2.Control {
|
|||||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
|
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition)
|
||||||
textField.cursorPosition = initialCursorPosition + text.length
|
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