Compare commits
136 Commits
v25.12.2
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
508c2bee46 | ||
|
|
e1bbbfe4fd | ||
|
|
7a2211f8e0 | ||
|
|
4155e9116a | ||
|
|
a989ef42b2 | ||
|
|
2babf44b28 | ||
|
|
89e42dbc53 | ||
|
|
ba57570dbf | ||
|
|
1a500a087b | ||
|
|
e54955ec0c | ||
|
|
8608b3b62e | ||
|
|
fea0cfbf4e | ||
|
|
5b6e5a25e5 | ||
|
|
58b47b8711 | ||
|
|
b45967508c | ||
|
|
2ec1fa92fa | ||
|
|
7602a56594 | ||
|
|
e8da02be7d | ||
|
|
71c84be4b4 | ||
|
|
b684fb451d | ||
|
|
3a416990ca | ||
|
|
9b7d16973d | ||
|
|
dd59e63574 | ||
|
|
7f7e48dfd4 | ||
|
|
7119132e4b | ||
|
|
6001cc6d4f | ||
|
|
989f1ad020 | ||
|
|
a119563ba7 | ||
|
|
5e473aae0a | ||
|
|
aa0272a02d | ||
|
|
fc6f345036 | ||
|
|
4fa011c266 | ||
|
|
44ebcc6b9a | ||
|
|
32cc5d4e48 | ||
|
|
bbc5a92f62 | ||
|
|
4656bf4ee7 | ||
|
|
5f7967363f | ||
|
|
a02a04d966 | ||
|
|
dffec2f0d5 | ||
|
|
68b00b9fc5 | ||
|
|
aa823c7629 | ||
|
|
73b82b69d8 | ||
|
|
bd0588ca99 | ||
|
|
e5b7601dac | ||
|
|
f4ac3346f5 | ||
|
|
58ea229b67 | ||
|
|
fd44ff972a | ||
|
|
c5eb63da15 | ||
|
|
79acceb29a | ||
|
|
8022a7aae5 | ||
|
|
d26862dfb6 | ||
|
|
c25c095c1b | ||
|
|
e09e4fb7dc | ||
|
|
657c8a0dcd | ||
|
|
82f54b4f2c | ||
|
|
9e7cd0eb09 | ||
|
|
9bdb9b6a7c | ||
|
|
fa4a93140d | ||
|
|
a41966ecd7 | ||
|
|
edcf81f0da | ||
|
|
0a98c732e0 | ||
|
|
d734e114a6 | ||
|
|
946e505a24 | ||
|
|
092980c0ff | ||
|
|
6fcb1cc1e3 | ||
|
|
c572480d10 | ||
|
|
531df7a3b2 | ||
|
|
706f1f7836 | ||
|
|
45c5806c5a | ||
|
|
abf37c90cb | ||
|
|
5d9508b165 | ||
|
|
f5a652f7a1 | ||
|
|
f165cb1f10 | ||
|
|
11132c3e89 | ||
|
|
089ab2009a | ||
|
|
6ebedc5dfc | ||
|
|
e5b2fb7316 | ||
|
|
5da3aff607 | ||
|
|
3fa46d52fe | ||
|
|
ece9811d82 | ||
|
|
b543b1c2f9 | ||
|
|
5065bccb64 | ||
|
|
10c3125668 | ||
|
|
73019cfea2 | ||
|
|
4bc6a88acf | ||
|
|
56a49cfc50 | ||
|
|
c50380b448 | ||
|
|
673a8c8bc8 | ||
|
|
c298c348a4 | ||
|
|
2fcd535aeb | ||
|
|
8f5e68de5d | ||
|
|
5fe36a9057 | ||
|
|
8d8c6444e0 | ||
|
|
03d5955c8d | ||
|
|
197d7ce8e8 | ||
|
|
efbf6d96d4 | ||
|
|
b8cd3c69c2 | ||
|
|
1da24191f0 | ||
|
|
2ecc567792 | ||
|
|
66e1fe067d | ||
|
|
d08f014def | ||
|
|
63941d6685 | ||
|
|
38523c97c5 | ||
|
|
65c6f4c1d3 | ||
|
|
0cb3fd32f4 | ||
|
|
b02cb0157e | ||
|
|
ca163fc169 | ||
|
|
5687eb33e1 | ||
|
|
fc795e50b3 | ||
|
|
c4adfe7939 | ||
|
|
ce96232bbd | ||
|
|
95653cf3f7 | ||
|
|
0c11cd331f | ||
|
|
3c8ca0d421 | ||
|
|
52f71d5c55 | ||
|
|
00f7dd5175 | ||
|
|
52da8415e9 | ||
|
|
953e4cd792 | ||
|
|
ee4b1578b1 | ||
|
|
ae24424a32 | ||
|
|
51be282824 | ||
|
|
c539dfc352 | ||
|
|
fe734206df | ||
|
|
79a49165f1 | ||
|
|
ada5eef088 | ||
|
|
9cc5778126 | ||
|
|
b129805f94 | ||
|
|
887865c0aa | ||
|
|
1336194cf4 | ||
|
|
5a7ae3563e | ||
|
|
7c56e24fdc | ||
|
|
e6b9abeca3 | ||
|
|
1b45784a88 | ||
|
|
a12b0d5282 | ||
|
|
a6d75f2ff5 | ||
|
|
b8f7f33b9a |
@@ -2,7 +2,7 @@
|
|||||||
"id": "org.kde.neochat",
|
"id": "org.kde.neochat",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"runtime": "org.kde.Platform",
|
"runtime": "org.kde.Platform",
|
||||||
"runtime-version": "6.9",
|
"runtime-version": "6.10",
|
||||||
"sdk": "org.kde.Sdk",
|
"sdk": "org.kde.Sdk",
|
||||||
"command": "neochat",
|
"command": "neochat",
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -31,19 +31,6 @@
|
|||||||
"/share/ndk-modules"
|
"/share/ndk-modules"
|
||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
|
||||||
"name": "kirigamiaddons",
|
|
||||||
"config-opts": [
|
|
||||||
"-DBUILD_TESTING=OFF"
|
|
||||||
],
|
|
||||||
"buildsystem": "cmake-ninja",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://invent.kde.org/libraries/kirigami-addons.git"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "opencv",
|
"name": "opencv",
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
@@ -78,6 +65,7 @@
|
|||||||
"name": "olm",
|
"name": "olm",
|
||||||
"buildsystem": "cmake-ninja",
|
"buildsystem": "cmake-ninja",
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
|
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
|
||||||
"-DOLM_TESTS=OFF"
|
"-DOLM_TESTS=OFF"
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
@@ -184,8 +172,8 @@
|
|||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz",
|
"url": "https://download.kde.org/stable/release-service/25.08.3/src/kunifiedpush-25.08.3.tar.xz",
|
||||||
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518",
|
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a",
|
||||||
"x-checker-data": {
|
"x-checker-data": {
|
||||||
"type": "anitya",
|
"type": "anitya",
|
||||||
"project-id": 8763,
|
"project-id": 8763,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
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 "25")
|
set(RELEASE_SERVICE_VERSION_MAJOR "26")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "12")
|
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "2")
|
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
||||||
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})
|
||||||
@@ -46,10 +46,6 @@ if (NOT ANDROID)
|
|||||||
include(KDEClangFormat)
|
include(KDEClangFormat)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NEOCHAT_FLATPAK)
|
|
||||||
include(cmake/Flatpak.cmake)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
|
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
|
||||||
|
|
||||||
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
|
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
|
|||||||
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
|
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
|
||||||
model->setRoom(room);
|
model->setRoom(room);
|
||||||
|
|
||||||
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
|
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TimelineMessageModelTest::cleanup()
|
void TimelineMessageModelTest::cleanup()
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
|
||||||
# SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
|
||||||
|
|
||||||
# Include FontConfig config which uses the Emoji One font from the
|
|
||||||
# KDE Flatpak SDK.
|
|
||||||
install(
|
|
||||||
FILES
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
|
|
||||||
DESTINATION
|
|
||||||
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
|
||||||
<fontconfig>
|
|
||||||
<alias>
|
|
||||||
<family>serif</family>
|
|
||||||
<prefer>
|
|
||||||
<family>Noto Color Emoji</family>
|
|
||||||
</prefer>
|
|
||||||
</alias>
|
|
||||||
<alias>
|
|
||||||
<family>sans-serif</family>
|
|
||||||
<prefer>
|
|
||||||
<family>Noto Color Emoji</family>
|
|
||||||
</prefer>
|
|
||||||
</alias>
|
|
||||||
<alias>
|
|
||||||
<family>monospace</family>
|
|
||||||
<prefer>
|
|
||||||
<family>Noto Color Emoji</family>
|
|
||||||
</prefer>
|
|
||||||
</alias>
|
|
||||||
</fontconfig>
|
|
||||||
|
|
||||||
@@ -193,7 +193,6 @@
|
|||||||
<li xml:lang="ar">التصويت - MSC3381</li>
|
<li xml:lang="ar">التصويت - MSC3381</li>
|
||||||
<li xml:lang="ca">Votacions - MSC3381</li>
|
<li xml:lang="ca">Votacions - MSC3381</li>
|
||||||
<li xml:lang="ca-valencia">Votacions - MSC3381</li>
|
<li xml:lang="ca-valencia">Votacions - MSC3381</li>
|
||||||
<li xml:lang="de">Umfragen – MSC3381</li>
|
|
||||||
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
|
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
|
||||||
<li xml:lang="en-GB">Polls - MSC3381</li>
|
<li xml:lang="en-GB">Polls - MSC3381</li>
|
||||||
<li xml:lang="eo">Enketoj - MSC3381</li>
|
<li xml:lang="eo">Enketoj - MSC3381</li>
|
||||||
@@ -228,7 +227,6 @@
|
|||||||
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
|
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
|
||||||
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
||||||
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
|
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
|
||||||
<li xml:lang="de">Sticker-Pakete – MSC2545</li>
|
|
||||||
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
|
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
|
||||||
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
|
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
|
||||||
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
|
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
|
||||||
@@ -322,7 +320,6 @@
|
|||||||
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
||||||
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
||||||
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
||||||
<value key="KDE::supporters">Anonymous donor, Akseli</value>
|
|
||||||
</custom>
|
</custom>
|
||||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
@@ -490,7 +487,6 @@
|
|||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="25.12.2" date="2026-02-05"/>
|
|
||||||
<release version="25.12.1" date="2026-01-08"/>
|
<release version="25.12.1" date="2026-01-08"/>
|
||||||
<release version="25.12.0" date="2025-12-11"/>
|
<release version="25.12.0" date="2025-12-11"/>
|
||||||
<release version="25.08.3" date="2025-11-06"/>
|
<release version="25.08.3" date="2025-11-06"/>
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ Comment[ia]=Conversation en ditecto sur Matrix
|
|||||||
Comment[it]= su Matrix
|
Comment[it]= su Matrix
|
||||||
Comment[ka]=ჩატი Matrix-ზე
|
Comment[ka]=ჩატი Matrix-ზე
|
||||||
Comment[ko]=Matrix에서 대화하기
|
Comment[ko]=Matrix에서 대화하기
|
||||||
|
Comment[lt]=Pokalbiai per Matrix
|
||||||
Comment[lv]=Tērzējiet „Matrix“ tīklā
|
Comment[lv]=Tērzējiet „Matrix“ tīklā
|
||||||
Comment[nl]=Chat op Matrix
|
Comment[nl]=Chat op Matrix
|
||||||
Comment[pl]=Rozmawiaj na Matriksie
|
Comment[pl]=Rozmawiaj na Matriksie
|
||||||
|
|||||||
1200
po/ar/neochat.po
1200
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1239
po/az/neochat.po
1239
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1298
po/ca/neochat.po
1298
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1137
po/cs/neochat.po
1137
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1149
po/da/neochat.po
1149
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1359
po/de/neochat.po
1359
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1295
po/el/neochat.po
1295
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1323
po/en_GB/neochat.po
1323
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1311
po/eo/neochat.po
1311
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
1062
po/es/neochat.po
1062
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1201
po/eu/neochat.po
1201
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1305
po/fi/neochat.po
1305
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1192
po/fr/neochat.po
1192
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
920
po/ga/neochat.po
920
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
1324
po/gl/neochat.po
1324
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
1172
po/he/neochat.po
1172
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
1303
po/hi/neochat.po
1303
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
1313
po/hu/neochat.po
1313
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1774
po/ia/neochat.po
1774
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1281
po/id/neochat.po
1281
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1222
po/ie/neochat.po
1222
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1241
po/it/neochat.po
1241
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
915
po/ja/neochat.po
915
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1181
po/ka/neochat.po
1181
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1305
po/ko/neochat.po
1305
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
1251
po/lt/neochat.po
1251
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1541
po/lv/neochat.po
1541
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
1197
po/nl/neochat.po
1197
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1160
po/nn/neochat.po
1160
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1229
po/pa/neochat.po
1229
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1212
po/pl/neochat.po
1212
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1275
po/pt/neochat.po
1275
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1153
po/pt_BR/neochat.po
1153
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1142
po/ro/neochat.po
1142
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
1344
po/ru/neochat.po
1344
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1309
po/sa/neochat.po
1309
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
1248
po/sk/neochat.po
1248
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1140
po/sl/neochat.po
1140
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1477
po/sv/neochat.po
1477
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1467
po/ta/neochat.po
1467
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1140
po/tok/neochat.po
1140
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||||||
>carl@carlschwan.eu</email
|
>carl@carlschwan.eu</email
|
||||||
></author>
|
></author>
|
||||||
<date
|
<date
|
||||||
>2022-11-01</date>
|
>2022‒11‒01</date>
|
||||||
<releaseinfo
|
<releaseinfo
|
||||||
>22.09</releaseinfo>
|
>22.09</releaseinfo>
|
||||||
<productname
|
<productname
|
||||||
@@ -111,9 +111,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||||||
><title
|
><title
|
||||||
>Telif Hakkı</title>
|
>Telif Hakkı</title>
|
||||||
<para
|
<para
|
||||||
>Telif hakkı © 2020-2022 Tobias Fella </para>
|
>Telif hakkı © 2020–2022 Tobias Fella </para>
|
||||||
<para
|
<para
|
||||||
>Telif hakkı © 2020-2022 Carl Schwan </para>
|
>Telif hakkı © 2020–2022 Carl Schwan </para>
|
||||||
<para
|
<para
|
||||||
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası <<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
|
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası <<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
|
||||||
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
|
>https://www.gnu.org/licenses/gpl-3.0.html</ulink
|
||||||
|
|||||||
1130
po/tr/neochat.po
1130
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1158
po/uk/neochat.po
1158
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1023
po/zh_CN/neochat.po
1023
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1171
po/zh_TW/neochat.po
1171
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
# SPDX-License-Identifier: BSD-2-Clause
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
add_library(neochat STATIC
|
qt_add_library(neochat STATIC
|
||||||
controller.cpp
|
controller.cpp
|
||||||
controller.h
|
controller.h
|
||||||
roommanager.cpp
|
roommanager.cpp
|
||||||
@@ -35,6 +35,8 @@ add_library(neochat STATIC
|
|||||||
models/commonroomsmodel.h
|
models/commonroomsmodel.h
|
||||||
texttospeechhelper.h
|
texttospeechhelper.h
|
||||||
texttospeechhelper.cpp
|
texttospeechhelper.cpp
|
||||||
|
models/limitermodel.cpp
|
||||||
|
models/limitermodel.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||||
@@ -104,6 +106,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/NewPollDialog.qml
|
qml/NewPollDialog.qml
|
||||||
qml/UserMenu.qml
|
qml/UserMenu.qml
|
||||||
qml/MeetingDialog.qml
|
qml/MeetingDialog.qml
|
||||||
|
qml/SeenByDialog.qml
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
QtCore
|
QtCore
|
||||||
QtQuick
|
QtQuick
|
||||||
@@ -140,10 +143,17 @@ if(WIN32)
|
|||||||
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
|
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(neochat-app
|
qt_add_executable(neochat-app
|
||||||
main.cpp
|
main.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
set_target_properties(neochat-app PROPERTIES
|
||||||
|
OUTPUT_NAME "neochat-app"
|
||||||
|
PREFIX "lib"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(TARGET Qt::WebView)
|
if(TARGET Qt::WebView)
|
||||||
target_link_libraries(neochat-app PUBLIC Qt::WebView)
|
target_link_libraries(neochat-app PUBLIC Qt::WebView)
|
||||||
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
|
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
|
||||||
@@ -184,7 +194,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||||
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
|
target_link_libraries(neochat PRIVATE neochatplugin Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
|
||||||
target_link_libraries(neochat PUBLIC
|
target_link_libraries(neochat PUBLIC
|
||||||
LibNeoChat
|
LibNeoChat
|
||||||
Timeline
|
Timeline
|
||||||
|
|||||||
@@ -246,7 +246,10 @@ void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatC
|
|||||||
if (newConnection) {
|
if (newConnection) {
|
||||||
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
|
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
|
||||||
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
|
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
|
||||||
|
|
||||||
|
// Refresh and update manually, in case we init too late for the badge count to actually change.
|
||||||
newConnection->refreshBadgeNotificationCount();
|
newConnection->refreshBadgeNotificationCount();
|
||||||
|
updateBadgeNotificationCount(newConnection->badgeNotificationCount());
|
||||||
}
|
}
|
||||||
Q_EMIT activeConnectionChanged(newConnection);
|
Q_EMIT activeConnectionChanged(newConnection);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,12 +166,6 @@ int main(int argc, char *argv[])
|
|||||||
Connection::setEncryptionDefault(true);
|
Connection::setEncryptionDefault(true);
|
||||||
Connection::setDirectChatEncryptionDefault(true);
|
Connection::setDirectChatEncryptionDefault(true);
|
||||||
|
|
||||||
#ifdef NEOCHAT_FLATPAK
|
|
||||||
// Copy over the included FontConfig configuration to the
|
|
||||||
// app's config dir:
|
|
||||||
QFile::copy(u"/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"_s, u"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"_s);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ColorSchemer colorScheme;
|
ColorSchemer colorScheme;
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "jobs/neochatgetcommonroomsjob.h"
|
#include "jobs/neochatgetcommonroomsjob.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <Quotient/room.h>
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
@@ -39,8 +40,22 @@ void CommonRoomsModel::setUserId(const QString &userId)
|
|||||||
|
|
||||||
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
|
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(index)
|
auto roomId = m_commonRooms[index.row()];
|
||||||
Q_UNUSED(roleName)
|
auto room = connection()->room(roomId);
|
||||||
|
if (!room) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (roleName) {
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
case RoomNameRole:
|
||||||
|
return room->displayName();
|
||||||
|
case RoomAvatarRole:
|
||||||
|
return room->avatarUrl();
|
||||||
|
case RoomIdRole:
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +65,15 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const
|
|||||||
return m_commonRooms.size();
|
return m_commonRooms.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> CommonRoomsModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{RoomIdRole, "roomId"},
|
||||||
|
{RoomNameRole, "roomName"},
|
||||||
|
{RoomAvatarRole, "roomAvatar"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void CommonRoomsModel::reload()
|
void CommonRoomsModel::reload()
|
||||||
{
|
{
|
||||||
if (!m_connection || m_userId.isEmpty()) {
|
if (!m_connection || m_userId.isEmpty()) {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class CommonRoomsModel : public QAbstractListModel
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
enum Roles {
|
enum Roles {
|
||||||
RoomIdRole = Qt::DisplayRole,
|
RoomIdRole = Qt::UserRole,
|
||||||
|
RoomNameRole,
|
||||||
|
RoomAvatarRole,
|
||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ public:
|
|||||||
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
|
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
|
||||||
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
|
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
|
||||||
|
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void connectionChanged();
|
void connectionChanged();
|
||||||
void userIdChanged();
|
void userIdChanged();
|
||||||
|
|||||||
41
src/app/models/limitermodel.cpp
Normal file
41
src/app/models/limitermodel.cpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "models/limitermodel.h"
|
||||||
|
|
||||||
|
LimiterModel::LimiterModel(QObject *parent)
|
||||||
|
: QSortFilterProxyModel(parent)
|
||||||
|
{
|
||||||
|
connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged);
|
||||||
|
connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged);
|
||||||
|
connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LimiterModel::maximumCount() const
|
||||||
|
{
|
||||||
|
return m_maximumCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LimiterModel::setMaximumCount(int maximumCount)
|
||||||
|
{
|
||||||
|
if (m_maximumCount != maximumCount) {
|
||||||
|
m_maximumCount = maximumCount;
|
||||||
|
Q_EMIT maximumCountChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int LimiterModel::extraCount() const
|
||||||
|
{
|
||||||
|
if (sourceModel()) {
|
||||||
|
return std::max(sourceModel()->rowCount() - maximumCount(), 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(source_parent)
|
||||||
|
return source_row < maximumCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_limitermodel.cpp"
|
||||||
41
src/app/models/limitermodel.h
Normal file
41
src/app/models/limitermodel.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class LimiterModel
|
||||||
|
*
|
||||||
|
* @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount.
|
||||||
|
*
|
||||||
|
* Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch.
|
||||||
|
*/
|
||||||
|
class LimiterModel : public QSortFilterProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged)
|
||||||
|
Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LimiterModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] int maximumCount() const;
|
||||||
|
void setMaximumCount(int maximumCount);
|
||||||
|
|
||||||
|
[[nodiscard]] int extraCount() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void maximumCountChanged();
|
||||||
|
void extraCountChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_maximumCount = 0;
|
||||||
|
};
|
||||||
@@ -287,6 +287,7 @@ Name[ia]=Comparti
|
|||||||
Name[it]=Condivisione
|
Name[it]=Condivisione
|
||||||
Name[ka]=გაზიარება
|
Name[ka]=გაზიარება
|
||||||
Name[ko]=공유
|
Name[ko]=공유
|
||||||
|
Name[lt]=Bendrinti
|
||||||
Name[lv]=Kopīgot
|
Name[lv]=Kopīgot
|
||||||
Name[nl]=Gedeelde
|
Name[nl]=Gedeelde
|
||||||
Name[nn]=Del
|
Name[nn]=Del
|
||||||
@@ -322,6 +323,7 @@ Comment[ia]=Le exito de compartir un pecietta de contento
|
|||||||
Comment[it]=Il risultato della condivisione di un contenuto
|
Comment[it]=Il risultato della condivisione di un contenuto
|
||||||
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
|
Comment[ka]=შემცველობის ნაწილის გაზიარების შედეგი
|
||||||
Comment[ko]=콘텐츠 공유 결과
|
Comment[ko]=콘텐츠 공유 결과
|
||||||
|
Comment[lt]=Turinio dalies bendrinimo rezultatas
|
||||||
Comment[lv]=Satura kopīgošanas rezultāts
|
Comment[lv]=Satura kopīgošanas rezultāts
|
||||||
Comment[nl]=Het resultaat van het delen van een stukje inhoud
|
Comment[nl]=Het resultaat van het delen van een stukje inhoud
|
||||||
Comment[nn]=Resultatet av deling av innhald
|
Comment[nn]=Resultatet av deling av innhald
|
||||||
|
|||||||
@@ -211,10 +211,6 @@
|
|||||||
<label>Enable threads</label>
|
<label>Enable threads</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
<entry name="SecretBackup" type="bool">
|
|
||||||
<label>Enable secret backup</label>
|
|
||||||
<default>false</default>
|
|
||||||
</entry>
|
|
||||||
<entry name="Phone3PId" type="bool">
|
<entry name="Phone3PId" type="bool">
|
||||||
<label>Enable add phone numbers as 3PIDs</label>
|
<label>Enable add phone numbers as 3PIDs</label>
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ KirigamiComponents.ConvergentContextMenu {
|
|||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18nc("@action:inmenu", "Open Secret Backup")
|
text: i18nc("@action:inmenu", "Open Secret Backup")
|
||||||
icon.name: "unlock"
|
icon.name: "unlock"
|
||||||
visible: NeoChatConfig.secretBackup
|
|
||||||
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
|
||||||
title: i18nc("@title:window", "Open Key Backup")
|
title: i18nc("@title:window", "Open Key Backup")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,14 +45,12 @@ Labs.MenuBar {
|
|||||||
}
|
}
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
icon.name: "compass-symbolic"
|
icon.name: "compass-symbolic"
|
||||||
text: i18nc("@action:inmenu", "Explore Rooms")
|
text: i18nc("@action:inmenu Explore public rooms and spaces", "Explore")
|
||||||
enabled: root.connection
|
enabled: root.connection
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
}, {
|
}, {});
|
||||||
title: i18nc("@title", "Explore Rooms")
|
|
||||||
});
|
|
||||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,16 +27,17 @@ ColumnLayout {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
|
|
||||||
KirigamiComponents.Avatar {
|
KirigamiComponents.AvatarButton {
|
||||||
id: avatar
|
id: avatar
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
name: root.invitingMember.displayName
|
name: root.invitingMember.displayName
|
||||||
source: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl
|
source: NeoChatConfig.hideImages ? undefined : root.invitingMember.avatarUrl
|
||||||
color: root.invitingMember.color
|
color: root.invitingMember.color
|
||||||
|
|
||||||
|
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
@@ -54,6 +55,12 @@ ColumnLayout {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: root.currentRoom.displayName
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.SelectableLabel {
|
Kirigami.SelectableLabel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
font: Kirigami.Theme.smallFont
|
font: Kirigami.Theme.smallFont
|
||||||
@@ -61,12 +68,7 @@ ColumnLayout {
|
|||||||
visible: root.currentRoom && root.currentRoom.canonicalAlias
|
visible: root.currentRoom && root.currentRoom.canonicalAlias
|
||||||
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
|
text: root.currentRoom && root.currentRoom.canonicalAlias ? root.currentRoom.canonicalAlias : ""
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
}
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
Kirigami.Heading {
|
|
||||||
text: root.currentRoom.displayName
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,8 +139,24 @@ ColumnLayout {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
FormCard.FormButtonDelegate {
|
||||||
|
id: viewProfileDelegate
|
||||||
|
|
||||||
|
icon.name: "user-properties-symbolic"
|
||||||
|
text: i18nc("@action:button View this user's profile", "View %1's Profile", root.invitingMember.displayName)
|
||||||
|
|
||||||
|
onClicked: RoomManager.resolveResource(root.currentRoom.invitingUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {
|
||||||
|
above: viewProfileDelegate
|
||||||
|
below: ignoreUserDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
id: ignoreUserDelegate
|
||||||
|
|
||||||
icon.name: "list-remove-symbolic"
|
icon.name: "list-remove-symbolic"
|
||||||
text: i18nc("@action:button Block the user", "Block %1", root.invitingMember.displayName)
|
text: i18nc("@action:button Ignore the user", "Ignore %1 and Reject Invite", root.invitingMember.displayName)
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.currentRoom.forget()
|
root.currentRoom.forget()
|
||||||
|
|||||||
@@ -23,72 +23,63 @@ Kirigami.Page {
|
|||||||
name: "cancelled"
|
name: "cancelled"
|
||||||
when: root.session.state === KeyVerificationSession.CANCELED
|
when: root.session.state === KeyVerificationSession.CANCELED
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: verificationCanceled
|
||||||
sourceComponent: verificationCanceled
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "waitingForVerification"
|
name: "waitingForVerification"
|
||||||
when: root.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
|
when: root.session.state === KeyVerificationSession.WAITINGFORVERIFICATION
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: emojiSas
|
||||||
sourceComponent: emojiSas
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "waitingForReady"
|
name: "waitingForReady"
|
||||||
when: root.session.state === KeyVerificationSession.WAITINGFORREADY
|
when: root.session.state === KeyVerificationSession.WAITINGFORREADY
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "incoming"
|
name: "incoming"
|
||||||
when: root.session.state === KeyVerificationSession.INCOMING
|
when: root.session.state === KeyVerificationSession.INCOMING
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "waitingForKey"
|
name: "waitingForKey"
|
||||||
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
|
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "waitingForAccept"
|
name: "waitingForAccept"
|
||||||
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
|
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "waitingForMac"
|
name: "waitingForMac"
|
||||||
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
|
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "ready"
|
name: "ready"
|
||||||
when: root.session.state === KeyVerificationSession.READY
|
when: root.session.state === KeyVerificationSession.READY
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: chooseVerificationComponent
|
||||||
sourceComponent: chooseVerificationComponent
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "done"
|
name: "done"
|
||||||
when: root.session.state === KeyVerificationSession.DONE
|
when: root.session.state === KeyVerificationSession.DONE
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: stateLoader
|
stateLoader.sourceComponent: message
|
||||||
sourceComponent: message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,13 +23,45 @@ Components.AlbumMaximizeComponent {
|
|||||||
*/
|
*/
|
||||||
required property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
|
|
||||||
readonly property string currentEventId: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.EventIdRole)
|
readonly property string currentEventId: {
|
||||||
|
const idx = (content as ListView).currentIndex;
|
||||||
|
|
||||||
readonly property var currentAuthor: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.AuthorRole)
|
if (idx === -1) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var currentTime: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.TimeRole)
|
return model.data(model.index(idx, 0), TimelineMessageModel.EventIdRole)
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var currentProgressInfo: model.data(model.index((content as ListView).currentIndex, 0), TimelineMessageModel.ProgressInfoRole)
|
readonly property var currentAuthor: {
|
||||||
|
const idx = (content as ListView).currentIndex;
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.data(model.index(idx, 0), TimelineMessageModel.AuthorRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentTime: {
|
||||||
|
|
||||||
|
const idx = (content as ListView).currentIndex;
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.data(model.index(idx, 0), TimelineMessageModel.TimeRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var currentProgressInfo: {
|
||||||
|
const idx = (content as ListView).currentIndex;
|
||||||
|
|
||||||
|
if (idx === -1) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
model.data(model.index(idx, 0), TimelineMessageModel.ProgressInfoRole)
|
||||||
|
}
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
ShareAction {
|
ShareAction {
|
||||||
@@ -122,7 +154,10 @@ Components.AlbumMaximizeComponent {
|
|||||||
|
|
||||||
onOpened: forceActiveFocus()
|
onOpened: forceActiveFocus()
|
||||||
|
|
||||||
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
|
onItemRightClicked: {
|
||||||
|
const event = root.currentRoom.findEvent(root.currentEventId);
|
||||||
|
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.currentRoom)
|
||||||
|
}
|
||||||
|
|
||||||
onSaveItem: {
|
onSaveItem: {
|
||||||
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
|
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Kirigami.SearchDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: if (currentItem) {
|
onAccepted: if (currentItem) {
|
||||||
(root.currentItem as RoomDelegate).clicked();
|
(currentItem as QQC2.ItemDelegate).clicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
|
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
|
||||||
@@ -30,15 +30,13 @@ Kirigami.SearchDialog {
|
|||||||
emptyText: i18nc("Placeholder message", "No room found")
|
emptyText: i18nc("Placeholder message", "No room found")
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: exploreRoomAction
|
id: exploreRoomAction
|
||||||
text: i18nc("@action:button", "Explore rooms")
|
text: i18nc("@action:button Explore public rooms and spaces", "Explore")
|
||||||
icon.name: "compass"
|
icon.name: "compass"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.close()
|
root.close()
|
||||||
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||||
connection: root.connection
|
connection: root.connection
|
||||||
}, {
|
}, {});
|
||||||
title: i18nc("@title", "Explore Rooms")
|
|
||||||
});
|
|
||||||
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
dialog.roomSelected.connect((roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined) => {
|
||||||
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
RoomManager.resolveResource(roomId.length > 0 ? roomId : alias, isJoined ? "" : "join");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,14 +75,6 @@ Kirigami.Page {
|
|||||||
focus: true
|
focus: true
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
background: null // This needs to stay null, because of transparency blur
|
|
||||||
|
|
||||||
onHeightChanged: {
|
|
||||||
// HACK: See TimelineView for the hack details.
|
|
||||||
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
|
|
||||||
(timelineViewLoader.item as TimelineView).resetViewSettling();
|
|
||||||
}
|
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: jitsiMeetingAction
|
id: jitsiMeetingAction
|
||||||
@@ -263,7 +255,6 @@ Kirigami.Page {
|
|||||||
id: timelineView
|
id: timelineView
|
||||||
messageFilterModel: root.messageFilterModel
|
messageFilterModel: root.messageFilterModel
|
||||||
compactLayout: NeoChatConfig.compactLayout
|
compactLayout: NeoChatConfig.compactLayout
|
||||||
fileDropEnabled: !Controller.isFlatpak
|
|
||||||
markReadCondition: NeoChatConfig.markReadCondition
|
markReadCondition: NeoChatConfig.markReadCondition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,8 +349,8 @@ Kirigami.Page {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
|
function onShowDelegateMenu(parent: QtObject, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
|
||||||
(delegateContextMenu.createObject(root, {
|
(delegateContextMenu.createObject(parent, {
|
||||||
author: author,
|
author: author,
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
plainText: plainText,
|
plainText: plainText,
|
||||||
|
|||||||
90
src/app/qml/SeenByDialog.qml
Normal file
90
src/app/qml/SeenByDialog.qml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||||
|
import org.kde.kirigamiaddons.delegates as Delegates
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
Kirigami.Dialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var model
|
||||||
|
|
||||||
|
standardButtons: Kirigami.Dialog.NoButton
|
||||||
|
|
||||||
|
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||||
|
maximumHeight: Kirigami.Units.gridUnit * 24
|
||||||
|
title: i18nc("@title:menu Seen by/read marker dialog", "Seen By")
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
QQC2.ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
model: root.model
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
onCountChanged: {
|
||||||
|
if (listView.count === 0) {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Delegates.RoundedItemDelegate {
|
||||||
|
id: userDelegate
|
||||||
|
|
||||||
|
required property string displayName
|
||||||
|
required property url avatarUrl
|
||||||
|
required property color memberColor
|
||||||
|
required property string userId
|
||||||
|
|
||||||
|
implicitHeight: Kirigami.Units.gridUnit * 2
|
||||||
|
|
||||||
|
text: displayName
|
||||||
|
highlighted: false
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.close();
|
||||||
|
RoomManager.resolveResource(userDelegate.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
KirigamiComponents.Avatar {
|
||||||
|
implicitWidth: height
|
||||||
|
sourceSize {
|
||||||
|
height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||||
|
width: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 2.5
|
||||||
|
}
|
||||||
|
source: userDelegate.avatarUrl
|
||||||
|
name: userDelegate.displayName
|
||||||
|
color: userDelegate.memberColor
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
QQC2.Label {
|
||||||
|
text: userDelegate.displayName
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
clip: true // Intentional to limit insane Unicode in display names
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import org.kde.neochat
|
|||||||
FormCard.FormCardPage {
|
FormCard.FormCardPage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool processing: false
|
||||||
|
|
||||||
title: i18nc("@title:window", "Load your encrypted messages")
|
title: i18nc("@title:window", "Load your encrypted messages")
|
||||||
|
|
||||||
topPadding: Kirigami.Units.gridUnit
|
topPadding: Kirigami.Units.gridUnit
|
||||||
@@ -25,75 +27,42 @@ FormCard.FormCardPage {
|
|||||||
position: Kirigami.InlineMessage.Position.Header
|
position: Kirigami.InlineMessage.Position.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
property SSSSHandler ssssHandler: SSSSHandler {
|
Connections {
|
||||||
id: ssssHandler
|
target: Controller.activeConnection
|
||||||
|
function onKeyBackupError(): void {
|
||||||
|
securityKeyField.clear()
|
||||||
|
root.processing = false
|
||||||
|
banner.text = i18nc("@info:status", "The security key or backup passphrase was not correct.")
|
||||||
|
banner.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
property bool processing: false
|
function onKeyBackupUnlocked(): void {
|
||||||
|
root.processing = false
|
||||||
connection: Controller.activeConnection
|
|
||||||
onKeyBackupUnlocked: {
|
|
||||||
ssssHandler.processing = false
|
|
||||||
banner.text = i18nc("@info:status", "Encryption keys restored.")
|
banner.text = i18nc("@info:status", "Encryption keys restored.")
|
||||||
banner.type = Kirigami.MessageType.Positive
|
banner.type = Kirigami.MessageType.Positive
|
||||||
banner.visible = true
|
banner.visible = true
|
||||||
}
|
}
|
||||||
onError: error => {
|
|
||||||
if (error !== SSSSHandler.WrongKeyError) {
|
|
||||||
banner.text = error
|
|
||||||
banner.visible = true
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
passwordField.clear()
|
|
||||||
ssssHandler.processing = false
|
|
||||||
banner.text = i18nc("@info:status", "The security phrase was not correct.")
|
|
||||||
banner.visible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
title: i18nc("@title", "Unlock using Passphrase")
|
title: i18nc("@title", "Unlock using Security Key or Backup Passphrase")
|
||||||
}
|
}
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
FormCard.FormTextDelegate {
|
FormCard.FormTextDelegate {
|
||||||
description: i18nc("@info", "If you have a backup passphrase for this account, enter it below.")
|
description: i18nc("@info", "If you have a security key or backup passphrase for this account, enter it below or upload it as a file.")
|
||||||
}
|
|
||||||
FormCard.FormTextFieldDelegate {
|
|
||||||
id: passwordField
|
|
||||||
label: i18nc("@label:textbox", "Backup Password:")
|
|
||||||
echoMode: TextInput.Password
|
|
||||||
}
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
id: unlockButton
|
|
||||||
text: i18nc("@action:button", "Unlock")
|
|
||||||
icon.name: "unlock"
|
|
||||||
enabled: passwordField.text.length > 0 && !ssssHandler.processing
|
|
||||||
onClicked: {
|
|
||||||
ssssHandler.processing = true
|
|
||||||
banner.visible = false
|
|
||||||
ssssHandler.unlockSSSSWithPassphrase(passwordField.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormHeader {
|
|
||||||
title: i18nc("@title", "Unlock using Security Key")
|
|
||||||
}
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormTextDelegate {
|
|
||||||
description: i18nc("@info", "If you have a security key for this account, enter it below or upload it as a file.")
|
|
||||||
}
|
}
|
||||||
FormCard.FormTextFieldDelegate {
|
FormCard.FormTextFieldDelegate {
|
||||||
id: securityKeyField
|
id: securityKeyField
|
||||||
label: i18nc("@label:textbox", "Security Key:")
|
label: i18nc("@label:textbox", "Security Key or Backup Passphrase:")
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
}
|
}
|
||||||
FormCard.FormButtonDelegate {
|
FormCard.FormButtonDelegate {
|
||||||
id: uploadSecurityKeyButton
|
id: uploadSecurityKeyButton
|
||||||
text: i18nc("@action:button", "Upload from File")
|
text: i18nc("@action:button", "Upload from File")
|
||||||
icon.name: "cloud-upload"
|
icon.name: "cloud-upload"
|
||||||
enabled: !ssssHandler.processing
|
enabled: !root.processing
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ssssHandler.processing = true
|
root.processing = true
|
||||||
openFileDialog.open()
|
openFileDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +70,10 @@ FormCard.FormCardPage {
|
|||||||
id: unlockSecurityKeyButton
|
id: unlockSecurityKeyButton
|
||||||
text: i18nc("@action:button", "Unlock")
|
text: i18nc("@action:button", "Unlock")
|
||||||
icon.name: "unlock"
|
icon.name: "unlock"
|
||||||
enabled: securityKeyField.text.length > 0 && !ssssHandler.processing
|
enabled: securityKeyField.text.length > 0 && !root.processing
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ssssHandler.processing = true
|
root.processing = true
|
||||||
ssssHandler.unlockSSSSFromSecurityKey(securityKeyField.text)
|
Controller.activeConnection.unlockSSSS(securityKeyField.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +89,10 @@ FormCard.FormCardPage {
|
|||||||
id: unlockCrossSigningButton
|
id: unlockCrossSigningButton
|
||||||
icon.name: "emblem-shared-symbolic"
|
icon.name: "emblem-shared-symbolic"
|
||||||
text: i18nc("@action:button", "Request from other Devices")
|
text: i18nc("@action:button", "Request from other Devices")
|
||||||
enabled: !ssssHandler.processing
|
enabled: !root.processing
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ssssHandler.processing = true
|
root.processing = true
|
||||||
ssssHandler.unlockSSSSFromCrossSigning()
|
Controller.activeConnection.unlockSSSS("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,37 +20,62 @@ Kirigami.Dialog {
|
|||||||
// Make sure that code is prepared to deal with this property being null
|
// Make sure that code is prepared to deal with this property being null
|
||||||
property NeoChatRoom room
|
property NeoChatRoom room
|
||||||
property var user
|
property var user
|
||||||
|
// Used for the "View Main Profile" feature so you can toggle back to the room profile.
|
||||||
|
property NeoChatRoom oldRoom
|
||||||
|
|
||||||
property NeoChatConnection connection
|
property NeoChatConnection connection
|
||||||
|
|
||||||
leftPadding: 0
|
property CommonRoomsModel model: CommonRoomsModel {
|
||||||
rightPadding: 0
|
connection: root.connection
|
||||||
topPadding: 0
|
userId: root.user.id
|
||||||
bottomPadding: 0
|
}
|
||||||
|
|
||||||
|
property LimiterModel limiterModel: LimiterModel {
|
||||||
|
maximumCount: 5
|
||||||
|
sourceModel: root.model
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool isSelf: root.user.id === root.connection.localUserId
|
||||||
|
readonly property bool hasMutualRooms: root.model.count > 0
|
||||||
|
readonly property bool isRoomProfile: root.room
|
||||||
|
readonly property string shareUrl: "https://matrix.to/#/" + root.user.id
|
||||||
|
|
||||||
|
leftPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
rightPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
topPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
bottomPadding: Kirigami.Units.largeSpacing * 2
|
||||||
|
|
||||||
standardButtons: Kirigami.Dialog.NoButton
|
standardButtons: Kirigami.Dialog.NoButton
|
||||||
|
|
||||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||||
title: i18nc("@title:menu Account details dialog", "Account Details")
|
title: i18nc("@title:menu Account details dialog", "Account Details")
|
||||||
|
|
||||||
|
header: null
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: detailRow
|
id: detailRow
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
||||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
|
||||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
KirigamiComponents.Avatar {
|
KirigamiComponents.Avatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||||
|
|
||||||
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||||
source: root.room ? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
|
source: {
|
||||||
|
if (root.room) {
|
||||||
|
return root.room.member(root.user.id).avatarUrl;
|
||||||
|
} else if(root.user.avatarUrl.toString() !== '') {
|
||||||
|
return root.connection.makeMediaUrl(root.user.avatarUrl);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
|
color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,223 +100,355 @@ Kirigami.Dialog {
|
|||||||
id: idLabel
|
id: idLabel
|
||||||
textFormat: TextEdit.PlainText
|
textFormat: TextEdit.PlainText
|
||||||
text: idLabelTextMetrics.elidedText
|
text: idLabelTextMetrics.elidedText
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: idLabelTextMetrics
|
id: idLabelTextMetrics
|
||||||
text: root.user.id
|
text: root.user.id
|
||||||
elide: Qt.ElideRight
|
elide: Qt.ElideRight
|
||||||
elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
|
elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Label {
|
Kirigami.ActionToolBar {
|
||||||
property CommonRoomsModel model: CommonRoomsModel {
|
|
||||||
connection: root.connection
|
|
||||||
userId: root.user.id
|
|
||||||
}
|
|
||||||
|
|
||||||
text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count)
|
|
||||||
color: Kirigami.Theme.disabledTextColor
|
|
||||||
visible: model.count > 0
|
|
||||||
|
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:intoolbar Message this user directly", "Message")
|
||||||
|
icon.name: "document-send-symbolic"
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.close();
|
||||||
|
root.connection.requestDirectChat(root.user.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
icon.name: "im-invisible-user-symbolic"
|
||||||
|
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.close();
|
||||||
|
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
|
||||||
|
icon.name: "username-copy-symbolic"
|
||||||
|
|
||||||
|
onTriggered: Clipboard.saveText(root.shareUrl)
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
|
||||||
|
icon.name: "search-symbolic"
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
|
||||||
|
room: root.room,
|
||||||
|
senderId: root.user.id
|
||||||
|
}, {
|
||||||
|
title: i18nc("@action:title", "Search")
|
||||||
|
});
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:intoolbar", "Show QR Code")
|
||||||
|
icon.name: "view-barcode-qr-symbolic"
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||||
|
text: root.shareUrl,
|
||||||
|
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
|
||||||
|
subtitle: root.user.id,
|
||||||
|
avatarColor: root.room?.member(root.user.id).color,
|
||||||
|
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
|
||||||
|
}) as QrCodeMaximizeComponent;
|
||||||
|
root.close();
|
||||||
|
qrCode.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
|
||||||
|
icon.name: "dialog-warning-symbolic"
|
||||||
|
visible: root.connection.supportsMatrixSpecVersion("v1.13")
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||||
|
title: i18nc("@title:dialog", "Report User"),
|
||||||
|
placeholder: i18nc("@info:placeholder", "Reason for reporting this user"),
|
||||||
|
icon: "dialog-warning-symbolic",
|
||||||
|
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title", "Report User"),
|
||||||
|
width: Kirigami.Units.gridUnit * 25
|
||||||
|
}) as ReasonDialog;
|
||||||
|
dialog.accepted.connect(reason => {
|
||||||
|
root.connection.reportUser(root.user.id, reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: root.room
|
||||||
|
|
||||||
|
text: i18nc("@action:button", "View Main Profile")
|
||||||
|
icon.name: "user-properties-symbolic"
|
||||||
|
onTriggered: {
|
||||||
|
root.oldRoom = root.room;
|
||||||
|
root.room = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: !root.room && root.oldRoom
|
||||||
|
|
||||||
|
text: i18nc("@action:button", "View Room Profile")
|
||||||
|
icon.name: "user-properties-symbolic"
|
||||||
|
onTriggered: {
|
||||||
|
root.room = root.oldRoom;
|
||||||
|
root.oldRoom = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QQC2.AbstractButton {
|
}
|
||||||
id: qrButton
|
|
||||||
Layout.minimumHeight: avatar.height * 0.75
|
|
||||||
Layout.maximumHeight: avatar.height * 1.5
|
|
||||||
Layout.maximumWidth: avatar.height * 1.5
|
|
||||||
|
|
||||||
contentItem: Barcode {
|
Kirigami.Heading {
|
||||||
id: barcode
|
text: i18nc("@title Moderation actions for this user", "Moderation")
|
||||||
barcodeType: Barcode.QRCode
|
level: 2
|
||||||
content: "https://matrix.to/#/" + root.user.id
|
visible: root.isRoomProfile && moderationToolbar.actions.filter(function (it) { return it.visible; }).length > 0
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
}
|
||||||
text: barcode.content,
|
|
||||||
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
|
Kirigami.ActionToolBar {
|
||||||
subtitle: root.user.id,
|
id: moderationToolbar
|
||||||
avatarColor: root.room?.member(root.user.id).color,
|
|
||||||
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
|
flat: false
|
||||||
}) as QrCodeMaximizeComponent;
|
visible: root.isRoomProfile
|
||||||
root.close();
|
|
||||||
qrCode.open();
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: {
|
||||||
|
if (root.room) {
|
||||||
|
return !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text: i18nc("@action:button Kick the user from the room", "Kick…")
|
||||||
|
icon.name: "im-kick-user"
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||||
|
title: i18nc("@title:dialog", "Kick User"),
|
||||||
|
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
|
||||||
|
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||||
|
icon: "im-kick-user"
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title:dialog", "Kick User"),
|
||||||
|
width: Kirigami.Units.gridUnit * 25
|
||||||
|
});
|
||||||
|
dialog.accepted.connect(reason => {
|
||||||
|
root.room.kickMember(root.user.id, reason);
|
||||||
|
});
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: {
|
||||||
|
if (root.room) {
|
||||||
|
return !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text: i18nc("@action:button Ban this user from the room", "Ban…")
|
||||||
|
icon.name: "im-ban-user"
|
||||||
|
icon.color: Kirigami.Theme.negativeTextColor
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||||
|
title: i18nc("@title:dialog", "Ban User"),
|
||||||
|
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
|
||||||
|
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||||
|
icon: "im-ban-user"
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title:dialog", "Ban User"),
|
||||||
|
width: Kirigami.Units.gridUnit * 25
|
||||||
|
});
|
||||||
|
dialog.accepted.connect(reason => {
|
||||||
|
root.room.ban(root.user.id, reason);
|
||||||
|
});
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: {
|
||||||
|
if (root.room) {
|
||||||
|
return !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text: i18nc("@action:button Unban the user from this room", "Unban")
|
||||||
|
icon.name: "im-irc"
|
||||||
|
icon.color: Kirigami.Theme.negativeTextColor
|
||||||
|
onTriggered: {
|
||||||
|
root.room.unban(root.user.id);
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
visible: (root.user.id === root.connection.localUserId || (root.room?.canSendState("redact") ?? false))
|
||||||
|
|
||||||
|
text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…")
|
||||||
|
icon.name: "delete"
|
||||||
|
icon.color: Kirigami.Theme.negativeTextColor
|
||||||
|
onTriggered: {
|
||||||
|
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||||
|
title: i18nc("@title:dialog", "Remove Messages"),
|
||||||
|
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
|
||||||
|
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||||
|
icon: "delete"
|
||||||
|
}, {
|
||||||
|
title: i18nc("@title", "Remove Messages"),
|
||||||
|
width: Kirigami.Units.gridUnit * 25
|
||||||
|
});
|
||||||
|
dialog.accepted.connect(reason => {
|
||||||
|
root.room.deleteMessagesByUser(root.user.id, reason);
|
||||||
|
});
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Power Level")
|
||||||
|
level: 2
|
||||||
|
visible: root.isRoomProfile
|
||||||
|
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
visible: root.isRoomProfile
|
||||||
|
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Button {
|
||||||
|
visible: {
|
||||||
|
if (root.room) {
|
||||||
|
return root.room.canSendState("m.room.power_levels") && !(root.room.roomCreatorHasUltimatePowerLevel() && root.room.isCreator(root.user.id));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level")
|
||||||
|
icon.name: "document-edit-symbolic"
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered
|
||||||
QQC2.ToolTip.text: barcode.content
|
QQC2.ToolTip.text: text
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.Chip {
|
onClicked: {
|
||||||
visible: root.room
|
(powerLevelDialog.createObject(this, {
|
||||||
text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : ""
|
room: root.room,
|
||||||
closable: false
|
userId: root.user.id,
|
||||||
checkable: false
|
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
|
||||||
|
}) as PowerLevelDialog).open();
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
|
||||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
Component {
|
||||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.Separator {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.user.id !== root.connection.localUserId && !!root.user
|
|
||||||
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
|
|
||||||
icon.name: "im-invisible-user"
|
|
||||||
onClicked: {
|
|
||||||
root.close();
|
|
||||||
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
|
|
||||||
|
|
||||||
text: i18nc("@action:button", "Kick this user")
|
|
||||||
icon.name: "im-kick-user"
|
|
||||||
onClicked: {
|
|
||||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
|
||||||
title: i18nc("@title:dialog", "Kick User"),
|
|
||||||
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
|
|
||||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
|
||||||
icon: "im-kick-user"
|
|
||||||
}, {
|
|
||||||
title: i18nc("@title:dialog", "Kick User"),
|
|
||||||
width: Kirigami.Units.gridUnit * 25
|
|
||||||
});
|
|
||||||
dialog.accepted.connect(reason => {
|
|
||||||
root.room.kickMember(root.user.id, reason);
|
|
||||||
});
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("invite") && !root.room.containsUser(root.user.id)
|
|
||||||
|
|
||||||
enabled: root.room && !root.room.isUserBanned(root.user.id)
|
|
||||||
text: i18nc("@action:button", "Invite this user")
|
|
||||||
icon.name: "list-add-user"
|
|
||||||
onClicked: {
|
|
||||||
root.room.inviteToRoom(root.user.id);
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId)
|
|
||||||
|
|
||||||
text: i18nc("@action:button", "Ban this user")
|
|
||||||
icon.name: "im-ban-user"
|
|
||||||
icon.color: Kirigami.Theme.negativeTextColor
|
|
||||||
onClicked: {
|
|
||||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
|
||||||
title: i18nc("@title:dialog", "Ban User"),
|
|
||||||
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
|
|
||||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
|
||||||
icon: "im-ban-user"
|
|
||||||
}, {
|
|
||||||
title: i18nc("@title:dialog", "Ban User"),
|
|
||||||
width: Kirigami.Units.gridUnit * 25
|
|
||||||
});
|
|
||||||
dialog.accepted.connect(reason => {
|
|
||||||
root.room.ban(root.user.id, reason);
|
|
||||||
});
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id)
|
|
||||||
|
|
||||||
text: i18nc("@action:button", "Unban this user")
|
|
||||||
icon.name: "im-irc"
|
|
||||||
icon.color: Kirigami.Theme.negativeTextColor
|
|
||||||
onClicked: {
|
|
||||||
root.room.unban(root.user.id);
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
visible: root.room && root.room.canSendState("m.room.power_levels")
|
|
||||||
text: i18nc("@action:button", "Set user power level")
|
|
||||||
icon.name: "visibility"
|
|
||||||
onClicked: {
|
|
||||||
(powerLevelDialog.createObject(this, {
|
|
||||||
room: root.room,
|
|
||||||
userId: root.user.id,
|
|
||||||
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
|
|
||||||
}) as PowerLevelDialog).open();
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: powerLevelDialog
|
|
||||||
PowerLevelDialog {
|
|
||||||
id: powerLevelDialog
|
id: powerLevelDialog
|
||||||
|
PowerLevelDialog {
|
||||||
|
id: powerLevelDialog
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
Kirigami.Heading {
|
||||||
visible: root.room && (root.user.id === root.connection.localUserId || root.room.canSendState("redact"))
|
text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms")
|
||||||
|
level: 4
|
||||||
|
visible: !root.isSelf && root.hasMutualRooms
|
||||||
|
|
||||||
text: i18nc("@action:button", "Remove recent messages by this user")
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
icon.name: "delete"
|
}
|
||||||
icon.color: Kirigami.Theme.negativeTextColor
|
|
||||||
onClicked: {
|
RowLayout {
|
||||||
let dialog = ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
spacing: Kirigami.Units.smallSpacing
|
||||||
title: i18nc("@title:dialog", "Remove Messages"),
|
visible: !root.isSelf && root.hasMutualRooms
|
||||||
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
|
|
||||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
icon: "delete"
|
|
||||||
}, {
|
Repeater {
|
||||||
title: i18nc("@title", "Remove Messages"),
|
model: root.limiterModel
|
||||||
width: Kirigami.Units.gridUnit * 25
|
|
||||||
});
|
delegate: KirigamiComponents.AvatarButton {
|
||||||
dialog.accepted.connect(reason => {
|
required property string roomName
|
||||||
root.room.deleteMessagesByUser(root.user.id, reason);
|
required property string roomAvatar
|
||||||
});
|
required property string roomId
|
||||||
root.close();
|
|
||||||
|
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
|
||||||
|
name: roomName
|
||||||
|
source: roomAvatar
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.close();
|
||||||
|
RoomManager.resolveResource(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.text: name
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18ncp("@info:label And '%1' more rooms you have in common with this user, but are not shown", "and 1 more…", "and %1 more…", root.limiterModel.extraCount)
|
||||||
|
visible: root.limiterModel.extraCount > 0
|
||||||
|
color: Kirigami.Theme.disabledTextColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
Kirigami.Heading {
|
||||||
visible: root.user.id !== root.connection.localUserId
|
text: i18nc("@title Private note for this user", "Private Note")
|
||||||
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
|
level: 4
|
||||||
icon.name: "document-send"
|
|
||||||
onClicked: {
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
root.connection.requestDirectChat(root.user.id);
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
QQC2.TextArea {
|
||||||
text: i18nc("@action:button %1 is the name of the user.", "Search room for %1's messages", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName))
|
id: noteText
|
||||||
icon.name: "search-symbolic"
|
|
||||||
onClicked: {
|
|
||||||
((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
|
|
||||||
room: root.room,
|
|
||||||
senderId: root.user.id
|
|
||||||
}, {
|
|
||||||
title: i18nc("@action:title", "Search")
|
|
||||||
});
|
|
||||||
root.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
text: root.connection.noteForUser(root.user.id)
|
||||||
text: i18n("Copy link")
|
textFormat: TextEdit.PlainText
|
||||||
icon.name: "username-copy"
|
wrapMode: TextEdit.Wrap
|
||||||
onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id)
|
placeholderText: i18nc("@info:placeholder", "Only visible to you")
|
||||||
|
|
||||||
|
onTextEdited: editTimer.restart()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
// Prevent unnecessary edits by waiting 1 second
|
||||||
|
Timer {
|
||||||
|
id: editTimer
|
||||||
|
|
||||||
|
interval: 1000
|
||||||
|
onTriggered: root.connection.setNoteForUser(root.user.id, noteText.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,28 +29,6 @@
|
|||||||
#include <KIO/OpenUrlJob>
|
#include <KIO/OpenUrlJob>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Stops RoomManager from updating the last room and space config.
|
|
||||||
*/
|
|
||||||
class LastRoomBlocker
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit LastRoomBlocker(RoomManager *manager)
|
|
||||||
: m_manager(manager)
|
|
||||||
{
|
|
||||||
Q_ASSERT(manager);
|
|
||||||
|
|
||||||
m_manager->m_dontUpdateLastRoom = true;
|
|
||||||
}
|
|
||||||
~LastRoomBlocker()
|
|
||||||
{
|
|
||||||
m_manager->m_dontUpdateLastRoom = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
RoomManager *m_manager;
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomManager::RoomManager(QObject *parent)
|
RoomManager::RoomManager(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_config(KSharedConfig::openStateConfig())
|
, m_config(KSharedConfig::openStateConfig())
|
||||||
@@ -304,26 +282,21 @@ void RoomManager::viewEventSource(const QString &eventId)
|
|||||||
Q_EMIT showEventSource(eventId);
|
Q_EMIT showEventSource(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
|
||||||
{
|
{
|
||||||
if (eventId.isEmpty()) {
|
if (!event) {
|
||||||
qWarning() << "Tried to open event menu with empty event id";
|
qWarning() << "Tried to open event menu with empty event";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto it = room->findInTimeline(eventId);
|
Q_EMIT showDelegateMenu(parent,
|
||||||
if (it == room->historyEdge()) {
|
event->id(),
|
||||||
// This is probably a pending event
|
room->qmlSafeMember(event->senderId()),
|
||||||
return;
|
MessageComponentType::typeForEvent(*event),
|
||||||
}
|
EventHandler::plainBody(room, event),
|
||||||
const auto &event = **it;
|
EventHandler::richBody(room, event),
|
||||||
Q_EMIT showDelegateMenu(eventId,
|
EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
|
||||||
room->qmlSafeMember(event.senderId()),
|
room->fileTransferInfo(event->id()),
|
||||||
MessageComponentType::typeForEvent(event),
|
|
||||||
EventHandler::plainBody(room, &event),
|
|
||||||
EventHandler::richBody(room, &event),
|
|
||||||
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
|
|
||||||
room->fileTransferInfo(eventId),
|
|
||||||
selectedText,
|
selectedText,
|
||||||
hoveredLink);
|
hoveredLink);
|
||||||
}
|
}
|
||||||
@@ -346,6 +319,17 @@ void RoomManager::loadInitialRoom()
|
|||||||
resolveResource(m_arg);
|
resolveResource(m_arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_isMobile) {
|
||||||
|
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||||
|
// We can't have empty keys in KConfig, so we stored it as "Home"
|
||||||
|
if (lastSpace == u"Home"_s) {
|
||||||
|
lastSpace.clear();
|
||||||
|
}
|
||||||
|
setCurrentSpace(lastSpace, false);
|
||||||
|
// We don't want to open a room on startup on mobile
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_currentRoom) {
|
if (m_currentRoom) {
|
||||||
// we opened a room with the arg parsing already
|
// we opened a room with the arg parsing already
|
||||||
return;
|
return;
|
||||||
@@ -358,14 +342,16 @@ void RoomManager::loadInitialRoom()
|
|||||||
|
|
||||||
void RoomManager::openRoomForActiveConnection()
|
void RoomManager::openRoomForActiveConnection()
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_connection);
|
if (!m_connection) {
|
||||||
|
setCurrentRoom({});
|
||||||
|
setCurrentSpace({}, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||||
if (lastSpace == u"Home"_s) {
|
if (lastSpace == u"Home"_s) {
|
||||||
lastSpace.clear();
|
lastSpace.clear();
|
||||||
}
|
}
|
||||||
// We don't want to open a room on startup on mobile
|
setCurrentSpace(lastSpace, true);
|
||||||
setCurrentSpace(lastSpace, !m_isMobile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||||
@@ -522,7 +508,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
|
|||||||
Q_EMIT connectionChanged();
|
Q_EMIT connectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||||
{
|
{
|
||||||
m_currentSpaceId = spaceId;
|
m_currentSpaceId = spaceId;
|
||||||
|
|
||||||
@@ -542,26 +528,25 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
|||||||
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we requested to change to the last opened room, do so:
|
if (!setRoom) {
|
||||||
if (goToLastUsedRoom) {
|
return;
|
||||||
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
|
|
||||||
LastRoomBlocker blocker(this);
|
|
||||||
|
|
||||||
// We can't have empty keys in KConfig, so it's stored as "Home":
|
|
||||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
|
||||||
resolveResource(lastRoom, "no_join"_L1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no last room was opened, go to the space home:
|
|
||||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
|
||||||
resolveResource(spaceId, "no_join"_L1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to no room opened:
|
|
||||||
setCurrentRoom({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We intentionally don't want to open the last room on mobile
|
||||||
|
if (m_isMobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't have empty keys in KConfig, so it's stored as "Home"
|
||||||
|
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||||
|
resolveResource(lastRoom, "no_join"_L1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||||
|
resolveResource(spaceId, "no_join"_L1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentRoom({});
|
||||||
}
|
}
|
||||||
|
|
||||||
QString RoomManager::findSpaceIdForCurrentRoom() const
|
QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||||
@@ -621,23 +606,21 @@ void RoomManager::setCurrentRoom(const QString &roomId)
|
|||||||
|
|
||||||
Q_EMIT currentRoomChanged();
|
Q_EMIT currentRoomChanged();
|
||||||
|
|
||||||
if (!m_dontUpdateLastRoom) {
|
if (roomId.isEmpty()) {
|
||||||
if (roomId.isEmpty()) {
|
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
||||||
// We can't have empty keys in KConfig, so name it "Home"
|
// We can't have empty keys in KConfig, so name it "Home"
|
||||||
if (spaceIdForRoom.isEmpty()) {
|
if (spaceIdForRoom.isEmpty()) {
|
||||||
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||||
} else {
|
} else {
|
||||||
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_currentSpaceId != spaceIdForRoom) {
|
if (m_currentSpaceId != spaceIdForRoom) {
|
||||||
setCurrentSpace(spaceIdForRoom, false);
|
setCurrentSpace(spaceIdForRoom, false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,7 +233,8 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief Show a context menu for the given event.
|
* @brief Show a context menu for the given event.
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
Q_INVOKABLE void
|
||||||
|
viewEventMenu(QObject *parent, const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set a URL to be loaded as the initial room.
|
* @brief Set a URL to be loaded as the initial room.
|
||||||
@@ -306,7 +307,8 @@ Q_SIGNALS:
|
|||||||
/**
|
/**
|
||||||
* @brief Request to show a menu for the given event.
|
* @brief Request to show a menu for the given event.
|
||||||
*/
|
*/
|
||||||
void showDelegateMenu(const QString &eventId,
|
void showDelegateMenu(QObject *parent,
|
||||||
|
const QString &eventId,
|
||||||
const NeochatRoomMember *author,
|
const NeochatRoomMember *author,
|
||||||
MessageComponentType::Type messageComponentType,
|
MessageComponentType::Type messageComponentType,
|
||||||
const QString &plainText,
|
const QString &plainText,
|
||||||
@@ -337,11 +339,6 @@ Q_SIGNALS:
|
|||||||
|
|
||||||
void currentSpaceChanged();
|
void currentSpaceChanged();
|
||||||
|
|
||||||
protected:
|
|
||||||
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
|
|
||||||
|
|
||||||
friend class LastRoomBlocker;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_isMobile = false;
|
bool m_isMobile = false;
|
||||||
|
|
||||||
@@ -387,13 +384,8 @@ private:
|
|||||||
*/
|
*/
|
||||||
QString findSpaceIdForCurrentRoom() const;
|
QString findSpaceIdForCurrentRoom() const;
|
||||||
|
|
||||||
/**
|
// Space ID, "DM", or empty string
|
||||||
* @brief Sets the current space.
|
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
|
||||||
*
|
|
||||||
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
|
|
||||||
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
|
|
||||||
*/
|
|
||||||
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Resolve a user URI.
|
* @brief Resolve a user URI.
|
||||||
|
|||||||
@@ -286,6 +286,8 @@ QQC2.Control {
|
|||||||
quickFormatBar.selectionStart = selectionStart;
|
quickFormatBar.selectionStart = selectionStart;
|
||||||
quickFormatBar.selectionEnd = selectionEnd;
|
quickFormatBar.selectionEnd = selectionEnd;
|
||||||
quickFormatBar.open();
|
quickFormatBar.open();
|
||||||
|
} else if (quickFormatBar.visible) {
|
||||||
|
quickFormatBar.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ FormCard.FormCard {
|
|||||||
|
|
||||||
onToggled: NeoChatConfig.threads = checked
|
onToggled: NeoChatConfig.threads = checked
|
||||||
}
|
}
|
||||||
FormCard.FormCheckDelegate {
|
|
||||||
text: i18nc("@option:check Enable the matrix 'secret backup' feature", "Secret Backup")
|
|
||||||
checked: NeoChatConfig.secretBackup
|
|
||||||
|
|
||||||
onToggled: NeoChatConfig.secretBackup = checked
|
|
||||||
}
|
|
||||||
FormCard.FormCheckDelegate {
|
FormCard.FormCheckDelegate {
|
||||||
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
|
text: i18nc("@option:check Enable the matrix feature to add a phone number as a third party ID", "Add phone numbers as 3PIDs")
|
||||||
checked: NeoChatConfig.phone3PId
|
checked: NeoChatConfig.phone3PId
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ target_sources(LibNeoChat PRIVATE
|
|||||||
events/imagepackevent.cpp
|
events/imagepackevent.cpp
|
||||||
events/pollevent.cpp
|
events/pollevent.cpp
|
||||||
jobs/neochatgetcommonroomsjob.cpp
|
jobs/neochatgetcommonroomsjob.cpp
|
||||||
|
jobs/neochatreportroomjob.cpp
|
||||||
|
jobs/neochatreportuserjob.cpp
|
||||||
models/actionsmodel.cpp
|
models/actionsmodel.cpp
|
||||||
models/completionmodel.cpp
|
models/completionmodel.cpp
|
||||||
models/completionproxymodel.cpp
|
models/completionproxymodel.cpp
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
#include "chatbarcache.h"
|
#include "chatbarcache.h"
|
||||||
|
|
||||||
|
#include <QMimeData>
|
||||||
|
|
||||||
|
#include <KUrlMimeData>
|
||||||
|
|
||||||
#include <Quotient/roommember.h>
|
#include <Quotient/roommember.h>
|
||||||
|
|
||||||
#include "eventhandler.h"
|
#include "eventhandler.h"
|
||||||
@@ -295,4 +299,17 @@ void ChatBarCache::clearCache()
|
|||||||
clearRelations();
|
clearRelations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatBarCache::drop(QList<QUrl> u, const QString &transferPortal)
|
||||||
|
{
|
||||||
|
QMimeData mimeData;
|
||||||
|
mimeData.setUrls(u);
|
||||||
|
if (!transferPortal.isEmpty()) {
|
||||||
|
mimeData.setData(u"application/vnd.portal.filetransfer"_s, transferPortal.toLatin1());
|
||||||
|
}
|
||||||
|
auto urls = KUrlMimeData::urlsFromMimeData(&mimeData);
|
||||||
|
if (urls.size() > 0) {
|
||||||
|
setAttachmentPath(urls[0].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_chatbarcache.cpp"
|
#include "moc_chatbarcache.cpp"
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void postMessage();
|
Q_INVOKABLE void postMessage();
|
||||||
|
|
||||||
|
Q_INVOKABLE void drop(QList<QUrl> urls, const QString &transferPortal);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textChanged();
|
void textChanged();
|
||||||
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
void relationIdChanged(const QString &oldEventId, const QString &newEventId);
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ QString PowerLevel::nameForLevel(Level level)
|
|||||||
return i18n("Moderator");
|
return i18n("Moderator");
|
||||||
case PowerLevel::Admin:
|
case PowerLevel::Admin:
|
||||||
return i18n("Admin");
|
return i18n("Admin");
|
||||||
|
case PowerLevel::Owner:
|
||||||
|
return i18nc("The person that owns a room", "Owner");
|
||||||
|
case PowerLevel::Creator:
|
||||||
|
return i18nc("The person that created a room", "Creator");
|
||||||
case PowerLevel::Mute:
|
case PowerLevel::Mute:
|
||||||
return i18n("Mute");
|
return i18n("Mute");
|
||||||
case PowerLevel::Custom:
|
case PowerLevel::Custom:
|
||||||
@@ -30,6 +34,8 @@ int PowerLevel::valueForLevel(Level level)
|
|||||||
return 50;
|
return 50;
|
||||||
case PowerLevel::Admin:
|
case PowerLevel::Admin:
|
||||||
return 100;
|
return 100;
|
||||||
|
case PowerLevel::Owner:
|
||||||
|
return 150;
|
||||||
case PowerLevel::Mute:
|
case PowerLevel::Mute:
|
||||||
return -1;
|
return -1;
|
||||||
default:
|
default:
|
||||||
@@ -46,8 +52,12 @@ PowerLevel::Level PowerLevel::levelForValue(int value)
|
|||||||
return PowerLevel::Moderator;
|
return PowerLevel::Moderator;
|
||||||
case 100:
|
case 100:
|
||||||
return PowerLevel::Admin;
|
return PowerLevel::Admin;
|
||||||
|
case 150:
|
||||||
|
return PowerLevel::Owner;
|
||||||
case -1:
|
case -1:
|
||||||
return PowerLevel::Mute;
|
return PowerLevel::Mute;
|
||||||
|
case std::numeric_limits<int>::max():
|
||||||
|
return PowerLevel::Creator;
|
||||||
default:
|
default:
|
||||||
return PowerLevel::Custom;
|
return PowerLevel::Custom;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ public:
|
|||||||
enum Level {
|
enum Level {
|
||||||
Member, /**< A basic member. */
|
Member, /**< A basic member. */
|
||||||
Moderator, /**< A moderator with enhanced powers. */
|
Moderator, /**< A moderator with enhanced powers. */
|
||||||
Admin, /**< The highest power level in the room. */
|
Admin, /**< Power level 100. */
|
||||||
|
Owner, /**< Power level 150. */
|
||||||
Mute, /**< The level to remove posting privileges. */
|
Mute, /**< The level to remove posting privileges. */
|
||||||
NUMLevels,
|
NUMLevels,
|
||||||
Custom, /**< A non-standard value. Intentionally after NUMLevels so it doesn't appear in the model. */
|
Custom, /**< A non-standard value. Intentionally after NUMLevels so it doesn't appear in the model. */
|
||||||
|
Creator, /**< The user creating the (co-)creating the room. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Level);
|
Q_ENUM(Level);
|
||||||
|
|
||||||
|
|||||||
@@ -389,9 +389,9 @@ QString EventHandler::getBody(const NeoChatRoom *room, const Quotient::RoomEvent
|
|||||||
return i18n("left the room");
|
return i18n("left the room");
|
||||||
}
|
}
|
||||||
if (const auto &reason = e.contentJson()["reason"_L1].toString().toHtmlEscaped(); !reason.isEmpty()) {
|
if (const auto &reason = e.contentJson()["reason"_L1].toString().toHtmlEscaped(); !reason.isEmpty()) {
|
||||||
return i18n("has put %1 out of the room: %2", subjectName, reason);
|
return i18n("has removed %1 from the room: %2", subjectName, reason);
|
||||||
}
|
}
|
||||||
return i18n("has put %1 out of the room", subjectName);
|
return i18n("has removed %1 from the room", subjectName);
|
||||||
case Membership::Ban:
|
case Membership::Ban:
|
||||||
if (e.senderId() != e.userId()) {
|
if (e.senderId() != e.userId()) {
|
||||||
if (e.reason().isEmpty()) {
|
if (e.reason().isEmpty()) {
|
||||||
@@ -697,6 +697,9 @@ QString EventHandler::subtitleText(const NeoChatRoom *room, const Quotient::Room
|
|||||||
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
|
qCWarning(EventHandling) << "subtitleText called with event set to nullptr.";
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
if (room->isDirectChat()) {
|
||||||
|
return plainBody(room, event, true);
|
||||||
|
}
|
||||||
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? u" "_s : u": "_s) + plainBody(room, event, true);
|
return singleLineAuthorDisplayname(room, event) + (event->isStateEvent() ? u" "_s : u": "_s) + plainBody(room, event, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
src/libneochat/jobs/neochatreportroomjob.cpp
Normal file
14
src/libneochat/jobs/neochatreportroomjob.cpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "neochatreportroomjob.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
NeochatReportRoomJob::NeochatReportRoomJob(const QString &userId, const QString &reason)
|
||||||
|
: BaseJob(HttpVerb::Post, u"ReportRoomJob"_s, makePath(" /_matrix/client/v3/", userId, "/report"))
|
||||||
|
{
|
||||||
|
QJsonObject _dataJson;
|
||||||
|
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
|
||||||
|
setRequestData({_dataJson});
|
||||||
|
}
|
||||||
13
src/libneochat/jobs/neochatreportroomjob.h
Normal file
13
src/libneochat/jobs/neochatreportroomjob.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Quotient/jobs/basejob.h>
|
||||||
|
|
||||||
|
// TODO: Remove once libQuotient updates to Matrix API v1.14
|
||||||
|
class NeochatReportRoomJob : public Quotient::BaseJob
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NeochatReportRoomJob(const QString &roomId, const QString &reason);
|
||||||
|
};
|
||||||
14
src/libneochat/jobs/neochatreportuserjob.cpp
Normal file
14
src/libneochat/jobs/neochatreportuserjob.cpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "neochatreportuserjob.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
NeochatReportUserJob::NeochatReportUserJob(const QString &userId, const QString &reason)
|
||||||
|
: BaseJob(HttpVerb::Post, u"ReportUserJob"_s, makePath("/_matrix/client/v3/users/", userId, "/report"))
|
||||||
|
{
|
||||||
|
QJsonObject _dataJson;
|
||||||
|
addParam<IfNotEmpty>(_dataJson, "reason"_L1, reason);
|
||||||
|
setRequestData({_dataJson});
|
||||||
|
}
|
||||||
13
src/libneochat/jobs/neochatreportuserjob.h
Normal file
13
src/libneochat/jobs/neochatreportuserjob.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Quotient/jobs/basejob.h>
|
||||||
|
|
||||||
|
// TODO: Remove once libQuotient updates to Matrix API v1.14
|
||||||
|
class NeochatReportUserJob : public Quotient::BaseJob
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NeochatReportUserJob(const QString &userId, const QString &reason);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
#include "jobs/neochatreportuserjob.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
#include <Quotient/csapi/profile.h>
|
#include <Quotient/csapi/profile.h>
|
||||||
#include <Quotient/csapi/registration.h>
|
#include <Quotient/csapi/registration.h>
|
||||||
#include <Quotient/csapi/versions.h>
|
#include <Quotient/csapi/versions.h>
|
||||||
|
#include <Quotient/e2ee/sssshandler.h>
|
||||||
#include <Quotient/jobs/downloadfilejob.h>
|
#include <Quotient/jobs/downloadfilejob.h>
|
||||||
#include <Quotient/qt_connection_util.h>
|
#include <Quotient/qt_connection_util.h>
|
||||||
#include <Quotient/room.h>
|
#include <Quotient/room.h>
|
||||||
@@ -140,9 +142,9 @@ void NeoChatConnection::connectSignals()
|
|||||||
this,
|
this,
|
||||||
[this] {
|
[this] {
|
||||||
callApi<GetVersionsJob>(BackgroundRequest).onResult([this](const auto &job) {
|
callApi<GetVersionsJob>(BackgroundRequest).onResult([this](const auto &job) {
|
||||||
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_L1);
|
m_canCheckMutualRooms = job->unstableFeatures().value("uk.half-shot.msc2666.query_mutual_rooms"_L1, false);
|
||||||
Q_EMIT canCheckMutualRoomsChanged();
|
Q_EMIT canCheckMutualRoomsChanged();
|
||||||
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
|
m_canEraseData = job->unstableFeatures().value("org.matrix.msc4025"_L1, false) || job->versions().count("v1.10"_L1);
|
||||||
Q_EMIT canEraseDataChanged();
|
Q_EMIT canEraseDataChanged();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -166,6 +168,10 @@ void NeoChatConnection::refreshBadgeNotificationCount()
|
|||||||
for (const auto &r : allRooms()) {
|
for (const auto &r : allRooms()) {
|
||||||
if (const auto room = static_cast<NeoChatRoom *>(r)) {
|
if (const auto room = static_cast<NeoChatRoom *>(r)) {
|
||||||
count += room->contextAwareNotificationCount();
|
count += room->contextAwareNotificationCount();
|
||||||
|
|
||||||
|
if (room->joinState() == JoinState::Invite) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,4 +582,58 @@ bool NeoChatConnection::isVerifiedSession() const
|
|||||||
return isVerifiedDevice(userId(), deviceId());
|
return isVerifiedDevice(userId(), deviceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NeoChatConnection::unlockSSSS(const QString &secret)
|
||||||
|
{
|
||||||
|
auto handler = new SSSSHandler();
|
||||||
|
handler->setConnection(this);
|
||||||
|
connect(handler, &SSSSHandler::error, this, [secret, handler, this]() {
|
||||||
|
disconnect(handler, &SSSSHandler::error, this, nullptr);
|
||||||
|
if (!secret.isEmpty()) {
|
||||||
|
connect(handler, &SSSSHandler::error, this, [handler, this]() {
|
||||||
|
Q_EMIT keyBackupError();
|
||||||
|
delete handler;
|
||||||
|
});
|
||||||
|
handler->unlockSSSSWithPassphrase(secret);
|
||||||
|
} else {
|
||||||
|
Q_EMIT keyBackupError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(handler, &SSSSHandler::keyBackupUnlocked, this, [handler, this]() {
|
||||||
|
Q_EMIT keyBackupUnlocked();
|
||||||
|
connect(handler, &SSSSHandler::finished, handler, &SSSSHandler::deleteLater);
|
||||||
|
});
|
||||||
|
if (secret.isEmpty()) {
|
||||||
|
handler->unlockSSSSFromCrossSigning();
|
||||||
|
} else {
|
||||||
|
handler->unlockSSSSFromSecurityKey(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatConnection::reportUser(const QString &userId, const QString &reason)
|
||||||
|
{
|
||||||
|
callApi<NeochatReportUserJob>(userId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NeoChatConnection::supportsMatrixSpecVersion(const QString &version)
|
||||||
|
{
|
||||||
|
return supportedMatrixSpecVersions().contains(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatConnection::noteForUser(const QString &userId)
|
||||||
|
{
|
||||||
|
const auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
|
||||||
|
return object[userId].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatConnection::setNoteForUser(const QString &userId, const QString ¬e)
|
||||||
|
{
|
||||||
|
auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
|
||||||
|
if (note.isEmpty()) {
|
||||||
|
object.remove(userId);
|
||||||
|
} else {
|
||||||
|
object[userId] = note;
|
||||||
|
}
|
||||||
|
setAccountData(QStringLiteral("org.kde.neochat.user_note"), object);
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_neochatconnection.cpp"
|
#include "moc_neochatconnection.cpp"
|
||||||
|
|||||||
@@ -222,6 +222,28 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isVerifiedSession() const;
|
bool isVerifiedSession() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void unlockSSSS(const QString &secret);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Report a user.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void reportUser(const QString &userId, const QString &reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True if this connection supports the given spec version (e.g. "v1.11").
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE bool supportsMatrixSpecVersion(const QString &version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The private note for this user, if set.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE QString noteForUser(const QString &userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the private note for this user.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setNoteForUser(const QString &userId, const QString ¬e);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void globalUrlPreviewEnabledChanged();
|
void globalUrlPreviewEnabledChanged();
|
||||||
void labelChanged();
|
void labelChanged();
|
||||||
@@ -259,6 +281,9 @@ Q_SIGNALS:
|
|||||||
*/
|
*/
|
||||||
void ownSessionVerified();
|
void ownSessionVerified();
|
||||||
|
|
||||||
|
void keyBackupUnlocked();
|
||||||
|
void keyBackupError();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool m_globalUrlPreviewDefault;
|
static bool m_globalUrlPreviewDefault;
|
||||||
static PushRuleAction::Action m_defaultAction;
|
static PushRuleAction::Action m_defaultAction;
|
||||||
|
|||||||
@@ -50,12 +50,12 @@
|
|||||||
#include "roomlastmessageprovider.h"
|
#include "roomlastmessageprovider.h"
|
||||||
#include "spacehierarchycache.h"
|
#include "spacehierarchycache.h"
|
||||||
#include "urlhelper.h"
|
#include "urlhelper.h"
|
||||||
|
#include "jobs/neochatreportroomjob.h"
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
#include <KIO/Job>
|
#include <KIO/Job>
|
||||||
#include <KIO/JobTracker>
|
#include <KIO/JobTracker>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <KJobTrackerInterface>
|
#include <KJobTrackerInterface>
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
|||||||
const auto neochatconnection = static_cast<NeoChatConnection *>(connection);
|
const auto neochatconnection = static_cast<NeoChatConnection *>(connection);
|
||||||
Q_ASSERT(neochatconnection);
|
Q_ASSERT(neochatconnection);
|
||||||
connect(neochatconnection, &NeoChatConnection::globalUrlPreviewEnabledChanged, this, &NeoChatRoom::urlPreviewEnabledChanged);
|
connect(neochatconnection, &NeoChatConnection::globalUrlPreviewEnabledChanged, this, &NeoChatRoom::urlPreviewEnabledChanged);
|
||||||
|
connect(this, &Room::fullyReadMarkerMoved, this, &NeoChatRoom::invalidateLastUnreadHighlightId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NeoChatRoom::visible() const
|
bool NeoChatRoom::visible() const
|
||||||
@@ -346,6 +347,10 @@ void NeoChatRoom::forget()
|
|||||||
|
|
||||||
void NeoChatRoom::sendTypingNotification(bool isTyping)
|
void NeoChatRoom::sendTypingNotification(bool isTyping)
|
||||||
{
|
{
|
||||||
|
// During the chatbar setup sequence, this may get called while we're still initializing
|
||||||
|
if (localMember().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
connection()->callApi<SetTypingJob>(BackgroundRequest, localMember().id(), id(), isTyping, 10000);
|
connection()->callApi<SetTypingJob>(BackgroundRequest, localMember().id(), id(), isTyping, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1695,6 +1700,11 @@ std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString
|
|||||||
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
|
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RoomEvent *NeoChatRoom::findEvent(const QString &eventId) const
|
||||||
|
{
|
||||||
|
return getEvent(eventId).first;
|
||||||
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
||||||
{
|
{
|
||||||
#if Quotient_VERSION_MINOR > 9
|
#if Quotient_VERSION_MINOR > 9
|
||||||
@@ -1843,4 +1853,57 @@ QString NeoChatRoom::pinnedMessage() const
|
|||||||
return m_pinnedMessage;
|
return m_pinnedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::report(const QString &reason)
|
||||||
|
{
|
||||||
|
connection()->callApi<NeochatReportRoomJob>(id(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::findNextUnreadHighlightId()
|
||||||
|
{
|
||||||
|
const QString startEventId = !m_lastUnreadHighlightId.isEmpty() ? m_lastUnreadHighlightId : lastFullyReadEventId();
|
||||||
|
const auto startIt = findInTimeline(startEventId);
|
||||||
|
if (startIt == historyEdge()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = startIt.base(); it != messageEvents().cend(); ++it) {
|
||||||
|
const RoomEvent *ev = it->event();
|
||||||
|
if (highlights.contains(ev)) {
|
||||||
|
m_lastUnreadHighlightId = ev->id();
|
||||||
|
Q_EMIT highlightCycleStartedChanged();
|
||||||
|
return m_lastUnreadHighlightId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_lastUnreadHighlightId.isEmpty()) {
|
||||||
|
m_lastUnreadHighlightId.clear();
|
||||||
|
Q_EMIT highlightCycleStartedChanged();
|
||||||
|
return findNextUnreadHighlightId();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NeoChatRoom::highlightCycleStarted() const
|
||||||
|
{
|
||||||
|
return !m_lastUnreadHighlightId.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoChatRoom::invalidateLastUnreadHighlightId(const QString &fromEventId, const QString &toEventId)
|
||||||
|
{
|
||||||
|
Q_UNUSED(fromEventId);
|
||||||
|
|
||||||
|
if (m_lastUnreadHighlightId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto lastIt = findInTimeline(m_lastUnreadHighlightId);
|
||||||
|
const auto newReadIt = findInTimeline(toEventId);
|
||||||
|
|
||||||
|
// opposite comparision because both are reverse iterators :p
|
||||||
|
if (newReadIt <= lastIt) {
|
||||||
|
m_lastUnreadHighlightId.clear();
|
||||||
|
Q_EMIT highlightCycleStartedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#include "moc_neochatroom.cpp"
|
#include "moc_neochatroom.cpp"
|
||||||
|
|||||||
@@ -208,6 +208,11 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(QString pinnedMessage READ pinnedMessage NOTIFY pinnedMessageChanged)
|
Q_PROPERTY(QString pinnedMessage READ pinnedMessage NOTIFY pinnedMessageChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the highlight finding cycle has started.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool highlightCycleStarted READ highlightCycleStarted NOTIFY highlightCycleStartedChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
explicit NeoChatRoom(Quotient::Connection *connection, QString roomId, Quotient::JoinState joinState = {});
|
||||||
|
|
||||||
@@ -533,6 +538,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
std::pair<const Quotient::RoomEvent *, bool> getEvent(const QString &eventId) const;
|
std::pair<const Quotient::RoomEvent *, bool> getEvent(const QString &eventId) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the event object with the given ID if available.
|
||||||
|
*
|
||||||
|
* This function works identically to getEvent, except this is usable from QML.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
|
* @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
|
||||||
*/
|
*/
|
||||||
@@ -615,13 +627,31 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief Whether this user is considered a creator of this room. Only applies to post-v12 rooms.
|
* @brief Whether this user is considered a creator of this room. Only applies to post-v12 rooms.
|
||||||
*/
|
*/
|
||||||
bool isCreator(const QString &userId) const;
|
Q_INVOKABLE bool isCreator(const QString &userId) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The most recent pinned message in the room.
|
* @return The most recent pinned message in the room.
|
||||||
*/
|
*/
|
||||||
QString pinnedMessage() const;
|
QString pinnedMessage() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a report about this room.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void report(const QString &reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the ID of the next unread highlight in the room.
|
||||||
|
*
|
||||||
|
* Each call advances the internal highlight cursor. Once the last unread highlight
|
||||||
|
* is reached, the cycle is reset.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE QString findNextUnreadHighlightId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the highlight finding cycle has started.
|
||||||
|
*/
|
||||||
|
bool highlightCycleStarted() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_visible = false;
|
bool m_visible = false;
|
||||||
|
|
||||||
@@ -657,11 +687,15 @@ private:
|
|||||||
QString m_pinnedMessage;
|
QString m_pinnedMessage;
|
||||||
void loadPinnedMessage();
|
void loadPinnedMessage();
|
||||||
|
|
||||||
|
QString m_lastUnreadHighlightId;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void updatePushNotificationState(QString type);
|
void updatePushNotificationState(QString type);
|
||||||
|
|
||||||
void cacheLastEvent();
|
void cacheLastEvent();
|
||||||
|
|
||||||
|
void invalidateLastUnreadHighlightId(const QString &fromEventId, const QString &toEventId);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void cachedInputChanged();
|
void cachedInputChanged();
|
||||||
void busyChanged();
|
void busyChanged();
|
||||||
@@ -687,6 +721,7 @@ Q_SIGNALS:
|
|||||||
void extraEventNotFound(const QString &eventId);
|
void extraEventNotFound(const QString &eventId);
|
||||||
void inviteTimestampChanged();
|
void inviteTimestampChanged();
|
||||||
void pinnedMessageChanged();
|
void pinnedMessageChanged();
|
||||||
|
void highlightCycleStartedChanged();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Request a message be shown to the user of the given type.
|
* @brief Request a message be shown to the user of the given type.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ SearchPage {
|
|||||||
*/
|
*/
|
||||||
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
|
||||||
|
|
||||||
title: i18nc("@action:title", "Explore Rooms")
|
title: i18nc("@action:title Explore public rooms and spaces", "Explore")
|
||||||
customPlaceholderText: publicRoomListModel.redirectedText
|
customPlaceholderText: publicRoomListModel.redirectedText
|
||||||
customPlaceholderIcon: "data-warning"
|
customPlaceholderIcon: "data-warning"
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ ColumnLayout {
|
|||||||
/**
|
/**
|
||||||
* @brief The canonical alias of the room, if it exists. Otherwise falls back to the first available alias.
|
* @brief The canonical alias of the room, if it exists. Otherwise falls back to the first available alias.
|
||||||
*/
|
*/
|
||||||
readonly property string roomAlias: room?.aliases[0] ?? ""
|
readonly property var roomAlias: room.aliases[0]
|
||||||
|
|
||||||
spacing: 0
|
Layout.fillWidth: true
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@@ -53,22 +53,19 @@ ColumnLayout {
|
|||||||
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
playButton.icon.name: "media-playback-start"
|
||||||
icon.name: "media-playback-start"
|
playButton.onClicked: Message.room.downloadFile(root.eventId)
|
||||||
onClicked: Message.room.downloadFile(root.eventId)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "downloading"
|
name: "downloading"
|
||||||
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: downloadBar
|
downloadBar.visible: true
|
||||||
visible: true
|
|
||||||
}
|
}
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
playButton.icon.name: "media-playback-stop"
|
||||||
icon.name: "media-playback-stop"
|
playButton.onClicked: {
|
||||||
onClicked: {
|
|
||||||
Message.room.cancelFileTransfer(root.eventId);
|
Message.room.cancelFileTransfer(root.eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,9 +74,8 @@ ColumnLayout {
|
|||||||
name: "paused"
|
name: "paused"
|
||||||
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
playButton.icon.name: "media-playback-start"
|
||||||
icon.name: "media-playback-start"
|
playButton.onClicked: {
|
||||||
onClicked: {
|
|
||||||
audio.source = root.fileTransferInfo.localPath;
|
audio.source = root.fileTransferInfo.localPath;
|
||||||
MediaManager.startPlayback();
|
MediaManager.startPlayback();
|
||||||
audio.play();
|
audio.play();
|
||||||
@@ -91,11 +87,8 @@ ColumnLayout {
|
|||||||
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: playButton
|
playButton.icon.name: "media-playback-pause"
|
||||||
|
playButton.onClicked: audio.pause()
|
||||||
icon.name: "media-playback-pause"
|
|
||||||
|
|
||||||
onClicked: audio.pause()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -121,9 +121,19 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: root.time.toString() !== "Invalid Date"
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||||
|
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
|
||||||
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedDevices: PointerDevice.TouchScreen
|
acceptedDevices: PointerDevice.TouchScreen
|
||||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
onLongPressed: {
|
||||||
|
const event = root.Message.room.findEvent(root.eventId);
|
||||||
|
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background: null
|
background: null
|
||||||
|
|||||||
@@ -73,16 +73,13 @@ ColumnLayout {
|
|||||||
when: root.fileTransferInfo.completed && autoOpenFile
|
when: root.fileTransferInfo.completed && autoOpenFile
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
openButton.icon.name: "document-open"
|
||||||
icon.name: "document-open"
|
openButton.onClicked: openSavedFile()
|
||||||
onClicked: openSavedFile()
|
downloadButton {
|
||||||
}
|
icon.name: "download"
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||||
PropertyChanges {
|
onClicked: saveFileAs()
|
||||||
target: downloadButton
|
}
|
||||||
icon.name: "download"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
|
||||||
onClicked: saveFileAs()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
@@ -90,15 +87,12 @@ ColumnLayout {
|
|||||||
when: root.fileTransferInfo.completed && !autoOpenFile
|
when: root.fileTransferInfo.completed && !autoOpenFile
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
openButton.visible: false
|
||||||
visible: false
|
downloadButton {
|
||||||
}
|
icon.name: "document-open"
|
||||||
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||||
PropertyChanges {
|
onClicked: openSavedFile()
|
||||||
target: downloadButton
|
}
|
||||||
icon.name: "document-open"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
|
||||||
onClicked: openSavedFile()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
@@ -106,19 +100,13 @@ ColumnLayout {
|
|||||||
when: root.fileTransferInfo.active
|
when: root.fileTransferInfo.active
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: openButton
|
sizeLabel.text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.total))
|
||||||
visible: false
|
openButton.visible: false
|
||||||
}
|
downloadButton {
|
||||||
|
icon.name: "media-playback-stop"
|
||||||
PropertyChanges {
|
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||||
target: sizeLabel
|
onClicked: Message.room.cancelFileTransfer(root.eventId)
|
||||||
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.total))
|
}
|
||||||
}
|
|
||||||
PropertyChanges {
|
|
||||||
target: downloadButton
|
|
||||||
icon.name: "media-playback-stop"
|
|
||||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
|
||||||
onClicked: Message.room.cancelFileTransfer(root.eventId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -47,10 +47,13 @@ Item {
|
|||||||
implicitHeight: mediaSizeHelper.currentSize.height
|
implicitHeight: mediaSizeHelper.currentSize.height
|
||||||
|
|
||||||
QQC2.Button {
|
QQC2.Button {
|
||||||
|
id: hideButton
|
||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.margins: Kirigami.Units.smallSpacing
|
anchors.margins: Kirigami.Units.smallSpacing
|
||||||
visible: !_private.hideImage
|
// For tiny images, having the button looks super buggy at their size
|
||||||
|
visible: !_private.hideImage && root.width >= hideButton.width && root.height >= hideButton.height
|
||||||
icon.name: "view-hidden"
|
icon.name: "view-hidden"
|
||||||
text: i18nc("@action:button", "Hide Image")
|
text: i18nc("@action:button", "Hide Image")
|
||||||
display: QQC2.Button.IconOnly
|
display: QQC2.Button.IconOnly
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ QQC2.Control {
|
|||||||
enabled: !quoteText.hoveredLink
|
enabled: !quoteText.hoveredLink
|
||||||
acceptedDevices: PointerDevice.TouchScreen
|
acceptedDevices: PointerDevice.TouchScreen
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
onLongPressed: {
|
||||||
|
const event = root.Message.room.findEvent(root.eventId);
|
||||||
|
RoomManager.viewEventMenu(root.QQC2.Overlay.overlay, event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ Flow {
|
|||||||
*/
|
*/
|
||||||
required property ReactionModel reactionModel
|
required property ReactionModel reactionModel
|
||||||
|
|
||||||
// HACK: We do not set Layout properties here, see BUG 504344 for the crash it caused.
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.maximumWidth: Message.maxContentWidth
|
||||||
|
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user