Compare commits
85 Commits
release/24
...
work/tobia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a917e385 | ||
|
|
39046632aa | ||
|
|
fbb2afdb49 | ||
|
|
aff0402f71 | ||
|
|
cee9058c77 | ||
|
|
3f922b4c90 | ||
|
|
02d2d31cf3 | ||
|
|
240cf6a0ed | ||
|
|
dcd9ee93de | ||
|
|
2a8cd74ab1 | ||
|
|
63bc7055c2 | ||
|
|
1cca9733d6 | ||
|
|
1104da5e2c | ||
|
|
3a9718c09d | ||
|
|
55362c5573 | ||
|
|
0bba2299b3 | ||
|
|
45685af9e9 | ||
|
|
6c416a9338 | ||
|
|
1b0027e1d2 | ||
|
|
2409adf516 | ||
|
|
554801dfe4 | ||
|
|
20c23917e9 | ||
|
|
ef953b7574 | ||
|
|
6b79795229 | ||
|
|
9cb7ec2348 | ||
|
|
437c981d30 | ||
|
|
0334cae4c8 | ||
|
|
24c405d747 | ||
|
|
a3f5962809 | ||
|
|
0deb7495f0 | ||
|
|
d34f89fc4b | ||
|
|
a909ed498f | ||
|
|
16f4e17e8f | ||
|
|
0e9592a96c | ||
|
|
704ee6a53a | ||
|
|
5b9afbce9a | ||
|
|
396cc8e8ef | ||
|
|
bf776b5c06 | ||
|
|
be319f88d3 | ||
|
|
af40d555d4 | ||
|
|
f802dbe686 | ||
|
|
2379e3d83b | ||
|
|
9e90ac0412 | ||
|
|
c27948ca3c | ||
|
|
c3b9d664df | ||
|
|
31ef0a5223 | ||
|
|
14c58acea1 | ||
|
|
5dae20603e | ||
|
|
3f6fa94289 | ||
|
|
117615a8b0 | ||
|
|
4a52773c7d | ||
|
|
edfee495c6 | ||
|
|
7d112df7c6 | ||
|
|
9acaaade45 | ||
|
|
aaca28dbf6 | ||
|
|
d4ef5f9d4d | ||
|
|
2095dea801 | ||
|
|
a36f7ef10d | ||
|
|
9874962ee3 | ||
|
|
4b08022075 | ||
|
|
dc3db3aec4 | ||
|
|
0568c2a93d | ||
|
|
7ab0a6fc9e | ||
|
|
d6b780762e | ||
|
|
5ef66b5cf6 | ||
|
|
19e8cd5e48 | ||
|
|
df5117892f | ||
|
|
aaa4216f55 | ||
|
|
85ee5084b6 | ||
|
|
bb9ce117de | ||
|
|
00c5aa26bb | ||
|
|
bae4de227c | ||
|
|
253f891c5a | ||
|
|
6966159062 | ||
|
|
07d3b80c3e | ||
|
|
a41d0f3214 | ||
|
|
1ee15de78b | ||
|
|
b044358970 | ||
|
|
d2e11bb3bb | ||
|
|
a55bac899c | ||
|
|
c2380fb8df | ||
|
|
f31c644b13 | ||
|
|
26cd621d0e | ||
|
|
4c58512c54 | ||
|
|
04c1b47660 |
@@ -7,8 +7,8 @@
|
|||||||
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 "24")
|
set(RELEASE_SERVICE_VERSION_MAJOR "25")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "11")
|
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
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}")
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ A Qt/QML based Matrix client.
|
|||||||
|
|
||||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||||
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
|
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
|
||||||
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
|
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://apps.kde.org/store_badges/snapstore/en.svg'/></a>
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,26 @@
|
|||||||
"!room_id_1234:localhost:1234": {
|
"!room_id_1234:localhost:1234": {
|
||||||
"state": {
|
"state": {
|
||||||
"events": [
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"m.federate": true,
|
||||||
|
"predecessor": {
|
||||||
|
"event_id": "$something:example.org",
|
||||||
|
"room_id": "!oldroom:example.org"
|
||||||
|
},
|
||||||
|
"room_version": "11"
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234,
|
||||||
|
"membership": "join"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "m.room.member",
|
"type": "m.room.member",
|
||||||
"state_key": "@user:localhost:1234",
|
"state_key": "@user:localhost:1234",
|
||||||
@@ -26,6 +46,26 @@
|
|||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"events": [
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"m.federate": true,
|
||||||
|
"predecessor": {
|
||||||
|
"event_id": "$something:example.org",
|
||||||
|
"room_id": "!oldroom:example.org"
|
||||||
|
},
|
||||||
|
"room_version": "11"
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234,
|
||||||
|
"membership": "join"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "m.room.message",
|
"type": "m.room.message",
|
||||||
"sender": "@user:localhost:1234",
|
"sender": "@user:localhost:1234",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
||||||
#include "enums/messagecomponenttype.h"
|
#include "enums/messagecomponenttype.h"
|
||||||
#include "models/customemojimodel.h"
|
|
||||||
#include "neochatconnection.h"
|
#include "neochatconnection.h"
|
||||||
|
|
||||||
#include "testutils.h"
|
#include "testutils.h"
|
||||||
@@ -77,7 +76,6 @@ void TextHandlerTest::initTestCase()
|
|||||||
QJsonObject{{"body"_ls, "Test custom emoji"_ls},
|
QJsonObject{{"body"_ls, "Test custom emoji"_ls},
|
||||||
{"url"_ls, "mxc://example.org/test"_ls},
|
{"url"_ls, "mxc://example.org/test"_ls},
|
||||||
{"usage"_ls, QJsonArray{"emoticon"_ls}}}}}}});
|
{"usage"_ls, QJsonArray{"emoticon"_ls}}}}}}});
|
||||||
CustomEmojiModel::instance().setConnection(static_cast<NeoChatConnection *>(connection));
|
|
||||||
|
|
||||||
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-texthandler-sync.json"));
|
room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-texthandler-sync.json"));
|
||||||
}
|
}
|
||||||
@@ -535,7 +533,7 @@ void TextHandlerTest::componentOutput_data()
|
|||||||
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
|
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
|
||||||
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
|
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
|
||||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||||
MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}};
|
MessageComponent{MessageComponentType::Quote, QStringLiteral("“blockquote”"), {}}};
|
||||||
QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>")
|
QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n<p>Text</p>")
|
||||||
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
|
||||||
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
<summary xml:lang="es">Charle en Matrix</summary>
|
<summary xml:lang="es">Charle en Matrix</summary>
|
||||||
<summary xml:lang="eu">Berriketa Matrix-en</summary>
|
<summary xml:lang="eu">Berriketa Matrix-en</summary>
|
||||||
<summary xml:lang="fr">Discuter sur Matrix</summary>
|
<summary xml:lang="fr">Discuter sur Matrix</summary>
|
||||||
|
<summary xml:lang="gl">Charlar en Matrix</summary>
|
||||||
|
<summary xml:lang="hu">Csevegés Matrixon</summary>
|
||||||
<summary xml:lang="ia">Conversation en ditecto sur Matrix</summary>
|
<summary xml:lang="ia">Conversation en ditecto sur Matrix</summary>
|
||||||
<summary xml:lang="it">Chat su Matrix</summary>
|
<summary xml:lang="it">Chat su Matrix</summary>
|
||||||
<summary xml:lang="ka">ისაუბრეთ Matrix-ზე</summary>
|
<summary xml:lang="ka">ისაუბრეთ Matrix-ზე</summary>
|
||||||
@@ -286,6 +288,7 @@
|
|||||||
<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">Tanguy Fardet</value>
|
||||||
</custom>
|
</custom>
|
||||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
|
|||||||
@@ -87,47 +87,25 @@ GenericName[uk]=Клієнт Matrix
|
|||||||
GenericName[x-test]=xxMatrix Clientxx
|
GenericName[x-test]=xxMatrix Clientxx
|
||||||
GenericName[zh_CN]=Matrix 客户端
|
GenericName[zh_CN]=Matrix 客户端
|
||||||
GenericName[zh_TW]=Matrix 用戶端
|
GenericName[zh_TW]=Matrix 用戶端
|
||||||
Comment=Client for the Matrix protocol
|
Comment=Chat on Matrix
|
||||||
Comment[ar]=عميل لميفاق ماتركس
|
Comment[ca]=Xat a Matrix
|
||||||
Comment[az]=Matrix protokolu üçün müştəri
|
Comment[ca@valencia]=Xat a Matrix
|
||||||
Comment[ca]=Client per al protocol Matrix
|
Comment[en_GB]=Chat on Matrix
|
||||||
Comment[ca@valencia]=Client per al protocol Matrix
|
Comment[es]=Chat en Matrix
|
||||||
Comment[de]=Programm für das Matrix-Protokoll
|
Comment[eu]=Berriketa Matrix-en
|
||||||
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
|
Comment[fr]=Clavarder sur Matrix
|
||||||
Comment[en_GB]=Client for the Matrix protocol
|
Comment[gl]=Charle en Matrix
|
||||||
Comment[eo]=Kliento por la Matrix-protokolo
|
Comment[hu]=Csevegés Matrixon
|
||||||
Comment[es]=Cliente para el protocolo Matrix
|
Comment[ia]=Conversation en ditecto sur Matrix
|
||||||
Comment[eu]=Matrix protokolorako bezeroa
|
Comment[it]= su Matrix
|
||||||
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
|
Comment[ka]=ჩატი Matrix-ზე
|
||||||
Comment[fr]=Client pour le protocole « Matrix »
|
Comment[nl]=Chat op Matrix
|
||||||
Comment[gl]=Cliente para o protocolo Matrix.
|
Comment[pl]=Rozmawiaj na Matriksie
|
||||||
Comment[he]=לקוח לפרוטוקול Matrix
|
Comment[sl]=Klepet na Matrixu
|
||||||
Comment[hu]=Kliens a Matrix protokollhoz
|
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
|
||||||
Comment[ia]=Cliente per le protocollo de Matrix
|
Comment[tr]=Matrix Üzerinde Sohbet Et
|
||||||
Comment[id]=Klien untuk protokol Matrix
|
Comment[uk]=Спілкування у Matrix
|
||||||
Comment[ie]=Un cliente del protocol Matrix
|
Comment[x-test]=xxChat on Matrixxx
|
||||||
Comment[it]=Client per il protocollo Matrix
|
|
||||||
Comment[ka]=კლიენტი Matrix-ის პროტოკოლისთვის
|
|
||||||
Comment[ko]=Matrix 프로토콜용 클라이언트
|
|
||||||
Comment[lt]=Matrix protokolo kliento programa
|
|
||||||
Comment[lv]=Klients „Matrix“ protokolam
|
|
||||||
Comment[nl]=Client voor het Matrix-protocol
|
|
||||||
Comment[nn]=Klient for Matrix-protokollen
|
|
||||||
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
|
|
||||||
Comment[pl]=Program obsługi protokołu Matriksa
|
|
||||||
Comment[pt]=Cliente para o protocolo Matrix
|
|
||||||
Comment[pt_BR]=Cliente para o protocolo Matrix
|
|
||||||
Comment[ro]=Client pentru protocolul Matrix
|
|
||||||
Comment[ru]=Клиент для протокола Matrix
|
|
||||||
Comment[sk]=Klient protokolu Matrix
|
|
||||||
Comment[sl]=Odjemalec za protokol Matrix
|
|
||||||
Comment[sv]=Klient för protokollet Matrix
|
|
||||||
Comment[ta]=Matrix நெறிமுறைக்கான வாங்கி
|
|
||||||
Comment[tr]=Matrix protokolü için istemci
|
|
||||||
Comment[uk]=Клієнт протоколу Matrix
|
|
||||||
Comment[x-test]=xxClient for the Matrix protocolxx
|
|
||||||
Comment[zh_CN]=为 Matrix 协议打造的客户端
|
|
||||||
Comment[zh_TW]=Matrix 通訊協定的用戶端
|
|
||||||
MimeType=x-scheme-handler/matrix;
|
MimeType=x-scheme-handler/matrix;
|
||||||
Exec=neochat %u
|
Exec=neochat %u
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
577
po/ar/neochat.po
577
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
571
po/az/neochat.po
571
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
554
po/ca/neochat.po
554
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
575
po/cs/neochat.po
575
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
562
po/da/neochat.po
562
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
577
po/de/neochat.po
577
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
576
po/el/neochat.po
576
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
579
po/eo/neochat.po
579
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
576
po/es/neochat.po
576
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
572
po/eu/neochat.po
572
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
578
po/fi/neochat.po
578
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
588
po/fr/neochat.po
588
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
641
po/gl/neochat.po
641
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
644
po/hu/neochat.po
644
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
583
po/ia/neochat.po
583
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
581
po/id/neochat.po
581
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
569
po/ie/neochat.po
569
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
587
po/it/neochat.po
587
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
522
po/ja/neochat.po
522
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
554
po/ka/neochat.po
554
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
580
po/ko/neochat.po
580
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
522
po/lt/neochat.po
522
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
577
po/lv/neochat.po
577
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
556
po/nl/neochat.po
556
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
576
po/nn/neochat.po
576
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
571
po/pa/neochat.po
571
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
596
po/pl/neochat.po
596
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
581
po/pt/neochat.po
581
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
573
po/ru/neochat.po
573
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1176
po/sk/neochat.po
1176
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
554
po/sl/neochat.po
554
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
577
po/sv/neochat.po
577
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
592
po/ta/neochat.po
592
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
585
po/tr/neochat.po
585
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
558
po/uk/neochat.po
558
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,6 @@ endif()
|
|||||||
add_library(neochat STATIC
|
add_library(neochat STATIC
|
||||||
controller.cpp
|
controller.cpp
|
||||||
controller.h
|
controller.h
|
||||||
models/emojimodel.cpp
|
|
||||||
models/emojimodel.h
|
|
||||||
emojitones.cpp
|
|
||||||
emojitones.h
|
|
||||||
models/customemojimodel.cpp
|
|
||||||
models/customemojimodel.h
|
|
||||||
clipboard.cpp
|
clipboard.cpp
|
||||||
clipboard.h
|
clipboard.h
|
||||||
models/messageeventmodel.cpp
|
models/messageeventmodel.cpp
|
||||||
@@ -26,8 +20,6 @@ add_library(neochat STATIC
|
|||||||
models/roomlistmodel.h
|
models/roomlistmodel.h
|
||||||
models/sortfilterspacelistmodel.cpp
|
models/sortfilterspacelistmodel.cpp
|
||||||
models/sortfilterspacelistmodel.h
|
models/sortfilterspacelistmodel.h
|
||||||
models/accountemoticonmodel.cpp
|
|
||||||
models/accountemoticonmodel.h
|
|
||||||
spacehierarchycache.cpp
|
spacehierarchycache.cpp
|
||||||
spacehierarchycache.h
|
spacehierarchycache.h
|
||||||
roommanager.cpp
|
roommanager.cpp
|
||||||
@@ -50,8 +42,6 @@ add_library(neochat STATIC
|
|||||||
models/userdirectorylistmodel.h
|
models/userdirectorylistmodel.h
|
||||||
models/pushrulemodel.cpp
|
models/pushrulemodel.cpp
|
||||||
models/pushrulemodel.h
|
models/pushrulemodel.h
|
||||||
models/emoticonfiltermodel.cpp
|
|
||||||
models/emoticonfiltermodel.h
|
|
||||||
notificationsmanager.cpp
|
notificationsmanager.cpp
|
||||||
notificationsmanager.h
|
notificationsmanager.h
|
||||||
models/sortfilterroomlistmodel.cpp
|
models/sortfilterroomlistmodel.cpp
|
||||||
@@ -101,10 +91,6 @@ add_library(neochat STATIC
|
|||||||
texthandler.h
|
texthandler.h
|
||||||
logger.cpp
|
logger.cpp
|
||||||
logger.h
|
logger.h
|
||||||
models/stickermodel.cpp
|
|
||||||
models/stickermodel.h
|
|
||||||
models/imagepacksmodel.cpp
|
|
||||||
models/imagepacksmodel.h
|
|
||||||
events/imagepackevent.cpp
|
events/imagepackevent.cpp
|
||||||
events/imagepackevent.h
|
events/imagepackevent.h
|
||||||
events/joinrulesevent.cpp
|
events/joinrulesevent.cpp
|
||||||
@@ -282,6 +268,9 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
qml/ConfirmLeaveDialog.qml
|
qml/ConfirmLeaveDialog.qml
|
||||||
qml/CodeMaximizeComponent.qml
|
qml/CodeMaximizeComponent.qml
|
||||||
qml/EditStateDialog.qml
|
qml/EditStateDialog.qml
|
||||||
|
qml/EmojiPickerTypeHeader.qml
|
||||||
|
qml/EmojiPickerPackHeader.qml
|
||||||
|
qml/QuickReaction.qml
|
||||||
qml/ConsentDialog.qml
|
qml/ConsentDialog.qml
|
||||||
qml/AskDirectChatConfirmation.qml
|
qml/AskDirectChatConfirmation.qml
|
||||||
qml/HoverLinkIndicator.qml
|
qml/HoverLinkIndicator.qml
|
||||||
|
|||||||
@@ -176,13 +176,14 @@ QQC2.Control {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
QQC2.ScrollView {
|
QQC2.ScrollView {
|
||||||
id: chatBarScrollView
|
id: chatBarScrollView
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
||||||
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 3
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
|
||||||
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
|
|
||||||
|
|
||||||
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
// HACK: This is to stop the ScrollBar flickering on and off as the height is increased
|
||||||
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
|
||||||
@@ -320,12 +321,11 @@ QQC2.Control {
|
|||||||
id: actionsRow
|
id: actionsRow
|
||||||
spacing: 0
|
spacing: 0
|
||||||
Layout.alignment: Qt.AlignBottom
|
Layout.alignment: Qt.AlignBottom
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
|
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.actions
|
model: root.actions
|
||||||
delegate: QQC2.ToolButton {
|
delegate: QQC2.ToolButton {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||||
onClicked: modelData.trigger()
|
onClicked: modelData.trigger()
|
||||||
|
|
||||||
@@ -342,7 +342,6 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateSizeHelper {
|
DelegateSizeHelper {
|
||||||
id: chatBarSizeHelper
|
id: chatBarSizeHelper
|
||||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||||
@@ -520,7 +519,6 @@ QQC2.Control {
|
|||||||
y: -implicitHeight
|
y: -implicitHeight
|
||||||
|
|
||||||
modal: false
|
modal: false
|
||||||
includeCustom: true
|
|
||||||
closeOnChosen: false
|
closeOnChosen: false
|
||||||
|
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
|
|||||||
@@ -5,56 +5,30 @@ import QtQuick
|
|||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
QQC2.ItemDelegate {
|
QQC2.Button {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string name
|
required property string toolTip
|
||||||
property string emoji
|
|
||||||
property bool showTones: false
|
property bool showTones: false
|
||||||
property bool isImage: false
|
|
||||||
|
|
||||||
QQC2.ToolTip.text: root.name
|
QQC2.ToolTip.text: toolTip
|
||||||
QQC2.ToolTip.visible: hovered && root.name !== ""
|
QQC2.ToolTip.visible: hovered
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
leftInset: Kirigami.Units.smallSpacing
|
|
||||||
topInset: Kirigami.Units.smallSpacing
|
|
||||||
rightInset: Kirigami.Units.smallSpacing
|
|
||||||
bottomInset: Kirigami.Units.smallSpacing
|
|
||||||
|
|
||||||
contentItem: Item {
|
flat: true
|
||||||
Kirigami.Heading {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !root.emoji.startsWith("mxc") && !root.isImage
|
|
||||||
text: root.emoji
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
font.family: "emoji"
|
|
||||||
|
|
||||||
Kirigami.Icon {
|
contentItem: Kirigami.Heading {
|
||||||
width: Kirigami.Units.gridUnit * 0.5
|
text: root.text
|
||||||
height: Kirigami.Units.gridUnit * 0.5
|
horizontalAlignment: Text.AlignHCenter
|
||||||
source: "arrow-down-symbolic"
|
verticalAlignment: Text.AlignVCenter
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
visible: root.showTones
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.emoji.startsWith("mxc") || root.isImage
|
|
||||||
source: visible ? root.emoji : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
Kirigami.Icon {
|
||||||
color: root.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
|
width: Kirigami.Units.gridUnit * 0.5
|
||||||
radius: Kirigami.Units.cornerRadius
|
height: Kirigami.Units.gridUnit * 0.5
|
||||||
|
source: "arrow-down-symbolic"
|
||||||
Rectangle {
|
anchors.bottom: parent.bottom
|
||||||
radius: Kirigami.Units.cornerRadius
|
anchors.right: parent.right
|
||||||
anchors.fill: parent
|
visible: root.showTones
|
||||||
color: Kirigami.Theme.highlightColor
|
|
||||||
opacity: root.hovered && !root.pressed ? 0.2 : 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ QQC2.Popup {
|
|||||||
*/
|
*/
|
||||||
property NeoChatRoom currentRoom
|
property NeoChatRoom currentRoom
|
||||||
|
|
||||||
property bool includeCustom: false
|
|
||||||
property bool closeOnChosen: true
|
property bool closeOnChosen: true
|
||||||
property bool showQuickReaction: false
|
|
||||||
|
|
||||||
signal chosen(string emoji)
|
signal chosen(string emoji)
|
||||||
|
|
||||||
@@ -64,15 +62,15 @@ QQC2.Popup {
|
|||||||
padding: 2
|
padding: 2
|
||||||
|
|
||||||
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
|
implicitHeight: Kirigami.Units.gridUnit * 20 + 2 * padding
|
||||||
width: Math.min(contentItem.categoryIconSize * 11 + 2 * padding, applicationWindow().width)
|
width: Math.min(contentItem.implicitWidth + 2 * padding, applicationWindow().width)
|
||||||
|
|
||||||
contentItem: EmojiPicker {
|
contentItem: EmojiPicker {
|
||||||
id: emojiPicker
|
id: emojiPicker
|
||||||
height: 400
|
height: 400
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
includeCustom: root.includeCustom
|
|
||||||
showQuickReaction: root.showQuickReaction
|
|
||||||
onChosen: emoji => {
|
onChosen: emoji => {
|
||||||
root.chosen(emoji);
|
root.chosen(emoji);
|
||||||
|
ImageContentManager.emojiUsed(emoji)
|
||||||
if (root.closeOnChosen) {
|
if (root.closeOnChosen) {
|
||||||
root.close();
|
root.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella
|
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
import org.kde.textaddons.emoticons
|
||||||
|
|
||||||
QQC2.ScrollView {
|
QQC2.ScrollView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias model: emojis.model
|
readonly property int emojisPerRow: emojis.width / Kirigami.Units.iconSizes.large
|
||||||
property alias count: emojis.count
|
|
||||||
required property int targetIconSize
|
|
||||||
readonly property int emojisPerRow: emojis.width / targetIconSize
|
|
||||||
required property bool withCustom
|
|
||||||
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
|
|
||||||
required property QtObject header
|
required property QtObject header
|
||||||
property bool stickers: false
|
property bool stickers: false
|
||||||
|
|
||||||
@@ -25,6 +22,8 @@ QQC2.ScrollView {
|
|||||||
emojis.forceActiveFocus();
|
emojis.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
width: Kirigami.Units.gridUnit * 24
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
id: emojis
|
id: emojis
|
||||||
|
|
||||||
@@ -41,7 +40,9 @@ QQC2.ScrollView {
|
|||||||
onModelChanged: currentIndex = -1
|
onModelChanged: currentIndex = -1
|
||||||
|
|
||||||
cellWidth: emojis.width / root.emojisPerRow
|
cellWidth: emojis.width / root.emojisPerRow
|
||||||
cellHeight: root.targetIconSize
|
cellHeight: Kirigami.Units.iconSizes.large
|
||||||
|
|
||||||
|
model: EmojiModelManager.emojiModel
|
||||||
|
|
||||||
KeyNavigation.up: root.header
|
KeyNavigation.up: root.header
|
||||||
|
|
||||||
@@ -49,49 +50,49 @@ QQC2.ScrollView {
|
|||||||
|
|
||||||
delegate: EmojiDelegate {
|
delegate: EmojiDelegate {
|
||||||
id: emojiDelegate
|
id: emojiDelegate
|
||||||
checked: emojis.currentIndex === model.index
|
|
||||||
emoji: !!modelData ? modelData.unicode : model.url
|
required property string unicode
|
||||||
name: !!modelData ? modelData.shortName : model.body
|
required property string identifier
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
text: emojiDelegate.unicode
|
||||||
|
toolTip: emojiDelegate.identifier
|
||||||
|
checked: emojis.currentIndex === emojiDelegate.index
|
||||||
|
|
||||||
width: emojis.cellWidth
|
width: emojis.cellWidth
|
||||||
height: emojis.cellHeight
|
height: emojis.cellHeight
|
||||||
|
|
||||||
isImage: root.stickers
|
|
||||||
Keys.onEnterPressed: clicked()
|
Keys.onEnterPressed: clicked()
|
||||||
Keys.onReturnPressed: clicked()
|
Keys.onReturnPressed: clicked()
|
||||||
onClicked: {
|
// onClicked: {
|
||||||
if (root.stickers) {
|
// if (root.stickers) {
|
||||||
root.stickerChosen(model.index);
|
// root.stickerChosen(model.index);
|
||||||
}
|
// }
|
||||||
root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
|
// root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
|
||||||
EmojiModel.emojiUsed(modelData);
|
// EmojiModel.emojiUsed(modelData);
|
||||||
}
|
// }
|
||||||
Keys.onSpacePressed: pressAndHold()
|
// Keys.onSpacePressed: pressAndHold()
|
||||||
onPressAndHold: {
|
// onPressAndHold: {
|
||||||
if (EmojiModel.tones(modelData.shortName).length === 0) {
|
// if (!showTones) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
let tones = tonesPopupComponent.createObject(emojiDelegate, {
|
// let tones = Qt.createComponent("org.kde.neochat", "EmojiTonesPicker").createObject(emojiDelegate, {
|
||||||
shortName: modelData.shortName,
|
// shortName: modelData.shortName,
|
||||||
unicode: modelData.unicode,
|
// unicode: modelData.unicode,
|
||||||
categoryIconSize: root.targetIconSize
|
// categoryIconSize: root.targetIconSize,
|
||||||
});
|
// onChosen: root.chosen(emoji => root.chosen(emoji))
|
||||||
tones.open();
|
// });
|
||||||
tones.forceActiveFocus();
|
// tones.open();
|
||||||
}
|
// tones.forceActiveFocus();
|
||||||
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
|
// }
|
||||||
|
// showTones: model.hasTones
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
|
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
|
||||||
|
text: root.stickers ? i18nc("@info", "No stickers") : i18nc("@info", "No emojis")
|
||||||
visible: emojis.count === 0
|
visible: emojis.count === 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
|
||||||
id: tonesPopupComponent
|
|
||||||
EmojiTonesPicker {
|
|
||||||
onChosen: root.chosen(emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2022-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
import org.kde.textaddons.emoticons
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
@@ -13,86 +14,29 @@ ColumnLayout {
|
|||||||
/**
|
/**
|
||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
|
|
||||||
property bool includeCustom: false
|
|
||||||
property bool showQuickReaction: false
|
|
||||||
|
|
||||||
readonly property var currentEmojiModel: {
|
|
||||||
if (includeCustom) {
|
|
||||||
EmojiModel.categoriesWithCustom;
|
|
||||||
} else {
|
|
||||||
EmojiModel.categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int categoryIconSize: Math.round(Kirigami.Units.gridUnit * 2.5)
|
|
||||||
readonly property var currentCategory: currentEmojiModel[categories.currentIndex].category
|
|
||||||
readonly property alias categoryCount: categories.count
|
|
||||||
property int selectedType: 0
|
|
||||||
|
|
||||||
signal chosen(string emoji)
|
signal chosen(string emoji)
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
onActiveFocusChanged: if (activeFocus) {
|
onActiveFocusChanged: if (activeFocus) {
|
||||||
searchField.forceActiveFocus();
|
searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
spacing: 0
|
EmojiPickerTypeHeader {
|
||||||
|
id: emoticonPickerTypeHeader
|
||||||
|
|
||||||
Kirigami.NavigationTabBar {
|
|
||||||
id: types
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
onSelectedTypeChanged: emoticonPickerCategoryHeader.currentIndex = 0
|
||||||
|
|
||||||
background: null
|
|
||||||
actions: [
|
|
||||||
Kirigami.Action {
|
|
||||||
id: emojis
|
|
||||||
icon.name: "smiley"
|
|
||||||
text: i18n("Emojis")
|
|
||||||
checked: true
|
|
||||||
onTriggered: root.selectedType = 0
|
|
||||||
},
|
|
||||||
Kirigami.Action {
|
|
||||||
id: stickers
|
|
||||||
icon.name: "stickers"
|
|
||||||
text: i18n("Stickers")
|
|
||||||
onTriggered: root.selectedType = 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ScrollView {
|
EmojiPickerPackHeader {
|
||||||
|
id: emoticonPickerCategoryHeader
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
|
|
||||||
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
|
|
||||||
|
|
||||||
ListView {
|
model: UnicodeEmoticonManager.categories
|
||||||
id: categories
|
|
||||||
clip: true
|
|
||||||
focus: true
|
|
||||||
orientation: ListView.Horizontal
|
|
||||||
|
|
||||||
Keys.onReturnPressed: if (emojiGrid.count > 0) {
|
|
||||||
emojiGrid.focus = true;
|
|
||||||
}
|
|
||||||
Keys.onEnterPressed: if (emojiGrid.count > 0) {
|
|
||||||
emojiGrid.focus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
|
|
||||||
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
|
|
||||||
|
|
||||||
keyNavigationEnabled: true
|
|
||||||
keyNavigationWraps: true
|
|
||||||
Keys.forwardTo: searchField
|
|
||||||
interactive: width !== contentWidth
|
|
||||||
|
|
||||||
model: root.selectedType === 0 ? root.currentEmojiModel : stickerPackModel
|
|
||||||
Component.onCompleted: categories.forceActiveFocus()
|
|
||||||
|
|
||||||
delegate: root.selectedType === 0 ? emojiDelegate : stickerDelegate
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
@@ -104,114 +48,34 @@ ColumnLayout {
|
|||||||
id: searchField
|
id: searchField
|
||||||
Layout.margins: Kirigami.Units.smallSpacing
|
Layout.margins: Kirigami.Units.smallSpacing
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: selectedType === 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The focus is manged by the parent and we don't want to use the standard
|
|
||||||
* shortcut as it could block other SearchFields from using it.
|
|
||||||
*/
|
|
||||||
focusSequence: ""
|
focusSequence: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiGrid {
|
EmojiGrid {
|
||||||
id: emojiGrid
|
id: emojiGrid
|
||||||
targetIconSize: root.currentCategory === EmojiModel.Custom ? Kirigami.Units.gridUnit * 3 : root.categoryIconSize // Custom emojis are bigger
|
|
||||||
model: root.selectedType === 1 ? emoticonFilterModel : searchField.text.length === 0 ? EmojiModel.emojis(root.currentCategory) : (root.includeCustom ? EmojiModel.filterModel(searchField.text, false) : EmojiModel.filterModelNoCustom(searchField.text, false))
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
withCustom: root.includeCustom
|
|
||||||
onChosen: unicode => root.chosen(unicode)
|
onChosen: unicode => root.chosen(unicode)
|
||||||
header: categories
|
header: emoticonPickerCategoryHeader
|
||||||
Keys.forwardTo: searchField
|
Keys.forwardTo: searchField
|
||||||
stickers: root.selectedType === 1
|
stickers: emoticonPickerTypeHeader.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
|
||||||
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
|
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.Separator {
|
Kirigami.Separator {
|
||||||
visible: showQuickReaction
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 1
|
Layout.preferredHeight: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.ScrollView {
|
QuickReaction {
|
||||||
visible: showQuickReaction
|
id: quickReaction
|
||||||
|
onChosen: root.chosen(text)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
|
|
||||||
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: quickReactions
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
|
|
||||||
|
|
||||||
delegate: EmojiDelegate {
|
|
||||||
emoji: modelData
|
|
||||||
|
|
||||||
height: root.categoryIconSize
|
|
||||||
width: height
|
|
||||||
|
|
||||||
onClicked: root.chosen(modelData)
|
|
||||||
}
|
|
||||||
|
|
||||||
orientation: Qt.Horizontal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImagePacksModel {
|
|
||||||
id: stickerPackModel
|
|
||||||
room: root.currentRoom
|
|
||||||
showStickers: true
|
|
||||||
showEmoticons: false
|
|
||||||
}
|
|
||||||
|
|
||||||
StickerModel {
|
|
||||||
id: stickerModel
|
|
||||||
model: stickerPackModel
|
|
||||||
packIndex: 0
|
|
||||||
room: root.currentRoom
|
|
||||||
}
|
|
||||||
|
|
||||||
EmoticonFilterModel {
|
|
||||||
id: emoticonFilterModel
|
|
||||||
sourceModel: stickerModel
|
|
||||||
showStickers: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: emojiDelegate
|
|
||||||
Kirigami.NavigationTabButton {
|
|
||||||
width: root.categoryIconSize
|
|
||||||
height: width
|
|
||||||
checked: categories.currentIndex === model.index
|
|
||||||
text: modelData ? modelData.emoji : ""
|
|
||||||
QQC2.ToolTip.text: modelData ? modelData.name : ""
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
QQC2.ToolTip.visible: hovered
|
|
||||||
onClicked: {
|
|
||||||
categories.currentIndex = index;
|
|
||||||
categories.focus = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: stickerDelegate
|
|
||||||
Kirigami.NavigationTabButton {
|
|
||||||
width: root.categoryIconSize
|
|
||||||
height: width
|
|
||||||
checked: stickerModel.packIndex === model.index
|
|
||||||
contentItem: Image {
|
|
||||||
source: model.avatarUrl
|
|
||||||
}
|
|
||||||
QQC2.ToolTip.text: model.name
|
|
||||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
||||||
QQC2.ToolTip.visible: hovered && !!model.name
|
|
||||||
onClicked: stickerModel.packIndex = model.index
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearchField() {
|
function clearSearchField() {
|
||||||
searchField.text = "";
|
searchField.text = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -54,8 +54,8 @@ QQC2.Popup {
|
|||||||
delegate: EmojiDelegate {
|
delegate: EmojiDelegate {
|
||||||
id: emojiDelegate
|
id: emojiDelegate
|
||||||
checked: tonesList.currentIndex === model.index
|
checked: tonesList.currentIndex === model.index
|
||||||
emoji: modelData.unicode
|
text: modelData.unicode
|
||||||
name: modelData.shortName
|
toolTip: modelData.shortName
|
||||||
|
|
||||||
width: root.categoryIconSize
|
width: root.categoryIconSize
|
||||||
height: width
|
height: width
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
@@ -23,7 +24,7 @@ ColumnLayout {
|
|||||||
model: root.connection.accountDataEventTypes
|
model: root.connection.accountDataEventTypes
|
||||||
delegate: FormCard.FormButtonDelegate {
|
delegate: FormCard.FormButtonDelegate {
|
||||||
text: modelData
|
text: modelData
|
||||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||||
sourceText: root.connection.accountDataJsonString(modelData)
|
sourceText: root.connection.accountDataJsonString(modelData)
|
||||||
}, {
|
}, {
|
||||||
title: i18nc("@title:window", "Event Source"),
|
title: i18nc("@title:window", "Event Source"),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
@@ -47,7 +48,7 @@ ColumnLayout {
|
|||||||
model: root.room.accountDataEventTypes
|
model: root.room.accountDataEventTypes
|
||||||
delegate: FormCard.FormButtonDelegate {
|
delegate: FormCard.FormButtonDelegate {
|
||||||
text: modelData
|
text: modelData
|
||||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||||
sourceText: root.room.roomAcountDataJson(text)
|
sourceText: root.room.roomAcountDataJson(text)
|
||||||
}, {
|
}, {
|
||||||
title: i18n("Event Source"),
|
title: i18n("Event Source"),
|
||||||
@@ -77,7 +78,7 @@ ColumnLayout {
|
|||||||
if (model.eventCount === 1) {
|
if (model.eventCount === 1) {
|
||||||
openEventSource(model.type, model.stateKey);
|
openEventSource(model.type, model.stateKey);
|
||||||
} else {
|
} else {
|
||||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
|
root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat.devtools', 'StateKeys'), {
|
||||||
room: root.room,
|
room: root.room,
|
||||||
eventType: model.type
|
eventType: model.type
|
||||||
}, {
|
}, {
|
||||||
@@ -89,7 +90,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function openEventSource(type: string, stateKey: string): void {
|
function openEventSource(type: string, stateKey: string): void {
|
||||||
onClicked: applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
onClicked: root.Window.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'MessageSourceSheet'), {
|
||||||
model: stateModel,
|
model: stateModel,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
room: root.room,
|
room: root.room,
|
||||||
|
|||||||
1857
src/emojis.h
1857
src/emojis.h
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: None
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "emojitones.h"
|
|
||||||
#include "models/emojimodel.h"
|
|
||||||
|
|
||||||
QMultiHash<QString, QVariant> EmojiTones::_tones = {
|
|
||||||
#include "emojitones_data.h"
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: None
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class EmojiTones
|
|
||||||
*
|
|
||||||
* This class provides a _tones variable with the available emoji tones to EmojiModel.
|
|
||||||
*
|
|
||||||
* @sa EmojiModel
|
|
||||||
*/
|
|
||||||
class EmojiTones
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
static QMultiHash<QString, QVariant> _tones;
|
|
||||||
|
|
||||||
friend class EmojiModel;
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ LoginStep {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
FormCard.FormTextDelegate {
|
FormCard.FormTextDelegate {
|
||||||
|
textItem.wrapMode: Text.Wrap
|
||||||
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
|
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
|
||||||
}
|
}
|
||||||
FormCard.AbstractFormDelegate {
|
FormCard.AbstractFormDelegate {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import QtQuick.Layouts
|
|||||||
|
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
import org.kde.neochat.settings
|
import org.kde.neochat.settings
|
||||||
@@ -90,11 +91,27 @@ Kirigami.Page {
|
|||||||
id: loadedAccounts
|
id: loadedAccounts
|
||||||
model: AccountRegistry
|
model: AccountRegistry
|
||||||
delegate: FormCard.FormButtonDelegate {
|
delegate: FormCard.FormButtonDelegate {
|
||||||
text: model.userId
|
id: delegate
|
||||||
|
|
||||||
|
required property string userId
|
||||||
|
required property NeoChatConnection connection
|
||||||
|
|
||||||
|
text: QmlUtils.escapeString(connection.localUser.displayName)
|
||||||
|
description: connection.localUser.id
|
||||||
|
leadingPadding: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Controller.activeConnection = model.connection;
|
Controller.activeConnection = delegate.connection;
|
||||||
root.connectionChosen();
|
root.connectionChosen();
|
||||||
}
|
}
|
||||||
|
leading: KirigamiComponents.Avatar {
|
||||||
|
id: avatar
|
||||||
|
name: delegate.text
|
||||||
|
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
|
||||||
|
source: delegate.connection.localUser.avatarUrl.toString().length > 0 ? delegate.connection.makeMediaUrl(delegate.connection.localUser.avatarUrl) : ""
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ int main(int argc, char *argv[])
|
|||||||
KAboutData about(QStringLiteral("neochat"),
|
KAboutData about(QStringLiteral("neochat"),
|
||||||
i18n("NeoChat"),
|
i18n("NeoChat"),
|
||||||
QStringLiteral(NEOCHAT_VERSION_STRING),
|
QStringLiteral(NEOCHAT_VERSION_STRING),
|
||||||
i18n("Matrix client"),
|
i18n("Chat on Matrix"),
|
||||||
KAboutLicense::GPL_V3,
|
KAboutLicense::GPL_V3,
|
||||||
i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
|
i18n("© 2018-2020 Black Hat, 2020-2024 KDE Community"));
|
||||||
about.addAuthor(i18n("Carl Schwan"),
|
about.addAuthor(i18n("Carl Schwan"),
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "accountemoticonmodel.h"
|
|
||||||
|
|
||||||
#include <QImage>
|
|
||||||
#include <QMimeDatabase>
|
|
||||||
|
|
||||||
#include <Quotient/csapi/content-repo.h>
|
|
||||||
#include <Quotient/events/eventcontent.h>
|
|
||||||
#include <qcoro/qcorosignal.h>
|
|
||||||
|
|
||||||
#include "neochatconnection.h"
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
AccountEmoticonModel::AccountEmoticonModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
int AccountEmoticonModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
if (!m_images) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return m_images->images.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant AccountEmoticonModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &row = index.row();
|
|
||||||
const auto &image = m_images->images[row];
|
|
||||||
if (role == UrlRole) {
|
|
||||||
return m_connection->makeMediaUrl(image.url).toString();
|
|
||||||
}
|
|
||||||
if (role == BodyRole) {
|
|
||||||
if (image.body) {
|
|
||||||
return *image.body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (role == ShortCodeRole) {
|
|
||||||
return image.shortcode;
|
|
||||||
}
|
|
||||||
if (role == IsStickerRole) {
|
|
||||||
if (image.usage) {
|
|
||||||
return image.usage->isEmpty() || image.usage->contains("sticker"_ls);
|
|
||||||
}
|
|
||||||
if (m_images->pack && m_images->pack->usage) {
|
|
||||||
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("sticker"_ls);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == IsEmojiRole) {
|
|
||||||
if (image.usage) {
|
|
||||||
return image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
|
|
||||||
}
|
|
||||||
if (m_images->pack && m_images->pack->usage) {
|
|
||||||
return m_images->pack->usage->isEmpty() || m_images->pack->usage->contains("emoticon"_ls);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> AccountEmoticonModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{AccountEmoticonModel::UrlRole, "url"},
|
|
||||||
{AccountEmoticonModel::BodyRole, "body"},
|
|
||||||
{AccountEmoticonModel::ShortCodeRole, "shortcode"},
|
|
||||||
{AccountEmoticonModel::IsStickerRole, "isSticker"},
|
|
||||||
{AccountEmoticonModel::IsEmojiRole, "isEmoji"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
NeoChatConnection *AccountEmoticonModel::connection() const
|
|
||||||
{
|
|
||||||
return m_connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::setConnection(NeoChatConnection *connection)
|
|
||||||
{
|
|
||||||
if (m_connection) {
|
|
||||||
disconnect(m_connection, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_connection = connection;
|
|
||||||
Q_EMIT connectionChanged();
|
|
||||||
connect(m_connection, &Connection::accountDataChanged, this, [this](QString type) {
|
|
||||||
if (type == QStringLiteral("im.ponies.user_emotes")) {
|
|
||||||
reloadEmoticons();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
reloadEmoticons();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::reloadEmoticons()
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject json;
|
|
||||||
if (m_connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
|
||||||
json = m_connection->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
|
||||||
}
|
|
||||||
const auto &content = ImagePackEventContent(json);
|
|
||||||
beginResetModel();
|
|
||||||
m_images = content;
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::deleteEmoticon(int index)
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject data;
|
|
||||||
m_images->images.removeAt(index);
|
|
||||||
m_images->fillJson(&data);
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::setEmoticonBody(int index, const QString &text)
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_images->images[index].body = text;
|
|
||||||
QJsonObject data;
|
|
||||||
m_images->fillJson(&data);
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::setEmoticonShortcode(int index, const QString &shortcode)
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_images->images[index].shortcode = shortcode;
|
|
||||||
QJsonObject data;
|
|
||||||
m_images->fillJson(&data);
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::setEmoticonImage(int index, const QUrl &source)
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
doSetEmoticonImage(index, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
QCoro::Task<void> AccountEmoticonModel::doSetEmoticonImage(int index, QUrl source)
|
|
||||||
{
|
|
||||||
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
|
||||||
co_await qCoro(job.get(), &BaseJob::finished);
|
|
||||||
if (job->error() != BaseJob::NoError) {
|
|
||||||
co_return;
|
|
||||||
}
|
|
||||||
m_images->images[index].url = job->contentUri();
|
|
||||||
auto mime = QMimeDatabase().mimeTypeForUrl(source);
|
|
||||||
source.setScheme("file"_ls);
|
|
||||||
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
|
||||||
EventContent::ImageInfo info;
|
|
||||||
if (mime.name().startsWith("image/"_ls)) {
|
|
||||||
QImage image(source.toLocalFile());
|
|
||||||
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
|
|
||||||
}
|
|
||||||
m_images->images[index].info = info;
|
|
||||||
QJsonObject data;
|
|
||||||
m_images->fillJson(&data);
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
QCoro::Task<void> AccountEmoticonModel::doAddEmoticon(QUrl source, QString shortcode, QString description, QString type)
|
|
||||||
{
|
|
||||||
auto job = m_connection->uploadFile(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
|
||||||
co_await qCoro(job.get(), &BaseJob::finished);
|
|
||||||
if (job->error() != BaseJob::NoError) {
|
|
||||||
co_return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mime = QMimeDatabase().mimeTypeForUrl(source);
|
|
||||||
source.setScheme("file"_ls);
|
|
||||||
QFileInfo fileInfo(source.isLocalFile() ? source.toLocalFile() : source.toString());
|
|
||||||
EventContent::ImageInfo info;
|
|
||||||
if (mime.name().startsWith("image/"_ls)) {
|
|
||||||
QImage image(source.toLocalFile());
|
|
||||||
info = EventContent::ImageInfo(source, fileInfo.size(), mime, image.size(), fileInfo.fileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_images->images.append(ImagePackEventContent::ImagePackImage{
|
|
||||||
shortcode,
|
|
||||||
job->contentUri(),
|
|
||||||
description,
|
|
||||||
info,
|
|
||||||
QStringList{type},
|
|
||||||
});
|
|
||||||
QJsonObject data;
|
|
||||||
m_images->fillJson(&data);
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountEmoticonModel::addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type)
|
|
||||||
{
|
|
||||||
if (m_connection == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
doAddEmoticon(source, shortcode, description, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_accountemoticonmodel.cpp"
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "events/imagepackevent.h"
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QCoroTask>
|
|
||||||
#include <QList>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
class NeoChatConnection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class AccountEmoticonModel
|
|
||||||
*
|
|
||||||
* This class defines the model for visualising the account stickers and emojis.
|
|
||||||
*
|
|
||||||
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
|
||||||
*/
|
|
||||||
class AccountEmoticonModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The connection to get emoticons from.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Roles {
|
|
||||||
UrlRole = Qt::UserRole + 1, /**< The URL for the emoticon. */
|
|
||||||
ShortCodeRole, /**< The shortcode for the emoticon. */
|
|
||||||
BodyRole, //**< A textual description of the emoticon */
|
|
||||||
IsStickerRole, //**< Whether this emoticon is a sticker */
|
|
||||||
IsEmojiRole, //**< Whether this emoticon is an emoji */
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit AccountEmoticonModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Number of rows in the model.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::rowCount
|
|
||||||
*/
|
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the given role value at the given index.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::data
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a mapping from Role enum values to role names.
|
|
||||||
*
|
|
||||||
* @sa Roles, QAbstractItemModel::roleNames()
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatConnection *connection() const;
|
|
||||||
void setConnection(NeoChatConnection *connection);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Deletes the emoticon at the given index.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void deleteEmoticon(int index);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Changes the description for the emoticon at the given index.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void setEmoticonBody(int index, const QString &text);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Changes the shortcode for the emoticon at the given index.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void setEmoticonShortcode(int index, const QString &shortCode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Changes the image for the emoticon at the given index.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void setEmoticonImage(int index, const QUrl &source);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add an emoticon with the given parameters.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void addEmoticon(const QUrl &source, const QString &shortcode, const QString &description, const QString &type);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void connectionChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::optional<Quotient::ImagePackEventContent> m_images;
|
|
||||||
QPointer<NeoChatConnection> m_connection;
|
|
||||||
QCoro::Task<void> doSetEmoticonImage(int index, QUrl source);
|
|
||||||
QCoro::Task<void> doAddEmoticon(QUrl source, QString shortcode, QString description, QString type);
|
|
||||||
|
|
||||||
void reloadEmoticons();
|
|
||||||
};
|
|
||||||
@@ -600,14 +600,19 @@ bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messa
|
|||||||
} else {
|
} else {
|
||||||
originalString = event->plainBody();
|
originalString = event->plainBody();
|
||||||
}
|
}
|
||||||
|
QString replaceId = event->id();
|
||||||
|
const auto eventRelation = event->relatesTo();
|
||||||
|
if (eventRelation && eventRelation->type == "m.replace"_L1) {
|
||||||
|
replaceId = eventRelation->eventId;
|
||||||
|
}
|
||||||
if (flags == "/g"_L1) {
|
if (flags == "/g"_L1) {
|
||||||
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
|
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, replaceId);
|
||||||
} else {
|
} else {
|
||||||
room->postHtmlMessage(messageText,
|
room->postHtmlMessage(messageText,
|
||||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||||
event->msgtype(),
|
event->msgtype(),
|
||||||
{},
|
{},
|
||||||
event->id());
|
replaceId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
|
|
||||||
#include "actionsmodel.h"
|
#include "actionsmodel.h"
|
||||||
#include "completionproxymodel.h"
|
#include "completionproxymodel.h"
|
||||||
#include "customemojimodel.h"
|
// #include "emojimodel.h"
|
||||||
#include "emojimodel.h"
|
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
#include "userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
@@ -16,11 +15,13 @@ CompletionModel::CompletionModel(QObject *parent)
|
|||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_filterModel(new CompletionProxyModel())
|
, m_filterModel(new CompletionProxyModel())
|
||||||
, m_userListModel(RoomManager::instance().userListModel())
|
, m_userListModel(RoomManager::instance().userListModel())
|
||||||
, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
//, m_emojiModel(new QConcatenateTablesProxyModel(this))
|
||||||
{
|
{
|
||||||
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
|
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);
|
||||||
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
||||||
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
m_userListModel->setRoom(m_room);
|
||||||
|
});
|
||||||
|
// TODO m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CompletionModel::text() const
|
QString CompletionModel::text() const
|
||||||
@@ -85,29 +86,23 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
|||||||
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
|
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
|
||||||
}
|
}
|
||||||
if (role == IconNameRole) {
|
if (role == IconNameRole) {
|
||||||
auto mediaId = m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
|
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
|
||||||
if (mediaId.isEmpty()) {
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
if (m_room) {
|
|
||||||
return m_room->connection()->makeMediaUrl(QUrl(QStringLiteral("mxc://%1").arg(mediaId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m_autoCompletionType == Emoji) {
|
|
||||||
if (role == DisplayNameRole) {
|
|
||||||
return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
|
|
||||||
}
|
|
||||||
if (role == IconNameRole) {
|
|
||||||
return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
|
|
||||||
}
|
|
||||||
if (role == ReplacedTextRole) {
|
|
||||||
return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
|
|
||||||
}
|
|
||||||
if (role == SubtitleRole) {
|
|
||||||
return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if (m_autoCompletionType == Emoji) {
|
||||||
|
// if (role == DisplayNameRole) {
|
||||||
|
// return m_filterModel->data(filterIndex, CustomEmojiModel::DisplayRole);
|
||||||
|
// }
|
||||||
|
// if (role == IconNameRole) {
|
||||||
|
// return m_filterModel->data(filterIndex, CustomEmojiModel::MxcUrl);
|
||||||
|
// }
|
||||||
|
// if (role == ReplacedTextRole) {
|
||||||
|
// return m_filterModel->data(filterIndex, CustomEmojiModel::ReplacedTextRole);
|
||||||
|
// }
|
||||||
|
// if (role == SubtitleRole) {
|
||||||
|
// // TODO return m_filterModel->data(filterIndex, EmojiModel::DescriptionRole);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -153,8 +148,8 @@ void CompletionModel::updateCompletion()
|
|||||||
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
|
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
|
||||||
m_filterModel->setSourceModel(m_emojiModel);
|
m_filterModel->setSourceModel(m_emojiModel);
|
||||||
m_autoCompletionType = Emoji;
|
m_autoCompletionType = Emoji;
|
||||||
m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
// m_filterModel->setFilterRole(CustomEmojiModel::Name);
|
||||||
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
// TODO m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
||||||
m_filterModel->setFullText(m_fullText);
|
m_filterModel->setFullText(m_fullText);
|
||||||
m_filterModel->setFilterText(m_text);
|
m_filterModel->setFilterText(m_text);
|
||||||
m_filterModel->invalidate();
|
m_filterModel->invalidate();
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "customemojimodel.h"
|
|
||||||
|
|
||||||
#include <QImage>
|
|
||||||
#include <QMimeDatabase>
|
|
||||||
|
|
||||||
#include "emojimodel.h"
|
|
||||||
|
|
||||||
#include <Quotient/csapi/account-data.h>
|
|
||||||
#include <Quotient/csapi/content-repo.h>
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
void CustomEmojiModel::setConnection(NeoChatConnection *connection)
|
|
||||||
{
|
|
||||||
if (connection == m_connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_connection = connection;
|
|
||||||
Q_EMIT connectionChanged();
|
|
||||||
fetchEmojis();
|
|
||||||
}
|
|
||||||
|
|
||||||
NeoChatConnection *CustomEmojiModel::connection() const
|
|
||||||
{
|
|
||||||
return m_connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomEmojiModel::fetchEmojis()
|
|
||||||
{
|
|
||||||
if (!m_connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
|
|
||||||
if (data == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QJsonObject emojis = data->contentJson()["images"_ls].toObject();
|
|
||||||
|
|
||||||
// TODO: Remove with stable migration
|
|
||||||
const auto legacyEmojis = data->contentJson()["emoticons"_ls].toObject();
|
|
||||||
for (const auto &emoji : legacyEmojis.keys()) {
|
|
||||||
if (!emojis.contains(emoji)) {
|
|
||||||
emojis[emoji] = legacyEmojis[emoji];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beginResetModel();
|
|
||||||
m_emojis.clear();
|
|
||||||
|
|
||||||
for (const auto &emoji : emojis.keys()) {
|
|
||||||
const auto &data = emojis[emoji];
|
|
||||||
|
|
||||||
const auto e = emoji.startsWith(":"_ls) ? emoji : (QStringLiteral(":") + emoji + QStringLiteral(":"));
|
|
||||||
|
|
||||||
m_emojis << CustomEmoji{e, data.toObject()["url"_ls].toString(), QRegularExpression(e)};
|
|
||||||
}
|
|
||||||
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
|
|
||||||
{
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
auto job = m_connection->uploadFile(location.toLocalFile());
|
|
||||||
|
|
||||||
connect(job, &BaseJob::success, this, [name, location, job, this] {
|
|
||||||
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
|
|
||||||
auto json = data != nullptr ? data->contentJson() : QJsonObject();
|
|
||||||
auto emojiData = json["images"_ls].toObject();
|
|
||||||
|
|
||||||
QString url;
|
|
||||||
url = job->contentUri().toString();
|
|
||||||
|
|
||||||
QImage image(location.toLocalFile());
|
|
||||||
QJsonObject imageInfo;
|
|
||||||
imageInfo["w"_ls] = image.width();
|
|
||||||
imageInfo["h"_ls] = image.height();
|
|
||||||
imageInfo["mimetype"_ls] = QMimeDatabase().mimeTypeForFile(location.toLocalFile()).name();
|
|
||||||
imageInfo["size"_ls] = image.sizeInBytes();
|
|
||||||
|
|
||||||
emojiData[QStringLiteral("%1").arg(name)] = QJsonObject({
|
|
||||||
{QStringLiteral("url"), url},
|
|
||||||
{QStringLiteral("info"), imageInfo},
|
|
||||||
{QStringLiteral("body"), location.fileName()},
|
|
||||||
{"usage"_ls, "emoticon"_ls},
|
|
||||||
});
|
|
||||||
|
|
||||||
json["images"_ls] = emojiData;
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomEmojiModel::removeEmoji(const QString &name)
|
|
||||||
{
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
const auto &data = m_connection->accountData("im.ponies.user_emotes"_ls);
|
|
||||||
Q_ASSERT(data);
|
|
||||||
auto json = data->contentJson();
|
|
||||||
const QString _name = name.mid(1).chopped(1);
|
|
||||||
auto emojiData = json["images"_ls].toObject();
|
|
||||||
|
|
||||||
if (emojiData.contains(name)) {
|
|
||||||
emojiData.remove(name);
|
|
||||||
json["images"_ls] = emojiData;
|
|
||||||
}
|
|
||||||
if (emojiData.contains(_name)) {
|
|
||||||
emojiData.remove(_name);
|
|
||||||
json["images"_ls] = emojiData;
|
|
||||||
}
|
|
||||||
emojiData = json["emoticons"_ls].toObject();
|
|
||||||
if (emojiData.contains(name)) {
|
|
||||||
emojiData.remove(name);
|
|
||||||
json["emoticons"_ls] = emojiData;
|
|
||||||
}
|
|
||||||
if (emojiData.contains(_name)) {
|
|
||||||
emojiData.remove(_name);
|
|
||||||
json["emoticons"_ls] = emojiData;
|
|
||||||
}
|
|
||||||
m_connection->setAccountData("im.ponies.user_emotes"_ls, json);
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomEmojiModel::CustomEmojiModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
connect(this, &CustomEmojiModel::connectionChanged, this, [this]() {
|
|
||||||
if (!m_connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CustomEmojiModel::fetchEmojis();
|
|
||||||
connect(m_connection, &Connection::accountDataChanged, this, [this](const QString &id) {
|
|
||||||
if (id != QStringLiteral("im.ponies.user_emotes")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchEmojis();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
CustomEmojiModel::fetchEmojis();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant CustomEmojiModel::data(const QModelIndex &idx, int role) const
|
|
||||||
{
|
|
||||||
const auto row = idx.row();
|
|
||||||
if (row >= m_emojis.length()) {
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
const auto &data = m_emojis[row];
|
|
||||||
|
|
||||||
switch (Roles(role)) {
|
|
||||||
case Roles::ModelData:
|
|
||||||
return QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(data.url)).toString(), data.name, true));
|
|
||||||
case Roles::Name:
|
|
||||||
case Roles::DisplayRole:
|
|
||||||
case Roles::ReplacedTextRole:
|
|
||||||
return data.name;
|
|
||||||
case Roles::ImageURL:
|
|
||||||
return m_connection->makeMediaUrl(QUrl(data.url));
|
|
||||||
case Roles::MxcUrl:
|
|
||||||
return m_connection->makeMediaUrl(QUrl(data.url));
|
|
||||||
default:
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
int CustomEmojiModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(parent)
|
|
||||||
|
|
||||||
return m_emojis.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> CustomEmojiModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{Name, "name"},
|
|
||||||
{ImageURL, "imageURL"},
|
|
||||||
{ModelData, "modelData"},
|
|
||||||
{MxcUrl, "mxcUrl"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CustomEmojiModel::preprocessText(QString text)
|
|
||||||
{
|
|
||||||
for (const auto &emoji : std::as_const(m_emojis)) {
|
|
||||||
text.replace(
|
|
||||||
emoji.regexp,
|
|
||||||
QStringLiteral(R"(<img data-mx-emoticon="" src="%1" alt="%2" title="%2" height="32" vertical-align="middle" />)").arg(emoji.url, emoji.name));
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList CustomEmojiModel::filterModel(const QString &filter)
|
|
||||||
{
|
|
||||||
QVariantList results;
|
|
||||||
for (const auto &emoji : std::as_const(m_emojis)) {
|
|
||||||
if (results.length() >= 10)
|
|
||||||
break;
|
|
||||||
if (!emoji.name.contains(filter, Qt::CaseInsensitive))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
results << QVariant::fromValue(Emoji(m_connection->makeMediaUrl(QUrl(emoji.url)).toString(), emoji.name, true));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_customemojimodel.cpp"
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
|
|
||||||
#include "neochatconnection.h"
|
|
||||||
|
|
||||||
struct CustomEmoji {
|
|
||||||
QString name; // with :semicolons:
|
|
||||||
QString url; // mxc://
|
|
||||||
QRegularExpression regexp;
|
|
||||||
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString unicode MEMBER url)
|
|
||||||
Q_PROPERTY(QString name MEMBER name)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class CustomEmojiModel
|
|
||||||
*
|
|
||||||
* This class defines the model for custom user emojis.
|
|
||||||
*
|
|
||||||
* This is based upon the im.ponies.user_emotes spec (MSC2545).
|
|
||||||
*/
|
|
||||||
class CustomEmojiModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
QML_SINGLETON
|
|
||||||
|
|
||||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Defines the model roles.
|
|
||||||
*/
|
|
||||||
enum Roles {
|
|
||||||
Name = Qt::DisplayRole, /**< The name of the emoji. */
|
|
||||||
ImageURL, /**< The URL for the custom emoji. */
|
|
||||||
ModelData, /**< for emulating the regular emoji model's usage, otherwise the UI code would get too complicated. */
|
|
||||||
MxcUrl = 50, /**< The mxc source URL for the custom emoji. */
|
|
||||||
DisplayRole = 51, /**< The name of the emoji. For compatibility with EmojiModel. */
|
|
||||||
ReplacedTextRole = 52, /**< The name of the emoji. For compatibility with EmojiModel. */
|
|
||||||
DescriptionRole = 53, /**< Invalid, reserved. For compatibility with EmojiModel. */
|
|
||||||
};
|
|
||||||
Q_ENUM(Roles)
|
|
||||||
|
|
||||||
static CustomEmojiModel &instance()
|
|
||||||
{
|
|
||||||
static CustomEmojiModel _instance;
|
|
||||||
return _instance;
|
|
||||||
}
|
|
||||||
static CustomEmojiModel *create(QQmlEngine *engine, QJSEngine *)
|
|
||||||
{
|
|
||||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
|
||||||
return &instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the given role value at the given index.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::data
|
|
||||||
*/
|
|
||||||
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Number of rows in the model.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::rowCount
|
|
||||||
*/
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a mapping from Role enum values to role names.
|
|
||||||
*
|
|
||||||
* @sa Roles, QAbstractItemModel::roleNames()
|
|
||||||
*/
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Substitute any custom emojis for an image in the input text.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE QString preprocessText(QString text);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of custom emojis where the name contains the filter text.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE QVariantList filterModel(const QString &filter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a new emoji to the model.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void addEmoji(const QString &name, const QUrl &location);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove an emoji from the model.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE void removeEmoji(const QString &name);
|
|
||||||
|
|
||||||
void setConnection(NeoChatConnection *connection);
|
|
||||||
NeoChatConnection *connection() const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void connectionChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit CustomEmojiModel(QObject *parent = nullptr);
|
|
||||||
QList<CustomEmoji> m_emojis;
|
|
||||||
QPointer<NeoChatConnection> m_connection;
|
|
||||||
|
|
||||||
void fetchEmojis();
|
|
||||||
};
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
#include "emojimodel.h"
|
|
||||||
#include "emojitones.h"
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "customemojimodel.h"
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
EmojiModel::EmojiModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
, m_config(KSharedConfig::openStateConfig())
|
|
||||||
, m_configGroup(KConfigGroup(m_config, QStringLiteral("Editor")))
|
|
||||||
{
|
|
||||||
if (_emojis.isEmpty()) {
|
|
||||||
#include "emojis.h"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int EmojiModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(parent);
|
|
||||||
int total = 0;
|
|
||||||
for (const auto &category : std::as_const(_emojis)) {
|
|
||||||
total += category.count();
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant EmojiModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
auto row = index.row();
|
|
||||||
for (const auto &category : std::as_const(_emojis)) {
|
|
||||||
if (row >= category.count()) {
|
|
||||||
row -= category.count();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto emoji = category[row].value<Emoji>();
|
|
||||||
switch (role) {
|
|
||||||
case ShortNameRole:
|
|
||||||
return QStringLiteral(":%1:").arg(emoji.shortName);
|
|
||||||
case UnicodeRole:
|
|
||||||
case ReplacedTextRole:
|
|
||||||
return emoji.unicode;
|
|
||||||
case InvalidRole:
|
|
||||||
return QStringLiteral("invalid");
|
|
||||||
case DisplayRole:
|
|
||||||
return QStringLiteral("%2 :%1:").arg(emoji.shortName, emoji.unicode);
|
|
||||||
case DescriptionRole:
|
|
||||||
return emoji.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> EmojiModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {{ShortNameRole, "shortName"}, {UnicodeRole, "unicode"}};
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList EmojiModel::lastUsedEmojis() const
|
|
||||||
{
|
|
||||||
return m_configGroup.readEntry(QStringLiteral("lastUsedEmojis"), QStringList());
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::filterModel(const QString &filter, bool limit)
|
|
||||||
{
|
|
||||||
auto emojis = CustomEmojiModel::instance().filterModel(filter);
|
|
||||||
emojis += filterModelNoCustom(filter, limit);
|
|
||||||
return emojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::filterModelNoCustom(const QString &filter, bool limit)
|
|
||||||
{
|
|
||||||
QVariantList result;
|
|
||||||
|
|
||||||
const auto &values = _emojis.values();
|
|
||||||
for (const auto &e : values) {
|
|
||||||
for (const auto &variant : e) {
|
|
||||||
const auto &emoji = qvariant_cast<Emoji>(variant);
|
|
||||||
if (emoji.shortName.contains(filter, Qt::CaseInsensitive)) {
|
|
||||||
result.append(variant);
|
|
||||||
if (result.length() > 10 && limit) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EmojiModel::emojiUsed(const QVariant &modelData)
|
|
||||||
{
|
|
||||||
auto list = lastUsedEmojis();
|
|
||||||
const auto emoji = modelData.value<Emoji>();
|
|
||||||
|
|
||||||
auto it = list.begin();
|
|
||||||
while (it != list.end()) {
|
|
||||||
if (*it == emoji.shortName) {
|
|
||||||
it = list.erase(it);
|
|
||||||
} else {
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.push_front(emoji.shortName);
|
|
||||||
|
|
||||||
m_configGroup.writeEntry(QStringLiteral("lastUsedEmojis"), list);
|
|
||||||
|
|
||||||
Q_EMIT historyChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::emojis(Category category) const
|
|
||||||
{
|
|
||||||
if (category == History) {
|
|
||||||
return emojiHistory();
|
|
||||||
}
|
|
||||||
if (category == HistoryNoCustom) {
|
|
||||||
QVariantList list;
|
|
||||||
const auto &history = emojiHistory();
|
|
||||||
for (const auto &e : history) {
|
|
||||||
auto emoji = qvariant_cast<Emoji>(e);
|
|
||||||
if (!emoji.isCustom) {
|
|
||||||
list.append(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
if (category == Custom) {
|
|
||||||
return CustomEmojiModel::instance().filterModel({});
|
|
||||||
}
|
|
||||||
return _emojis[category];
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::tones(const QString &baseEmoji) const
|
|
||||||
{
|
|
||||||
if (baseEmoji.endsWith(QStringLiteral("tone"))) {
|
|
||||||
return EmojiTones::_tones.values(baseEmoji.split(QStringLiteral(":"))[0]);
|
|
||||||
}
|
|
||||||
return EmojiTones::_tones.values(baseEmoji);
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<EmojiModel::Category, QVariantList> EmojiModel::_emojis;
|
|
||||||
|
|
||||||
QVariantList EmojiModel::categories() const
|
|
||||||
{
|
|
||||||
return QVariantList{
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::HistoryNoCustom},
|
|
||||||
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Smileys},
|
|
||||||
{QStringLiteral("name"), i18nc("'Smileys' is a category of emoji", "Smileys")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("😏")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::People},
|
|
||||||
{QStringLiteral("name"), i18nc("'People' is a category of emoji", "People")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🙋♂️")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Nature},
|
|
||||||
{QStringLiteral("name"), i18nc("'Nature' is a category of emoji", "Nature")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🌲")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Food},
|
|
||||||
{QStringLiteral("name"), i18nc("'Food' is a category of emoji", "Food")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🍛")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Activities},
|
|
||||||
{QStringLiteral("name"), i18nc("'Activities' is a category of emoji", "Activities")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🚁")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Travel},
|
|
||||||
{QStringLiteral("name"), i18nc("'Travel' is a category of emoji", "Travel")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🚅")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Objects},
|
|
||||||
{QStringLiteral("name"), i18nc("'Objects' is a category of emoji", "Objects")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("💡")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Symbols},
|
|
||||||
{QStringLiteral("name"), i18nc("'Symbols' is a category of emoji", "Symbols")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🔣")},
|
|
||||||
}},
|
|
||||||
{QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Flags},
|
|
||||||
{QStringLiteral("name"), i18nc("'Flags' is a category of emoji", "Flags")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🏁")},
|
|
||||||
}},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::categoriesWithCustom() const
|
|
||||||
{
|
|
||||||
auto cats = categories();
|
|
||||||
cats.removeAt(0);
|
|
||||||
cats.insert(0,
|
|
||||||
QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::History},
|
|
||||||
{QStringLiteral("name"), i18nc("Previously used emojis", "History")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("⌛️")},
|
|
||||||
});
|
|
||||||
cats.insert(1,
|
|
||||||
QVariantMap{
|
|
||||||
{QStringLiteral("category"), EmojiModel::Custom},
|
|
||||||
{QStringLiteral("name"), i18nc("'Custom' is a category of emoji", "Custom")},
|
|
||||||
{QStringLiteral("emoji"), QStringLiteral("🖼️")},
|
|
||||||
});
|
|
||||||
;
|
|
||||||
return cats;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList EmojiModel::emojiHistory() const
|
|
||||||
{
|
|
||||||
QVariantList list;
|
|
||||||
const auto &lastUsed = lastUsedEmojis();
|
|
||||||
for (const auto &historicEmoji : lastUsed) {
|
|
||||||
for (const auto &emojiCategory : std::as_const(_emojis)) {
|
|
||||||
for (const auto &emoji : emojiCategory) {
|
|
||||||
if (qvariant_cast<Emoji>(emoji).shortName == historicEmoji) {
|
|
||||||
list.append(emoji);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_emojimodel.cpp"
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <KConfigGroup>
|
|
||||||
#include <KSharedConfig>
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
struct Emoji {
|
|
||||||
Emoji(QString unicode, QString shortname, bool isCustom = false)
|
|
||||||
: unicode(std::move(unicode))
|
|
||||||
, shortName(std::move(shortname))
|
|
||||||
, isCustom(isCustom)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
Emoji(QString unicode, QString shortname, QString description)
|
|
||||||
: unicode(std::move(unicode))
|
|
||||||
, shortName(std::move(shortname))
|
|
||||||
, description(std::move(description))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
Emoji() = default;
|
|
||||||
|
|
||||||
QString unicode;
|
|
||||||
QString shortName;
|
|
||||||
QString description;
|
|
||||||
bool isCustom = false;
|
|
||||||
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString unicode MEMBER unicode)
|
|
||||||
Q_PROPERTY(QString shortName MEMBER shortName)
|
|
||||||
Q_PROPERTY(QString description MEMBER description)
|
|
||||||
Q_PROPERTY(bool isCustom MEMBER isCustom)
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(Emoji)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class EmojiModel
|
|
||||||
*
|
|
||||||
* This class defines the model for visualising a list of emojis.
|
|
||||||
*/
|
|
||||||
class EmojiModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
QML_SINGLETON
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of emoji categories.
|
|
||||||
*
|
|
||||||
* @note No custom emoji categories will be included.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(QVariantList categories READ categories CONSTANT)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of emoji categories with custom emojis.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(QVariantList categoriesWithCustom READ categoriesWithCustom CONSTANT)
|
|
||||||
|
|
||||||
public:
|
|
||||||
static EmojiModel &instance()
|
|
||||||
{
|
|
||||||
static EmojiModel _instance;
|
|
||||||
return _instance;
|
|
||||||
}
|
|
||||||
static EmojiModel *create(QQmlEngine *engine, QJSEngine *)
|
|
||||||
{
|
|
||||||
engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership);
|
|
||||||
return &instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Defines the model roles.
|
|
||||||
*/
|
|
||||||
enum RoleNames {
|
|
||||||
ShortNameRole = Qt::DisplayRole, /**< The name of the emoji. */
|
|
||||||
UnicodeRole, /**< The unicode character of the emoji. */
|
|
||||||
InvalidRole = 50, /**< Invalid, reserved. */
|
|
||||||
DisplayRole = 51, /**< The display text for an emoji. */
|
|
||||||
ReplacedTextRole = 52, /**< The text to replace the short name with (i.e. the unicode character). */
|
|
||||||
DescriptionRole = 53, /**< The long description of an emoji. */
|
|
||||||
};
|
|
||||||
Q_ENUM(RoleNames)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Defines the potential categories an emoji can be placed in.
|
|
||||||
*/
|
|
||||||
enum Category {
|
|
||||||
Custom, /**< A custom user emoji. */
|
|
||||||
Search, /**< The results of a filter. */
|
|
||||||
SearchNoCustom, /**< The results of a filter with no custom emojis. */
|
|
||||||
History, /**< Recently used emojis. */
|
|
||||||
HistoryNoCustom, /**< Recently used emojis with no custom emojis. */
|
|
||||||
Smileys, /**< Smileys & emotion emojis. */
|
|
||||||
People, /**< People & Body emojis. */
|
|
||||||
Nature, /**< Animals & Nature emojis. */
|
|
||||||
Food, /**< Food & Drink emojis. */
|
|
||||||
Activities, /**< Activities emojis. */
|
|
||||||
Travel, /**< Travel & Places emojis. */
|
|
||||||
Objects, /**< Objects emojis. */
|
|
||||||
Symbols, /**< Symbols emojis. */
|
|
||||||
Flags, /**< Flags emojis. */
|
|
||||||
Component, /**< ??? */
|
|
||||||
};
|
|
||||||
Q_ENUM(Category)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the given role value at the given index.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::data
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Number of rows in the model.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::rowCount
|
|
||||||
*/
|
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a mapping from Role enum values to role names.
|
|
||||||
*
|
|
||||||
* @sa RoleNames, QAbstractItemModel::roleNames()
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a filtered list of emojis.
|
|
||||||
*
|
|
||||||
* @note This includes custom emojis, use filterModelNoCustom to return a result
|
|
||||||
* without custom emojis.
|
|
||||||
*
|
|
||||||
* @sa filterModelNoCustom
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE static QVariantList filterModel(const QString &filter, bool limit = true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a filtered list of emojis without custom emojis.
|
|
||||||
*
|
|
||||||
* @note Use filterModel to return a result with custom emojis.
|
|
||||||
*
|
|
||||||
* @sa filterModel
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE static QVariantList filterModelNoCustom(const QString &filter, bool limit = true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of emojis for the given category.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE QVariantList emojis(Category category) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of emoji tones for the given base emoji.
|
|
||||||
*/
|
|
||||||
Q_INVOKABLE QVariantList tones(const QString &baseEmoji) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a list of the last used emoji shortnames
|
|
||||||
*/
|
|
||||||
QStringList lastUsedEmojis() const;
|
|
||||||
|
|
||||||
QVariantList categories() const;
|
|
||||||
QVariantList categoriesWithCustom() const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void historyChanged();
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
|
||||||
void emojiUsed(const QVariant &modelData);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static QHash<Category, QVariantList> _emojis;
|
|
||||||
|
|
||||||
/// Returns QVariants containing the last used Emojis
|
|
||||||
QVariantList emojiHistory() const;
|
|
||||||
|
|
||||||
KSharedConfig::Ptr m_config;
|
|
||||||
KConfigGroup m_configGroup;
|
|
||||||
EmojiModel(QObject *parent = nullptr);
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "emoticonfiltermodel.h"
|
|
||||||
|
|
||||||
#include "accountemoticonmodel.h"
|
|
||||||
#include "stickermodel.h"
|
|
||||||
|
|
||||||
EmoticonFilterModel::EmoticonFilterModel(QObject *parent)
|
|
||||||
: QSortFilterProxyModel(parent)
|
|
||||||
{
|
|
||||||
connect(this, &EmoticonFilterModel::sourceModelChanged, this, [this]() {
|
|
||||||
if (dynamic_cast<StickerModel *>(sourceModel())) {
|
|
||||||
m_stickerRole = StickerModel::IsStickerRole;
|
|
||||||
m_emojiRole = StickerModel::IsEmojiRole;
|
|
||||||
} else {
|
|
||||||
m_stickerRole = AccountEmoticonModel::IsStickerRole;
|
|
||||||
m_emojiRole = AccountEmoticonModel::IsEmojiRole;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EmoticonFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(sourceParent);
|
|
||||||
auto stickerUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_stickerRole).toBool();
|
|
||||||
auto emojiUsage = sourceModel()->data(sourceModel()->index(sourceRow, 0), m_emojiRole).toBool();
|
|
||||||
return (stickerUsage && m_showStickers) || (emojiUsage && m_showEmojis);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EmoticonFilterModel::showStickers() const
|
|
||||||
{
|
|
||||||
return m_showStickers;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EmoticonFilterModel::setShowStickers(bool showStickers)
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
m_showStickers = showStickers;
|
|
||||||
endResetModel();
|
|
||||||
Q_EMIT showStickersChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EmoticonFilterModel::showEmojis() const
|
|
||||||
{
|
|
||||||
return m_showEmojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EmoticonFilterModel::setShowEmojis(bool showEmojis)
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
m_showEmojis = showEmojis;
|
|
||||||
endResetModel();
|
|
||||||
Q_EMIT showEmojisChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_emoticonfiltermodel.cpp"
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QQmlEngine>
|
|
||||||
#include <QSortFilterProxyModel>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class EmoticonFilterModel
|
|
||||||
*
|
|
||||||
* This class creates a custom QSortFilterProxyModel for filtering a emoticon by type
|
|
||||||
* (Sticker or Emoji).
|
|
||||||
*/
|
|
||||||
class EmoticonFilterModel : public QSortFilterProxyModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether stickers should be shown
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether emojis show be shown
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool showEmojis READ showEmojis WRITE setShowEmojis NOTIFY showEmojisChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EmoticonFilterModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Custom filter function checking the type of emoticon
|
|
||||||
*
|
|
||||||
* @note The filter cannot be modified and will always use the same filter properties.
|
|
||||||
*/
|
|
||||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
|
||||||
|
|
||||||
[[nodiscard]] bool showStickers() const;
|
|
||||||
void setShowStickers(bool showStickers);
|
|
||||||
|
|
||||||
[[nodiscard]] bool showEmojis() const;
|
|
||||||
void setShowEmojis(bool showEmojis);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void showStickersChanged();
|
|
||||||
void showEmojisChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_showStickers = false;
|
|
||||||
bool m_showEmojis = false;
|
|
||||||
int m_stickerRole = 0;
|
|
||||||
int m_emojiRole = 0;
|
|
||||||
};
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagepacksmodel.h"
|
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
ImagePacksModel::ImagePacksModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
int ImagePacksModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return m_events.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
const auto row = index.row();
|
|
||||||
if (row < 0 || row >= m_events.size()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const auto &event = m_events[row];
|
|
||||||
if (role == DisplayNameRole) {
|
|
||||||
if (event.pack->displayName) {
|
|
||||||
return *event.pack->displayName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (role == AvatarUrlRole) {
|
|
||||||
if (event.pack->avatarUrl) {
|
|
||||||
return m_room->connection()->makeMediaUrl(*event.pack->avatarUrl);
|
|
||||||
} else if (!event.images.empty()) {
|
|
||||||
return m_room->connection()->makeMediaUrl(event.images[0].url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> ImagePacksModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{DisplayNameRole, "displayName"},
|
|
||||||
{AvatarUrlRole, "avatarUrl"},
|
|
||||||
{AttributionRole, "attribution"},
|
|
||||||
{IdRole, "id"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
NeoChatRoom *ImagePacksModel::room() const
|
|
||||||
{
|
|
||||||
return m_room;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksModel::setRoom(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
if (m_room) {
|
|
||||||
disconnect(m_room, nullptr, this, nullptr);
|
|
||||||
disconnect(m_room->connection(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
m_room = room;
|
|
||||||
|
|
||||||
if (m_room) {
|
|
||||||
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
|
||||||
if (type == "im.ponies.user_emotes"_ls) {
|
|
||||||
reloadImages();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// TODO listen to packs changing
|
|
||||||
reloadImages();
|
|
||||||
Q_EMIT roomChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksModel::reloadImages()
|
|
||||||
{
|
|
||||||
if (!m_room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
beginResetModel();
|
|
||||||
m_events.clear();
|
|
||||||
|
|
||||||
// Load emoticons from the account data
|
|
||||||
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
|
|
||||||
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
|
|
||||||
json["pack"_ls] = QJsonObject{
|
|
||||||
{"display_name"_ls,
|
|
||||||
m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
|
|
||||||
};
|
|
||||||
const auto &content = ImagePackEventContent(json);
|
|
||||||
if (!content.images.isEmpty()) {
|
|
||||||
m_events += ImagePackEventContent(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load emoticons from the saved rooms
|
|
||||||
const auto &accountData = m_room->connection()->accountData("im.ponies.emote_rooms"_ls);
|
|
||||||
if (accountData) {
|
|
||||||
const auto &rooms = accountData->contentJson()["rooms"_ls].toObject();
|
|
||||||
for (const auto &roomId : rooms.keys()) {
|
|
||||||
if (roomId == m_room->id()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto packs = rooms[roomId].toObject();
|
|
||||||
const auto &stickerRoom = m_room->connection()->room(roomId);
|
|
||||||
if (!stickerRoom) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const auto &packKey : packs.keys()) {
|
|
||||||
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
|
|
||||||
const auto packContent = pack->content();
|
|
||||||
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|
|
||||||
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers()))
|
|
||||||
&& !packContent.images.isEmpty()) {
|
|
||||||
m_events += packContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load emoticons from the current room
|
|
||||||
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
|
|
||||||
for (const auto &event : events) {
|
|
||||||
auto packContent = eventCast<const ImagePackEvent>(event)->content();
|
|
||||||
if (packContent.pack.has_value()) {
|
|
||||||
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|
|
||||||
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers())) {
|
|
||||||
m_events += packContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Q_EMIT imagesLoaded();
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagePacksModel::showStickers() const
|
|
||||||
{
|
|
||||||
return m_showStickers;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksModel::setShowStickers(bool showStickers)
|
|
||||||
{
|
|
||||||
m_showStickers = showStickers;
|
|
||||||
Q_EMIT showStickersChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagePacksModel::showEmoticons() const
|
|
||||||
{
|
|
||||||
return m_showEmoticons;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksModel::setShowEmoticons(bool showEmoticons)
|
|
||||||
{
|
|
||||||
m_showEmoticons = showEmoticons;
|
|
||||||
Q_EMIT showEmoticonsChanged();
|
|
||||||
}
|
|
||||||
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_events.size()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return m_events[index].images;
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_imagepacksmodel.cpp"
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "events/imagepackevent.h"
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QList>
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
class NeoChatRoom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class ImagePacksModel
|
|
||||||
*
|
|
||||||
* Defines the model for visualising image packs.
|
|
||||||
*
|
|
||||||
* See Matrix MSC2545 for more details on image packs.
|
|
||||||
* https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
|
|
||||||
*/
|
|
||||||
class ImagePacksModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The current room that the model is being used in.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether sticker image packs should be shown.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool showStickers READ showStickers WRITE setShowStickers NOTIFY showStickersChanged)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether emoticon image packs should be shown.
|
|
||||||
*/
|
|
||||||
Q_PROPERTY(bool showEmoticons READ showEmoticons WRITE setShowEmoticons NOTIFY showEmoticonsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Defines the model roles.
|
|
||||||
*/
|
|
||||||
enum Roles {
|
|
||||||
DisplayNameRole = Qt::DisplayRole, /**< The display name of the image pack. */
|
|
||||||
AvatarUrlRole, /**< The source mxc URL for the pack avatar. */
|
|
||||||
AttributionRole, /**< The attribution for the pack author(s). */
|
|
||||||
IdRole, /**< The ID of the image pack. */
|
|
||||||
};
|
|
||||||
Q_ENUM(Roles)
|
|
||||||
|
|
||||||
explicit ImagePacksModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the given role value at the given index.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::data
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Number of rows in the model.
|
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::rowCount
|
|
||||||
*/
|
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a mapping from Role enum values to role names.
|
|
||||||
*
|
|
||||||
* @sa Roles, QAbstractItemModel::roleNames()
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *room() const;
|
|
||||||
void setRoom(NeoChatRoom *room);
|
|
||||||
|
|
||||||
[[nodiscard]] bool showStickers() const;
|
|
||||||
void setShowStickers(bool showStickers);
|
|
||||||
|
|
||||||
[[nodiscard]] bool showEmoticons() const;
|
|
||||||
void setShowEmoticons(bool showEmoticons);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Return a vector of the images in the pack at the given index.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void roomChanged();
|
|
||||||
void showStickersChanged();
|
|
||||||
void showEmoticonsChanged();
|
|
||||||
void imagesLoaded();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QPointer<NeoChatRoom> m_room;
|
|
||||||
QList<Quotient::ImagePackEventContent> m_events;
|
|
||||||
bool m_showStickers = true;
|
|
||||||
bool m_showEmoticons = true;
|
|
||||||
void reloadImages();
|
|
||||||
};
|
|
||||||
@@ -34,7 +34,7 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event
|
|||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_room(room)
|
, m_room(room)
|
||||||
, m_eventId(eventId)
|
, m_eventId(eventId)
|
||||||
, m_isPending(isPending)
|
, m_currentState(isPending ? Pending : Unknown)
|
||||||
, m_isReply(isReply)
|
, m_isReply(isReply)
|
||||||
{
|
{
|
||||||
initializeModel();
|
initializeModel();
|
||||||
@@ -45,19 +45,27 @@ void MessageContentModel::initializeModel()
|
|||||||
Q_ASSERT(m_room != nullptr);
|
Q_ASSERT(m_room != nullptr);
|
||||||
Q_ASSERT(!m_eventId.isEmpty());
|
Q_ASSERT(!m_eventId.isEmpty());
|
||||||
|
|
||||||
connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent);
|
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
|
||||||
|
if (m_room != nullptr && m_currentState == Unknown) {
|
||||||
|
initializeEvent();
|
||||||
|
updateReplyModel();
|
||||||
|
resetModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
||||||
if (m_room != nullptr) {
|
if (m_room != nullptr) {
|
||||||
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
|
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
|
||||||
beginResetModel();
|
|
||||||
m_isPending = false;
|
|
||||||
m_eventId = serverEvent->id();
|
m_eventId = serverEvent->id();
|
||||||
initializeEvent();
|
|
||||||
endResetModel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() {
|
||||||
|
if (m_room != nullptr && m_currentState == Pending) {
|
||||||
|
initializeEvent();
|
||||||
|
updateReplyModel();
|
||||||
|
resetModel();
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
|
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
|
||||||
if (m_room != nullptr) {
|
if (m_room != nullptr) {
|
||||||
for (int i = fromIndex; i <= toIndex; i++) {
|
for (int i = fromIndex; i <= toIndex; i++) {
|
||||||
@@ -143,20 +151,33 @@ void MessageContentModel::initializeModel()
|
|||||||
});
|
});
|
||||||
|
|
||||||
initializeEvent();
|
initializeEvent();
|
||||||
updateReplyModel();
|
if (m_currentState == Available || m_currentState == Pending) {
|
||||||
|
updateReplyModel();
|
||||||
|
}
|
||||||
resetModel();
|
resetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageContentModel::initializeEvent()
|
void MessageContentModel::initializeEvent()
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
if (m_currentState == UnAvailable) {
|
||||||
if (event == nullptr) {
|
|
||||||
Q_EMIT eventUnavailable();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto eventResult = m_room->getEvent(m_eventId);
|
||||||
|
if (eventResult.first == nullptr) {
|
||||||
|
if (m_currentState != Pending) {
|
||||||
|
getEvent();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (eventResult.second) {
|
||||||
|
m_currentState = Pending;
|
||||||
|
} else {
|
||||||
|
m_currentState = Available;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_eventSenderObject == nullptr) {
|
if (m_eventSenderObject == nullptr) {
|
||||||
auto senderId = event->senderId();
|
auto senderId = eventResult.first->senderId();
|
||||||
// A pending event might not have a sender ID set yet but in that case it must
|
// A pending event might not have a sender ID set yet but in that case it must
|
||||||
// be the local member.
|
// be the local member.
|
||||||
if (senderId.isEmpty()) {
|
if (senderId.isEmpty()) {
|
||||||
@@ -172,7 +193,6 @@ void MessageContentModel::getEvent()
|
|||||||
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
|
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
|
||||||
if (m_room != nullptr) {
|
if (m_room != nullptr) {
|
||||||
if (eventId == m_eventId) {
|
if (eventId == m_eventId) {
|
||||||
m_notFound = false;
|
|
||||||
initializeEvent();
|
initializeEvent();
|
||||||
updateReplyModel();
|
updateReplyModel();
|
||||||
resetModel();
|
resetModel();
|
||||||
@@ -184,7 +204,7 @@ void MessageContentModel::getEvent()
|
|||||||
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
|
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
|
||||||
if (m_room != nullptr) {
|
if (m_room != nullptr) {
|
||||||
if (eventId == m_eventId) {
|
if (eventId == m_eventId) {
|
||||||
m_notFound = true;
|
m_currentState = UnAvailable;
|
||||||
resetModel();
|
resetModel();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -237,7 +257,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
const auto component = m_components[index.row()];
|
const auto component = m_components[index.row()];
|
||||||
|
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
if (event == nullptr) {
|
if (event.first == nullptr) {
|
||||||
if (role == DisplayRole) {
|
if (role == DisplayRole) {
|
||||||
if (m_isReply) {
|
if (m_isReply) {
|
||||||
return i18n("Loading reply");
|
return i18n("Loading reply");
|
||||||
@@ -252,7 +272,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == DisplayRole) {
|
if (role == DisplayRole) {
|
||||||
if (m_notFound || m_room->connection()->isIgnored(m_eventSenderId)) {
|
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(m_eventSenderId)) {
|
||||||
Kirigami::Platform::PlatformTheme *theme =
|
Kirigami::Platform::PlatformTheme *theme =
|
||||||
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||||
|
|
||||||
@@ -276,7 +296,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
if (!component.content.isEmpty()) {
|
if (!component.content.isEmpty()) {
|
||||||
return component.content;
|
return component.content;
|
||||||
}
|
}
|
||||||
return EventHandler::richBody(m_room, event);
|
return EventHandler::richBody(m_room, event.first);
|
||||||
}
|
}
|
||||||
if (role == ComponentTypeRole) {
|
if (role == ComponentTypeRole) {
|
||||||
return component.type;
|
return component.type;
|
||||||
@@ -285,53 +305,53 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
return component.attributes;
|
return component.attributes;
|
||||||
}
|
}
|
||||||
if (role == EventIdRole) {
|
if (role == EventIdRole) {
|
||||||
return EventHandler::id(event);
|
return EventHandler::id(event.first);
|
||||||
}
|
}
|
||||||
if (role == TimeRole) {
|
if (role == TimeRole) {
|
||||||
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
|
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
|
||||||
return event->transactionId() == pendingEvent->transactionId();
|
return event.first->transactionId() == pendingEvent->transactionId();
|
||||||
});
|
});
|
||||||
|
|
||||||
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
||||||
return EventHandler::time(event, m_isPending, lastUpdated);
|
return EventHandler::time(event.first, m_currentState == Pending, lastUpdated);
|
||||||
}
|
}
|
||||||
if (role == TimeStringRole) {
|
if (role == TimeStringRole) {
|
||||||
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
|
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
|
||||||
return event->transactionId() == pendingEvent->transactionId();
|
return event.first->transactionId() == pendingEvent->transactionId();
|
||||||
});
|
});
|
||||||
|
|
||||||
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
|
||||||
return EventHandler::timeString(event, QStringLiteral("hh:mm"), m_isPending, lastUpdated);
|
return EventHandler::timeString(event.first, QStringLiteral("hh:mm"), m_currentState == Pending, lastUpdated);
|
||||||
}
|
}
|
||||||
if (role == AuthorRole) {
|
if (role == AuthorRole) {
|
||||||
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
|
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
|
||||||
}
|
}
|
||||||
if (role == MediaInfoRole) {
|
if (role == MediaInfoRole) {
|
||||||
return EventHandler::mediaInfo(m_room, event);
|
return EventHandler::mediaInfo(m_room, event.first);
|
||||||
}
|
}
|
||||||
if (role == FileTransferInfoRole) {
|
if (role == FileTransferInfoRole) {
|
||||||
return QVariant::fromValue(m_room->cachedFileTransferInfo(event));
|
return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first));
|
||||||
}
|
}
|
||||||
if (role == ItineraryModelRole) {
|
if (role == ItineraryModelRole) {
|
||||||
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
|
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
|
||||||
}
|
}
|
||||||
if (role == LatitudeRole) {
|
if (role == LatitudeRole) {
|
||||||
return EventHandler::latitude(event);
|
return EventHandler::latitude(event.first);
|
||||||
}
|
}
|
||||||
if (role == LongitudeRole) {
|
if (role == LongitudeRole) {
|
||||||
return EventHandler::longitude(event);
|
return EventHandler::longitude(event.first);
|
||||||
}
|
}
|
||||||
if (role == AssetRole) {
|
if (role == AssetRole) {
|
||||||
return EventHandler::locationAssetType(event);
|
return EventHandler::locationAssetType(event.first);
|
||||||
}
|
}
|
||||||
if (role == PollHandlerRole) {
|
if (role == PollHandlerRole) {
|
||||||
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
|
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
|
||||||
}
|
}
|
||||||
if (role == ReplyEventIdRole) {
|
if (role == ReplyEventIdRole) {
|
||||||
return EventHandler::replyId(event);
|
return EventHandler::replyId(event.first);
|
||||||
}
|
}
|
||||||
if (role == ReplyAuthorRole) {
|
if (role == ReplyAuthorRole) {
|
||||||
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event));
|
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
|
||||||
}
|
}
|
||||||
if (role == ReplyContentModelRole) {
|
if (role == ReplyContentModelRole) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
||||||
@@ -387,18 +407,17 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
|
|||||||
|
|
||||||
void MessageContentModel::resetModel()
|
void MessageContentModel::resetModel()
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_components.clear();
|
m_components.clear();
|
||||||
|
|
||||||
if (m_room->connection()->isIgnored(m_eventSenderId) || m_notFound) {
|
if (m_room->connection()->isIgnored(m_eventSenderId) || m_currentState == UnAvailable) {
|
||||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||||
endResetModel();
|
endResetModel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event == nullptr) {
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
|
if (event.first == nullptr) {
|
||||||
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
||||||
endResetModel();
|
endResetModel();
|
||||||
return;
|
return;
|
||||||
@@ -431,19 +450,19 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
|
|||||||
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
|
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
if (event == nullptr) {
|
if (event.first == nullptr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessageComponent> newComponents;
|
QList<MessageComponent> newComponents;
|
||||||
|
|
||||||
if (eventCast<const Quotient::RoomMessageEvent>(event)
|
if (eventCast<const Quotient::RoomMessageEvent>(event.first)
|
||||||
&& eventCast<const Quotient::RoomMessageEvent>(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
&& eventCast<const Quotient::RoomMessageEvent>(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||||
return newComponents;
|
return newComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event->isRedacted()) {
|
if (event.first->isRedacted()) {
|
||||||
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||||
return newComponents;
|
return newComponents;
|
||||||
}
|
}
|
||||||
@@ -455,7 +474,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
|||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||||
} else {
|
} else {
|
||||||
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event)));
|
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_room->urlPreviewEnabled()) {
|
if (m_room->urlPreviewEnabled()) {
|
||||||
@@ -463,7 +482,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
|
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
|
||||||
if (isThreading && !EventHandler::isThreaded(event)) {
|
if (isThreading && !EventHandler::isThreaded(event.first)) {
|
||||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,11 +492,11 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
|||||||
void MessageContentModel::updateReplyModel()
|
void MessageContentModel::updateReplyModel()
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
if (event == nullptr || m_isReply) {
|
if (event.first == nullptr || m_isReply) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EventHandler::hasReply(event) || (EventHandler::isThreaded(event) && NeoChatConfig::self()->threads())) {
|
if (!EventHandler::hasReply(event.first) || (EventHandler::isThreaded(event.first) && NeoChatConfig::self()->threads())) {
|
||||||
if (m_replyModel) {
|
if (m_replyModel) {
|
||||||
delete m_replyModel;
|
delete m_replyModel;
|
||||||
}
|
}
|
||||||
@@ -488,7 +507,7 @@ void MessageContentModel::updateReplyModel()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event), true, false, this);
|
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event.first), true, false, this);
|
||||||
|
|
||||||
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
|
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
|
||||||
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
|
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
|
||||||
@@ -498,13 +517,13 @@ void MessageContentModel::updateReplyModel()
|
|||||||
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
|
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
if (event == nullptr) {
|
if (event.first == nullptr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageComponentType::Text: {
|
case MessageComponentType::Text: {
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||||
return TextHandler().textComponents(body,
|
return TextHandler().textComponents(body,
|
||||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||||
@@ -515,11 +534,11 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
|||||||
case MessageComponentType::File: {
|
case MessageComponentType::File: {
|
||||||
QList<MessageComponent> components;
|
QList<MessageComponent> components;
|
||||||
components += MessageComponent{MessageComponentType::File, QString(), {}};
|
components += MessageComponent{MessageComponentType::File, QString(), {}};
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||||
|
|
||||||
if (m_emptyItinerary) {
|
if (m_emptyItinerary) {
|
||||||
if (!m_isReply) {
|
if (!m_isReply) {
|
||||||
auto fileTransferInfo = m_room->cachedFileTransferInfo(event);
|
auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first);
|
||||||
|
|
||||||
#ifndef Q_OS_ANDROID
|
#ifndef Q_OS_ANDROID
|
||||||
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has<EventContent::FileContent>());
|
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has<EventContent::FileContent>());
|
||||||
@@ -567,17 +586,24 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
|
|||||||
case MessageComponentType::Image:
|
case MessageComponentType::Image:
|
||||||
case MessageComponentType::Audio:
|
case MessageComponentType::Audio:
|
||||||
case MessageComponentType::Video: {
|
case MessageComponentType::Video: {
|
||||||
if (!event->is<StickerEvent>()) {
|
if (!event.first->is<StickerEvent>()) {
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||||
QList<MessageComponent> components;
|
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
|
||||||
components += MessageComponent{type, QString(), {}};
|
if (fileContent != nullptr) {
|
||||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
const auto fileInfo = fileContent->commonInfo();
|
||||||
components += TextHandler().textComponents(body,
|
const auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
// Do not attach the description to the image, if it's the same as the original filename.
|
||||||
m_room,
|
if (fileInfo.originalName != body) {
|
||||||
roomMessageEvent,
|
QList<MessageComponent> components;
|
||||||
roomMessageEvent->isReplaced());
|
components += MessageComponent{type, QString(), {}};
|
||||||
return components;
|
components += TextHandler().textComponents(body,
|
||||||
|
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||||
|
m_room,
|
||||||
|
roomMessageEvent,
|
||||||
|
roomMessageEvent->isReplaced());
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -635,7 +661,7 @@ QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageCompon
|
|||||||
|
|
||||||
void MessageContentModel::closeLinkPreview(int row)
|
void MessageContentModel::closeLinkPreview(int row)
|
||||||
{
|
{
|
||||||
if (row < 0 || row > m_components.size()) {
|
if (row < 0 || row >= m_components.size()) {
|
||||||
qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
|
qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -645,6 +671,7 @@ void MessageContentModel::closeLinkPreview(int row)
|
|||||||
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
|
m_removedLinkPreviews += m_components[row].attributes["link"_ls].toUrl();
|
||||||
m_components.remove(row);
|
m_components.remove(row);
|
||||||
m_components.squeeze();
|
m_components.squeeze();
|
||||||
|
endResetModel();
|
||||||
resetContent();
|
resetContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,13 +679,13 @@ void MessageContentModel::closeLinkPreview(int row)
|
|||||||
void MessageContentModel::updateItineraryModel()
|
void MessageContentModel::updateItineraryModel()
|
||||||
{
|
{
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
if (m_room == nullptr || event == nullptr) {
|
if (m_room == nullptr || event.first == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
|
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
|
||||||
if (roomMessageEvent->has<EventContent::FileContent>()) {
|
if (roomMessageEvent->has<EventContent::FileContent>()) {
|
||||||
auto filePath = m_room->cachedFileTransferInfo(event).localPath;
|
auto filePath = m_room->cachedFileTransferInfo(event.first).localPath;
|
||||||
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
|
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
|
||||||
delete m_itineraryModel;
|
delete m_itineraryModel;
|
||||||
m_itineraryModel = nullptr;
|
m_itineraryModel = nullptr;
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ class MessageContentModel : public QAbstractListModel
|
|||||||
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum MessageState {
|
||||||
|
Unknown, /**< The message state is unknown. */
|
||||||
|
Pending, /**< The message is a new pending message which the server has not yet acknowledged. */
|
||||||
|
Available, /**< The message is available and acknowledged by the server. */
|
||||||
|
UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */
|
||||||
|
};
|
||||||
|
Q_ENUM(MessageState)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Defines the model roles.
|
* @brief Defines the model roles.
|
||||||
*/
|
*/
|
||||||
@@ -98,7 +106,6 @@ public:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void showAuthorChanged();
|
void showAuthorChanged();
|
||||||
void eventUnavailable();
|
|
||||||
void eventUpdated();
|
void eventUpdated();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -107,10 +114,9 @@ private:
|
|||||||
QString m_eventSenderId;
|
QString m_eventSenderId;
|
||||||
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
|
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
|
||||||
|
|
||||||
bool m_isPending;
|
MessageState m_currentState = Unknown;
|
||||||
bool m_showAuthor = true;
|
bool m_showAuthor = true;
|
||||||
bool m_isReply;
|
bool m_isReply;
|
||||||
bool m_notFound = false;
|
|
||||||
|
|
||||||
void initializeModel();
|
void initializeModel();
|
||||||
void initializeEvent();
|
void initializeEvent();
|
||||||
|
|||||||
@@ -160,12 +160,21 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
refreshLastUserEvents(i);
|
refreshLastUserEvents(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0)
|
||||||
|
connect(m_currentRoom, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
|
||||||
|
m_initialized = true;
|
||||||
|
createEventObjects(event, true);
|
||||||
|
beginInsertRows({}, 0, 0);
|
||||||
|
endInsertRows();
|
||||||
|
});
|
||||||
|
#else
|
||||||
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
|
connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
|
||||||
m_initialized = true;
|
m_initialized = true;
|
||||||
createEventObjects(event);
|
createEventObjects(event, true);
|
||||||
beginInsertRows({}, 0, 0);
|
beginInsertRows({}, 0, 0);
|
||||||
});
|
});
|
||||||
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
|
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
|
||||||
|
#endif
|
||||||
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) {
|
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) {
|
||||||
Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole});
|
Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole});
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
@@ -618,7 +627,7 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
|
|||||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
|
void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, bool isPending)
|
||||||
{
|
{
|
||||||
if (event == nullptr) {
|
if (event == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -641,7 +650,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
|
|||||||
|
|
||||||
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
|
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
|
||||||
if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, eventId));
|
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, eventId, false, isPending));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ private:
|
|||||||
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
|
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
|
||||||
void moveReadMarker(const QString &toEventId);
|
void moveReadMarker(const QString &toEventId);
|
||||||
|
|
||||||
void createEventObjects(const Quotient::RoomEvent *event);
|
void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false);
|
||||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
|
|||||||
return room->displayName().toHtmlEscaped();
|
return room->displayName().toHtmlEscaped();
|
||||||
}
|
}
|
||||||
if (role == AvatarRole) {
|
if (role == AvatarRole) {
|
||||||
return room->avatarMediaId();
|
return room->avatarMediaUrl();
|
||||||
}
|
}
|
||||||
if (role == CanonicalAliasRole) {
|
if (role == CanonicalAliasRole) {
|
||||||
return room->canonicalAlias();
|
return room->canonicalAlias();
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ QHash<int, QByteArray> RoomTreeModel::roleNames() const
|
|||||||
roles[IconRole] = "icon";
|
roles[IconRole] = "icon";
|
||||||
roles[AttentionRole] = "attention";
|
roles[AttentionRole] = "attention";
|
||||||
roles[FavouriteRole] = "favourite";
|
roles[FavouriteRole] = "favourite";
|
||||||
|
roles[RoomTypeRole] = "roomType";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +324,7 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
|
|||||||
return room->displayName();
|
return room->displayName();
|
||||||
}
|
}
|
||||||
if (role == AvatarRole) {
|
if (role == AvatarRole) {
|
||||||
return room->avatarMediaId();
|
return room->avatarMediaUrl();
|
||||||
}
|
}
|
||||||
if (role == CanonicalAliasRole) {
|
if (role == CanonicalAliasRole) {
|
||||||
return room->canonicalAlias();
|
return room->canonicalAlias();
|
||||||
@@ -385,6 +386,11 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
|
|||||||
if (role == FavouriteRole) {
|
if (role == FavouriteRole) {
|
||||||
return room->isFavourite();
|
return room->isFavourite();
|
||||||
}
|
}
|
||||||
|
if (role == RoomTypeRole) {
|
||||||
|
if (room->creation()) {
|
||||||
|
return room->creation()->contentPart<QString>("type"_L1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public:
|
|||||||
IconRole,
|
IconRole,
|
||||||
AttentionRole, /**< Whether there are any notifications. */
|
AttentionRole, /**< Whether there are any notifications. */
|
||||||
FavouriteRole, /**< Whether the room is favourited. */
|
FavouriteRole, /**< Whether the room is favourited. */
|
||||||
|
RoomTypeRole, /**< The room's type. */
|
||||||
};
|
};
|
||||||
Q_ENUM(EventRoles)
|
Q_ENUM(EventRoles)
|
||||||
explicit RoomTreeModel(QObject *parent = nullptr);
|
explicit RoomTreeModel(QObject *parent = nullptr);
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ bool SortFilterRoomTreeModel::filterAcceptsRow(int source_row, const QModelIndex
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide rooms with defined types, assuming that data-holding rooms have a defined type
|
||||||
|
if (!sourceModel()->data(index, RoomTreeModel::RoomTypeRole).toString().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static auto config = NeoChatConfig::self();
|
static auto config = NeoChatConfig::self();
|
||||||
if (config->allRoomsInHome() && RoomManager::instance().currentSpace().isEmpty()) {
|
if (config->allRoomsInHome() && RoomManager::instance().currentSpace().isEmpty()) {
|
||||||
return acceptRoom;
|
return acceptRoom;
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ Action=Popup
|
|||||||
Name=Share
|
Name=Share
|
||||||
Name[ar]=شارك
|
Name[ar]=شارك
|
||||||
Name[ca]=Compartició
|
Name[ca]=Compartició
|
||||||
Name[ca@valencia]=Compartició
|
Name[ca@valencia]=Compartiu
|
||||||
Name[cs]=Sdílet
|
Name[cs]=Sdílet
|
||||||
Name[de]=Teilen
|
Name[de]=Teilen
|
||||||
Name[el]=Κοινοποίηση
|
Name[el]=Κοινοποίηση
|
||||||
|
|||||||
@@ -431,9 +431,9 @@ QDateTime NeoChatRoom::lastActiveTime()
|
|||||||
return messageEvents().rbegin()->get()->originTimestamp();
|
return messageEvents().rbegin()->get()->originTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::avatarMediaId() const
|
QUrl NeoChatRoom::avatarMediaUrl() const
|
||||||
{
|
{
|
||||||
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
|
if (const auto avatar = Room::avatarUrl(); !avatar.isEmpty()) {
|
||||||
return avatar;
|
return avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ QString NeoChatRoom::avatarMediaId() const
|
|||||||
const auto directChatMembers = this->directChatMembers();
|
const auto directChatMembers = this->directChatMembers();
|
||||||
for (const auto member : directChatMembers) {
|
for (const auto member : directChatMembers) {
|
||||||
if (member != localMember()) {
|
if (member != localMember()) {
|
||||||
return member.avatarMediaId();
|
return member.avatarUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1749,25 +1749,31 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::getEvent(const QString &eventId) const
|
std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString &eventId) const
|
||||||
{
|
{
|
||||||
if (eventId.isEmpty()) {
|
if (eventId.isEmpty()) {
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
const auto timelineIt = findInTimeline(eventId);
|
const auto timelineIt = findInTimeline(eventId);
|
||||||
if (timelineIt != historyEdge()) {
|
if (timelineIt != historyEdge()) {
|
||||||
return timelineIt->get();
|
return std::make_pair(timelineIt->get(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto pendingIt = findPendingEvent(eventId);
|
auto pendingIt = findPendingEvent(eventId);
|
||||||
if (pendingIt != pendingEvents().end()) {
|
if (pendingIt != pendingEvents().end()) {
|
||||||
return pendingIt->event();
|
return std::make_pair(pendingIt->event(), true);
|
||||||
|
}
|
||||||
|
// findPendingEvent() searches by transaction ID, we also need to check event ID.
|
||||||
|
for (const auto &event : pendingEvents()) {
|
||||||
|
if (event->id() == eventId || event->transactionId() == eventId) {
|
||||||
|
return std::make_pair(event.event(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto extraIt = std::find_if(m_extraEvents.begin(), m_extraEvents.end(), [eventId](const Quotient::event_ptr_tt<Quotient::RoomEvent> &event) {
|
auto extraIt = std::find_if(m_extraEvents.begin(), m_extraEvents.end(), [eventId](const Quotient::event_ptr_tt<Quotient::RoomEvent> &event) {
|
||||||
return event->id() == eventId;
|
return event->id() == eventId;
|
||||||
});
|
});
|
||||||
return extraIt != m_extraEvents.end() ? extraIt->get() : nullptr;
|
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ class NeoChatRoom : public Quotient::Room
|
|||||||
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
|
Q_PROPERTY(bool readMarkerLoaded READ readMarkerLoaded NOTIFY readMarkerLoadedChanged)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The avatar image to be used for the room.
|
* @brief The avatar image to be used for the room, as a mxc:// URL.
|
||||||
*/
|
*/
|
||||||
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
|
Q_PROPERTY(QUrl avatarMediaUrl READ avatarMediaUrl NOTIFY avatarChanged STORED false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get a RoomMember object for the other person in a direct chat.
|
* @brief Get a RoomMember object for the other person in a direct chat.
|
||||||
@@ -320,7 +320,7 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] bool readMarkerLoaded() const;
|
[[nodiscard]] bool readMarkerLoaded() const;
|
||||||
|
|
||||||
[[nodiscard]] QString avatarMediaId() const;
|
[[nodiscard]] QUrl avatarMediaUrl() const;
|
||||||
|
|
||||||
NeochatRoomMember *directChatRemoteMember();
|
NeochatRoomMember *directChatRemoteMember();
|
||||||
|
|
||||||
@@ -570,7 +570,7 @@ public:
|
|||||||
*
|
*
|
||||||
* The result will be nullptr if not found so needs to be managed.
|
* The result will be nullptr if not found so needs to be managed.
|
||||||
*/
|
*/
|
||||||
const Quotient::RoomEvent *getEvent(const QString &eventId) const;
|
std::pair<const Quotient::RoomEvent *, bool> getEvent(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.
|
||||||
|
|||||||
@@ -153,15 +153,6 @@ QColor NeochatRoomMember::color() const
|
|||||||
return m_room->member(m_memberId).color();
|
return m_room->member(m_memberId).color();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeochatRoomMember::avatarMediaId() const
|
|
||||||
{
|
|
||||||
if (m_room == nullptr || m_memberId.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_room->member(m_memberId).avatarMediaId();
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl NeochatRoomMember::avatarUrl() const
|
QUrl NeochatRoomMember::avatarUrl() const
|
||||||
{
|
{
|
||||||
if (m_room == nullptr || m_memberId.isEmpty()) {
|
if (m_room == nullptr || m_memberId.isEmpty()) {
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ public:
|
|||||||
int hue() const;
|
int hue() const;
|
||||||
qreal hueF() const;
|
qreal hueF() const;
|
||||||
QColor color() const;
|
QColor color() const;
|
||||||
QString avatarMediaId() const;
|
|
||||||
QUrl avatarUrl() const;
|
QUrl avatarUrl() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class PollHandler : public QObject
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("Use NeoChatRoom::poll")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The question for the poll.
|
* @brief The question for the poll.
|
||||||
@@ -91,7 +92,7 @@ Q_SIGNALS:
|
|||||||
void hasEndedChanged();
|
void hasEndedChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Quotient::PollStartEvent *m_pollStartEvent;
|
const Quotient::PollStartEvent *m_pollStartEvent = nullptr;
|
||||||
|
|
||||||
void updatePoll(Quotient::RoomEventsRange events);
|
void updatePoll(Quotient::RoomEventsRange events);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"Name[nn]": "Tobias Fella",
|
"Name[nn]": "Tobias Fella",
|
||||||
"Name[pl]": "Tobias Fella",
|
"Name[pl]": "Tobias Fella",
|
||||||
"Name[ru]": "Tobias Fella",
|
"Name[ru]": "Tobias Fella",
|
||||||
|
"Name[sk]": "Tobias Fella",
|
||||||
"Name[sl]": "Tobias Fella",
|
"Name[sl]": "Tobias Fella",
|
||||||
"Name[sv]": "Tobias Fella",
|
"Name[sv]": "Tobias Fella",
|
||||||
"Name[ta]": "டோபியாஸ் ஃபெல்லா",
|
"Name[ta]": "டோபியாஸ் ஃபெல்லா",
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
"Name[nn]": "NeoChat",
|
"Name[nn]": "NeoChat",
|
||||||
"Name[pl]": "NeoChat",
|
"Name[pl]": "NeoChat",
|
||||||
"Name[ru]": "NeoChat",
|
"Name[ru]": "NeoChat",
|
||||||
|
"Name[sk]": "NeoChat",
|
||||||
"Name[sl]": "NeoChat",
|
"Name[sl]": "NeoChat",
|
||||||
"Name[sv]": "NeoChat",
|
"Name[sv]": "NeoChat",
|
||||||
"Name[ta]": "நியோச்சாட்",
|
"Name[ta]": "நியோச்சாட்",
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ QQC2.Menu {
|
|||||||
text: "https://matrix.to/#/" + root.connection.localUser.id,
|
text: "https://matrix.to/#/" + root.connection.localUser.id,
|
||||||
title: root.connection.localUser.displayName,
|
title: root.connection.localUser.displayName,
|
||||||
subtitle: root.connection.localUser.id,
|
subtitle: root.connection.localUser.id,
|
||||||
avatarSource: root.connection.makeMediaUrl(root.connection.localUser.avatarUrl)
|
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
|
||||||
|
avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : ""
|
||||||
});
|
});
|
||||||
if (typeof root.closeDialog === "function") {
|
if (typeof root.closeDialog === "function") {
|
||||||
root.closeDialog();
|
root.closeDialog();
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Kirigami.Dialog {
|
|||||||
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
width: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||||
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
height: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing
|
||||||
}
|
}
|
||||||
source: userDelegate.connection.localUser.avatarMediaId ? userDelegate.connection.makeMediaUrl("mxc://" + userDelegate.connection.localUser.avatarMediaId) : ""
|
source: userDelegate.connection.localUser.avatarUrl.toString().length > 0 ? userDelegate.connection.makeMediaUrl(userDelegate.connection.localUser.avatarUrl) : ""
|
||||||
name: userDelegate.connection.localUser.displayName ?? userDelegate.connection.localUser.id
|
name: userDelegate.connection.localUser.displayName ?? userDelegate.connection.localUser.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ QQC2.ItemDelegate {
|
|||||||
required property NeoChatRoom currentRoom
|
required property NeoChatRoom currentRoom
|
||||||
required property bool categoryVisible
|
required property bool categoryVisible
|
||||||
required property string filterText
|
required property string filterText
|
||||||
required property string avatar
|
required property url avatar
|
||||||
required property string displayName
|
required property string displayName
|
||||||
|
|
||||||
topPadding: Kirigami.Units.largeSpacing
|
topPadding: Kirigami.Units.largeSpacing
|
||||||
@@ -32,7 +32,7 @@ QQC2.ItemDelegate {
|
|||||||
visible: root.categoryVisible || filterText.length > 0
|
visible: root.categoryVisible || filterText.length > 0
|
||||||
|
|
||||||
contentItem: KirigamiComponents.Avatar {
|
contentItem: KirigamiComponents.Avatar {
|
||||||
source: root.avatar ? root.currentRoom.connection.makeMediaUrl("mxc://" + root.avatar) : ""
|
source: root.avatar
|
||||||
name: root.displayName
|
name: root.displayName
|
||||||
|
|
||||||
sourceSize {
|
sourceSize {
|
||||||
|
|||||||
@@ -27,38 +27,17 @@ Loader {
|
|||||||
Component {
|
Component {
|
||||||
id: regularMenu
|
id: regularMenu
|
||||||
QQC2.Menu {
|
QQC2.Menu {
|
||||||
QQC2.MenuItem {
|
|
||||||
text: room.isFavourite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
|
|
||||||
icon.name: room.isFavourite ? "bookmark-remove" : "bookmark-new"
|
|
||||||
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.MenuItem {
|
|
||||||
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
|
|
||||||
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
|
|
||||||
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18n("Mark as Read")
|
text: i18n("Mark as Read")
|
||||||
icon.name: "checkmark"
|
icon.name: "checkmark"
|
||||||
|
enabled: room.notificationCount > 0
|
||||||
onTriggered: room.markAllMessagesAsRead()
|
onTriggered: room.markAllMessagesAsRead()
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuSeparator {}
|
||||||
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
|
|
||||||
icon.name: "edit-copy"
|
|
||||||
onTriggered: if (room.isDirectChat()) {
|
|
||||||
Clipboard.saveText(room.directChatRemoteMember.id);
|
|
||||||
} else if (room.canonicalAlias.length === 0) {
|
|
||||||
Clipboard.saveText(room.id);
|
|
||||||
} else {
|
|
||||||
Clipboard.saveText(room.canonicalAlias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC2.Menu {
|
QQC2.Menu {
|
||||||
title: i18n("Notification State")
|
title: i18nc("@action:inmenu", "Notifications")
|
||||||
icon.name: "notifications"
|
icon.name: "notifications"
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
@@ -107,6 +86,32 @@ Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: room.isFavourite ? i18n("Remove from Favorites") : i18n("Add to Favorites")
|
||||||
|
icon.name: room.isFavourite ? "rating" : "rating-unrated"
|
||||||
|
onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: room.isLowPriority ? i18n("Reprioritize") : i18n("Deprioritize")
|
||||||
|
icon.name: room.isLowPriority ? "arrow-up-symbolic" : "arrow-down-symbolic"
|
||||||
|
onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.MenuSeparator {}
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
text: room.isDirectChat() ? i18nc("@action:inmenu", "Copy user's Matrix ID to Clipboard") : i18nc("@action:inmenu", "Copy Address to Clipboard")
|
||||||
|
icon.name: "edit-copy"
|
||||||
|
onTriggered: if (room.isDirectChat()) {
|
||||||
|
Clipboard.saveText(room.directChatRemoteMember.id);
|
||||||
|
} else if (room.canonicalAlias.length === 0) {
|
||||||
|
Clipboard.saveText(room.id);
|
||||||
|
} else {
|
||||||
|
Clipboard.saveText(room.canonicalAlias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.MenuItem {
|
QQC2.MenuItem {
|
||||||
text: i18nc("@action:inmenu", "Room Settings")
|
text: i18nc("@action:inmenu", "Room Settings")
|
||||||
icon.name: 'settings-configure-symbolic'
|
icon.name: 'settings-configure-symbolic'
|
||||||
@@ -163,7 +168,7 @@ Loader {
|
|||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
KirigamiComponents.Avatar {
|
KirigamiComponents.Avatar {
|
||||||
id: avatar
|
id: avatar
|
||||||
source: room.avatarMediaId ? root.connection.makeMediaUrl("mxc://" + room.avatarMediaId) : ""
|
source: room.avatarMediaUrl
|
||||||
name: room.displayName
|
name: room.displayName
|
||||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls as QQC2
|
import QtQuick.Controls as QQC2
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
import org.kde.kirigami as Kirigami
|
import org.kde.kirigami as Kirigami
|
||||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||||
import org.kde.kirigamiaddons.formcard as FormCard
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
@@ -177,10 +178,25 @@ Loader {
|
|||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.actions
|
model: root.actions
|
||||||
QQC2.MenuItem {
|
DelegateChooser {
|
||||||
visible: modelData.visible
|
role: "separator"
|
||||||
action: modelData
|
DelegateChoice {
|
||||||
onClicked: root.item.close()
|
roleValue: true
|
||||||
|
|
||||||
|
QQC2.MenuSeparator {
|
||||||
|
visible: modelData.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: false
|
||||||
|
|
||||||
|
QQC2.MenuItem {
|
||||||
|
visible: modelData.visible
|
||||||
|
action: modelData
|
||||||
|
onClicked: root.item.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QQC2.Menu {
|
QQC2.Menu {
|
||||||
@@ -341,15 +357,30 @@ Loader {
|
|||||||
id: listViewAction
|
id: listViewAction
|
||||||
model: root.actions
|
model: root.actions
|
||||||
|
|
||||||
FormCard.FormButtonDelegate {
|
DelegateChooser {
|
||||||
icon.name: modelData.icon.name
|
role: "separator"
|
||||||
icon.color: modelData.icon.color ?? undefined
|
DelegateChoice {
|
||||||
enabled: modelData.enabled
|
roleValue: true
|
||||||
visible: modelData.visible
|
|
||||||
text: modelData.text
|
FormCard.FormDelegateSeparator {
|
||||||
onClicked: {
|
visible: modelData.visible
|
||||||
modelData.triggered();
|
}
|
||||||
root.item.close();
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: false
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
icon.name: modelData.icon.name
|
||||||
|
icon.color: modelData.icon.color ?? undefined
|
||||||
|
enabled: modelData.enabled
|
||||||
|
visible: modelData.visible
|
||||||
|
text: modelData.text
|
||||||
|
onClicked: {
|
||||||
|
modelData.triggered();
|
||||||
|
root.item.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
contentItem: KirigamiComponents.Avatar {
|
contentItem: KirigamiComponents.Avatar {
|
||||||
name: root.room ? root.room.displayName : ""
|
name: root.room ? root.room.displayName : ""
|
||||||
source: root.room ? root.room.connection.makeMediaUrl("mxc://" + root.room.avatarMediaId) : ""
|
source: root.room ? root.room.avatarMediaUrl : ""
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: root.room.usesEncryption
|
visible: root.room.usesEncryption
|
||||||
|
|||||||
60
src/qml/EmojiPickerPackHeader.qml
Normal file
60
src/qml/EmojiPickerPackHeader.qml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
|
||||||
|
QQC2.ScrollView {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
|
||||||
|
|
||||||
|
required property var model
|
||||||
|
property int currentIndex: 0
|
||||||
|
readonly property string category: root.model.data(root.model.index(root.currentIndex, 0), ImageContentPackRole.IdentifierRole)
|
||||||
|
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.large + root.QQC2.ScrollBar.horizontal.height
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: categories
|
||||||
|
clip: true
|
||||||
|
focus: true
|
||||||
|
orientation: ListView.Horizontal
|
||||||
|
currentIndex: root.currentIndex
|
||||||
|
|
||||||
|
Keys.onReturnPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
||||||
|
Keys.onEnterPressed: if (emojiGrid.count > 0) emojiGrid.focus = true
|
||||||
|
|
||||||
|
KeyNavigation.down: emojiGrid.count > 0 ? emojiGrid : categories
|
||||||
|
KeyNavigation.tab: emojiGrid.count > 0 ? emojiGrid : categories
|
||||||
|
|
||||||
|
keyNavigationEnabled: true
|
||||||
|
keyNavigationWraps: true
|
||||||
|
Keys.forwardTo: searchField
|
||||||
|
interactive: width !== contentWidth
|
||||||
|
|
||||||
|
Component.onCompleted: categories.forceActiveFocus()
|
||||||
|
|
||||||
|
model: root.model
|
||||||
|
|
||||||
|
delegate: EmojiDelegate {
|
||||||
|
id: packDelegate
|
||||||
|
required property string name
|
||||||
|
required property string i18nName
|
||||||
|
width: Kirigami.Units.iconSizes.large
|
||||||
|
height: width
|
||||||
|
checked: categories.currentIndex === model.index
|
||||||
|
toolTip: packDelegate.i18nName
|
||||||
|
text: packDelegate.name
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/qml/EmojiPickerTypeHeader.qml
Normal file
39
src/qml/EmojiPickerTypeHeader.qml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
Kirigami.NavigationTabBar {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
enum EmoticonType {
|
||||||
|
Emoji,
|
||||||
|
Sticker
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
property var selectedType: EmojiPickerTypeHeader.EmoticonType.Emoji
|
||||||
|
|
||||||
|
background: null
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
id: emojis
|
||||||
|
icon.name: "smiley"
|
||||||
|
text: i18n("Emojis")
|
||||||
|
checked: root.selectedType === EmojiPickerTypeHeader.EmoticonType.Emoji
|
||||||
|
|
||||||
|
onTriggered: root.selectedType = EmojiPickerTypeHeader.EmoticonType.Emoji
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
id: stickers
|
||||||
|
icon.name: "stickers"
|
||||||
|
text: i18n("Stickers")
|
||||||
|
checked: root.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
|
||||||
|
onTriggered: root.selectedType = EmojiPickerTypeHeader.EmoticonType.Sticker
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -42,30 +42,36 @@ DelegateContextMenu {
|
|||||||
* Each action will be instantiated as a single line in the menu.
|
* Each action will be instantiated as a single line in the menu.
|
||||||
*/
|
*/
|
||||||
property list<Kirigami.Action> actions: [
|
property list<Kirigami.Action> actions: [
|
||||||
|
DelegateContextMenu.ReplyMessageAction {},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Open Externally")
|
separator: true
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
text: i18nc("@action:inmenu", "Open Image")
|
||||||
icon.name: "document-open"
|
icon.name: "document-open"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
currentRoom.openEventMediaExternally(root.eventId);
|
currentRoom.openEventMediaExternally(root.eventId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Save As")
|
text: i18nc("@action:inmenu", "Save Image…")
|
||||||
icon.name: "document-save"
|
icon.name: "document-save"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
|
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);
|
||||||
|
dialog.selectedFile = currentRoom.fileNameToDownload(eventId);
|
||||||
dialog.open();
|
dialog.open();
|
||||||
dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DelegateContextMenu.ReplyMessageAction {},
|
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Copy")
|
text: i18nc("@action:inmenu", "Copy Image")
|
||||||
icon.name: "edit-copy"
|
icon.name: "edit-copy"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
currentRoom.copyEventMedia(root.eventId);
|
currentRoom.copyEventMedia(root.eventId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
separator: true
|
||||||
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
|
visible: author.id === currentRoom.localMember.id || currentRoom.canSendState("redact")
|
||||||
text: i18n("Remove")
|
text: i18n("Remove")
|
||||||
@@ -88,7 +94,13 @@ DelegateContextMenu {
|
|||||||
},
|
},
|
||||||
DelegateContextMenu.ReportMessageAction {},
|
DelegateContextMenu.ReportMessageAction {},
|
||||||
DelegateContextMenu.ShowUserAction {},
|
DelegateContextMenu.ShowUserAction {},
|
||||||
DelegateContextMenu.ViewSourceAction {}
|
Kirigami.Action {
|
||||||
|
separator: true
|
||||||
|
visible: viewSourceAction.visible
|
||||||
|
},
|
||||||
|
DelegateContextMenu.ViewSourceAction {
|
||||||
|
id: viewSourceAction
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user