Compare commits
29 Commits
work/tobia
...
v24.11.80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd38fb435 | ||
|
|
a5b999e682 | ||
|
|
41d34fc0e4 | ||
|
|
b51194f90f | ||
|
|
80ac9e1ba7 | ||
|
|
e3874c824a | ||
|
|
6599c6b609 | ||
|
|
13d522221c | ||
|
|
dd8f926f32 | ||
|
|
258312e798 | ||
|
|
43d40c7e75 | ||
|
|
cbcc9a6514 | ||
|
|
625048610b | ||
|
|
fa47b67e3d | ||
|
|
9347a66acf | ||
|
|
317df56ffa | ||
|
|
fed9197716 | ||
|
|
1e892599e9 | ||
|
|
b7229ca0cf | ||
|
|
953b711823 | ||
|
|
01d903efd3 | ||
|
|
241dd81932 | ||
|
|
f6dfe0cbcf | ||
|
|
f10b97139c | ||
|
|
385c5b3405 | ||
|
|
7bc6f906f8 | ||
|
|
b8b1434a95 | ||
|
|
85c7a4bcb3 | ||
|
|
84b698a7e8 |
@@ -7,9 +7,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
# KDE Applications version, managed by release script.
|
# KDE Applications version, managed by release script.
|
||||||
set(RELEASE_SERVICE_VERSION_MAJOR "25")
|
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
||||||
set(RELEASE_SERVICE_VERSION_MINOR "03")
|
set(RELEASE_SERVICE_VERSION_MINOR "11")
|
||||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
set(RELEASE_SERVICE_VERSION_MICRO "80")
|
||||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||||
|
|
||||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#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"
|
||||||
@@ -76,6 +77,7 @@ 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"));
|
||||||
}
|
}
|
||||||
@@ -533,7 +535,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"), {}}};
|
||||||
|
|||||||
@@ -59,7 +59,6 @@
|
|||||||
<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="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>
|
||||||
@@ -288,7 +287,6 @@
|
|||||||
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
||||||
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
||||||
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
||||||
<value key="KDE::supporters">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>
|
||||||
|
|||||||
@@ -90,22 +90,16 @@ GenericName[zh_TW]=Matrix 用戶端
|
|||||||
Comment=Chat on Matrix
|
Comment=Chat on Matrix
|
||||||
Comment[ca]=Xat a Matrix
|
Comment[ca]=Xat a Matrix
|
||||||
Comment[ca@valencia]=Xat a Matrix
|
Comment[ca@valencia]=Xat a Matrix
|
||||||
Comment[en_GB]=Chat on Matrix
|
|
||||||
Comment[es]=Chat en Matrix
|
Comment[es]=Chat en Matrix
|
||||||
Comment[eu]=Berriketa Matrix-en
|
Comment[eu]=Berriketa Matrix-en
|
||||||
Comment[fr]=Clavarder sur Matrix
|
Comment[fr]=Clavarder sur Matrix
|
||||||
Comment[gl]=Charle en Matrix
|
Comment[gl]=Charle en Matrix
|
||||||
Comment[hu]=Csevegés Matrixon
|
Comment[hu]=Csevegés Matrixon
|
||||||
Comment[ia]=Conversation en ditecto sur Matrix
|
|
||||||
Comment[it]= su Matrix
|
Comment[it]= su Matrix
|
||||||
Comment[ka]=ჩატი Matrix-ზე
|
|
||||||
Comment[nl]=Chat op Matrix
|
|
||||||
Comment[pl]=Rozmawiaj na Matriksie
|
Comment[pl]=Rozmawiaj na Matriksie
|
||||||
Comment[sl]=Klepet na Matrixu
|
Comment[sl]=Klepet na Matrixu
|
||||||
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
|
|
||||||
Comment[tr]=Matrix Üzerinde Sohbet Et
|
Comment[tr]=Matrix Üzerinde Sohbet Et
|
||||||
Comment[uk]=Спілкування у Matrix
|
Comment[uk]=Спілкування у Matrix
|
||||||
Comment[x-test]=xxChat on Matrixxx
|
|
||||||
MimeType=x-scheme-handler/matrix;
|
MimeType=x-scheme-handler/matrix;
|
||||||
Exec=neochat %u
|
Exec=neochat %u
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
413
po/ar/neochat.po
413
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
407
po/az/neochat.po
407
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
693
po/ca/neochat.po
693
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
411
po/cs/neochat.po
411
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
401
po/da/neochat.po
401
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/de/neochat.po
413
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
410
po/el/neochat.po
410
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
415
po/eo/neochat.po
415
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/es/neochat.po
413
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
393
po/eu/neochat.po
393
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
414
po/fi/neochat.po
414
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
415
po/fr/neochat.po
415
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/gl/neochat.po
413
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/hu/neochat.po
413
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
424
po/ia/neochat.po
424
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
417
po/id/neochat.po
417
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
401
po/ie/neochat.po
401
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
415
po/it/neochat.po
415
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
366
po/ja/neochat.po
366
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
400
po/ka/neochat.po
400
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
416
po/ko/neochat.po
416
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
366
po/lt/neochat.po
366
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/lv/neochat.po
413
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
402
po/nl/neochat.po
402
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
416
po/nn/neochat.po
416
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
407
po/pa/neochat.po
407
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
415
po/pl/neochat.po
415
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
417
po/pt/neochat.po
417
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
409
po/ru/neochat.po
409
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1000
po/sk/neochat.po
1000
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
393
po/sl/neochat.po
393
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
413
po/sv/neochat.po
413
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
438
po/ta/neochat.po
438
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
413
po/tr/neochat.po
413
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
397
po/uk/neochat.po
397
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,6 +10,12 @@ 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
|
||||||
@@ -20,6 +26,8 @@ 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
|
||||||
@@ -42,6 +50,8 @@ 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
|
||||||
@@ -91,6 +101,10 @@ 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
|
||||||
@@ -180,32 +194,6 @@ add_library(neochat STATIC
|
|||||||
models/threadmodel.h
|
models/threadmodel.h
|
||||||
enums/messagetype.h
|
enums/messagetype.h
|
||||||
messagecomponent.h
|
messagecomponent.h
|
||||||
imagecontentmanager.h
|
|
||||||
imagecontentmanager.cpp
|
|
||||||
models/imagecontentmodel.cpp
|
|
||||||
models/imagecontentmodel.h
|
|
||||||
models/emojipacksmodel.cpp
|
|
||||||
models/emojipacksmodel.h
|
|
||||||
models/accountimagepackmodel.cpp
|
|
||||||
models/accountimagepackmodel.h
|
|
||||||
models/historyimagepackmodel.cpp
|
|
||||||
models/historyimagepackmodel.h
|
|
||||||
models/imagepacksproxymodel.cpp
|
|
||||||
models/imagepacksproxymodel.h
|
|
||||||
models/imagepacksmodel.cpp
|
|
||||||
models/imagepacksmodel.h
|
|
||||||
models/recentimagecontentmodel.h
|
|
||||||
models/recentimagecontentmodel.cpp
|
|
||||||
models/recentimagecontentproxymodel.h
|
|
||||||
models/recentimagecontentproxymodel.cpp
|
|
||||||
models/allimagecontentmodel.h
|
|
||||||
models/allimagecontentmodel.cpp
|
|
||||||
models/roomimagepacksmodel.h
|
|
||||||
models/roomimagepacksmodel.cpp
|
|
||||||
models/imagepackroomsmodel.h
|
|
||||||
models/imagepackroomsmodel.cpp
|
|
||||||
models/imagecontentfiltermodel.h
|
|
||||||
models/imagecontentfiltermodel.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||||
@@ -294,9 +282,6 @@ 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
|
||||||
@@ -313,12 +298,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
|||||||
org.kde.neochat.chatbar
|
org.kde.neochat.chatbar
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_resources(neochat "emoji"
|
|
||||||
PREFIX "/"
|
|
||||||
FILES
|
|
||||||
data/emojis.json
|
|
||||||
)
|
|
||||||
|
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(timeline)
|
add_subdirectory(timeline)
|
||||||
add_subdirectory(devtools)
|
add_subdirectory(devtools)
|
||||||
|
|||||||
@@ -176,14 +176,13 @@ 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
|
||||||
@@ -321,11 +320,12 @@ QQC2.Control {
|
|||||||
id: actionsRow
|
id: actionsRow
|
||||||
spacing: 0
|
spacing: 0
|
||||||
Layout.alignment: Qt.AlignBottom
|
Layout.alignment: Qt.AlignBottom
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
|
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
|
||||||
|
|
||||||
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,6 +342,7 @@ QQC2.Control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateSizeHelper {
|
DelegateSizeHelper {
|
||||||
id: chatBarSizeHelper
|
id: chatBarSizeHelper
|
||||||
startBreakpoint: Kirigami.Units.gridUnit * 46
|
startBreakpoint: Kirigami.Units.gridUnit * 46
|
||||||
@@ -519,6 +520,7 @@ QQC2.Control {
|
|||||||
y: -implicitHeight
|
y: -implicitHeight
|
||||||
|
|
||||||
modal: false
|
modal: false
|
||||||
|
includeCustom: true
|
||||||
closeOnChosen: false
|
closeOnChosen: false
|
||||||
|
|
||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
|
|||||||
@@ -5,30 +5,56 @@ 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.Button {
|
QQC2.ItemDelegate {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property string toolTip
|
property string name
|
||||||
|
property string emoji
|
||||||
property bool showTones: false
|
property bool showTones: false
|
||||||
|
property bool isImage: false
|
||||||
|
|
||||||
QQC2.ToolTip.text: toolTip
|
QQC2.ToolTip.text: root.name
|
||||||
QQC2.ToolTip.visible: hovered
|
QQC2.ToolTip.visible: hovered && root.name !== ""
|
||||||
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
|
||||||
|
|
||||||
flat: true
|
contentItem: Item {
|
||||||
|
Kirigami.Heading {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !root.emoji.startsWith("mxc") && !root.isImage
|
||||||
|
text: root.emoji
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
font.family: "emoji"
|
||||||
|
|
||||||
contentItem: Kirigami.Heading {
|
Kirigami.Icon {
|
||||||
text: root.text
|
width: Kirigami.Units.gridUnit * 0.5
|
||||||
horizontalAlignment: Text.AlignHCenter
|
height: Kirigami.Units.gridUnit * 0.5
|
||||||
verticalAlignment: Text.AlignVCenter
|
source: "arrow-down-symbolic"
|
||||||
|
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 : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Kirigami.Icon {
|
background: Rectangle {
|
||||||
width: Kirigami.Units.gridUnit * 0.5
|
color: root.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
|
||||||
height: Kirigami.Units.gridUnit * 0.5
|
radius: Kirigami.Units.cornerRadius
|
||||||
source: "arrow-down-symbolic"
|
|
||||||
anchors.bottom: parent.bottom
|
Rectangle {
|
||||||
anchors.right: parent.right
|
radius: Kirigami.Units.cornerRadius
|
||||||
visible: root.showTones
|
anchors.fill: parent
|
||||||
|
color: Kirigami.Theme.highlightColor
|
||||||
|
opacity: root.hovered && !root.pressed ? 0.2 : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ 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)
|
||||||
|
|
||||||
@@ -62,15 +64,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.implicitWidth + 2 * padding, applicationWindow().width)
|
width: Math.min(contentItem.categoryIconSize * 11 + 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,17 +1,20 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2022 Tobias Fella
|
||||||
// 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
|
||||||
|
|
||||||
readonly property int emojisPerRow: emojis.width / Kirigami.Units.iconSizes.large
|
property alias model: emojis.model
|
||||||
|
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
|
||||||
|
|
||||||
@@ -22,8 +25,6 @@ QQC2.ScrollView {
|
|||||||
emojis.forceActiveFocus();
|
emojis.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
width: Kirigami.Units.gridUnit * 24
|
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
id: emojis
|
id: emojis
|
||||||
|
|
||||||
@@ -40,9 +41,7 @@ QQC2.ScrollView {
|
|||||||
onModelChanged: currentIndex = -1
|
onModelChanged: currentIndex = -1
|
||||||
|
|
||||||
cellWidth: emojis.width / root.emojisPerRow
|
cellWidth: emojis.width / root.emojisPerRow
|
||||||
cellHeight: Kirigami.Units.iconSizes.large
|
cellHeight: root.targetIconSize
|
||||||
|
|
||||||
model: EmojiModelManager.emojiModel
|
|
||||||
|
|
||||||
KeyNavigation.up: root.header
|
KeyNavigation.up: root.header
|
||||||
|
|
||||||
@@ -50,49 +49,49 @@ QQC2.ScrollView {
|
|||||||
|
|
||||||
delegate: EmojiDelegate {
|
delegate: EmojiDelegate {
|
||||||
id: emojiDelegate
|
id: emojiDelegate
|
||||||
|
checked: emojis.currentIndex === model.index
|
||||||
required property string unicode
|
emoji: !!modelData ? modelData.unicode : model.url
|
||||||
required property string identifier
|
name: !!modelData ? modelData.shortName : model.body
|
||||||
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 (!showTones) {
|
if (EmojiModel.tones(modelData.shortName).length === 0) {
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// let tones = Qt.createComponent("org.kde.neochat", "EmojiTonesPicker").createObject(emojiDelegate, {
|
let tones = tonesPopupComponent.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.open();
|
tones.forceActiveFocus();
|
||||||
// tones.forceActiveFocus();
|
}
|
||||||
// }
|
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
|
||||||
// showTones: model.hasTones
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Kirigami.PlaceholderMessage {
|
Kirigami.PlaceholderMessage {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
|
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
|
||||||
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-2023 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2022 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,7 +6,6 @@ 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
|
||||||
@@ -14,29 +13,86 @@ ColumnLayout {
|
|||||||
/**
|
/**
|
||||||
* @brief The current room that user is viewing.
|
* @brief The current room that user is viewing.
|
||||||
*/
|
*/
|
||||||
required property NeoChatRoom currentRoom
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiPickerTypeHeader {
|
spacing: 0
|
||||||
id: emoticonPickerTypeHeader
|
|
||||||
|
|
||||||
|
Kirigami.NavigationTabBar {
|
||||||
|
id: types
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
onSelectedTypeChanged: emoticonPickerCategoryHeader.currentIndex = 0
|
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiPickerPackHeader {
|
QQC2.ScrollView {
|
||||||
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
|
||||||
|
|
||||||
model: UnicodeEmoticonManager.categories
|
ListView {
|
||||||
|
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 {
|
||||||
@@ -48,34 +104,114 @@ 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: emoticonPickerCategoryHeader
|
header: categories
|
||||||
Keys.forwardTo: searchField
|
Keys.forwardTo: searchField
|
||||||
stickers: emoticonPickerTypeHeader.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
|
stickers: root.selectedType === 1
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickReaction {
|
QQC2.ScrollView {
|
||||||
id: quickReaction
|
visible: showQuickReaction
|
||||||
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: 2024 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2022 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
|
||||||
text: modelData.unicode
|
emoji: modelData.unicode
|
||||||
toolTip: modelData.shortName
|
name: modelData.shortName
|
||||||
|
|
||||||
width: root.categoryIconSize
|
width: root.categoryIconSize
|
||||||
height: width
|
height: width
|
||||||
|
|||||||
55269
src/data/emojis.json
55269
src/data/emojis.json
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: 2024 Emojibase
|
|
||||||
SPDX-License-Identifier: MIT
|
|
||||||
1857
src/emojis.h
Normal file
1857
src/emojis.h
Normal file
File diff suppressed because it is too large
Load Diff
9
src/emojitones.cpp
Normal file
9
src/emojitones.cpp
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// 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"
|
||||||
|
};
|
||||||
21
src/emojitones.h
Normal file
21
src/emojitones.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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;
|
||||||
|
};
|
||||||
1784
src/emojitones_data.h
Normal file
1784
src/emojitones_data.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,334 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QFile>
|
|
||||||
|
|
||||||
#include <KConfigGroup>
|
|
||||||
#include <KSharedConfig>
|
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
#include "events/imagepackevent.h"
|
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
#include <Quotient/connection.h>
|
|
||||||
|
|
||||||
#define connection Controller::instance().activeConnection()
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
ImageContentManager::ImageContentManager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
|
|
||||||
static Connection *oldActiveConnection = nullptr;
|
|
||||||
disconnect(oldActiveConnection, nullptr, this, nullptr);
|
|
||||||
oldActiveConnection = Controller::instance().activeConnection();
|
|
||||||
setupConnection();
|
|
||||||
});
|
|
||||||
|
|
||||||
loadEmojis();
|
|
||||||
loadEmojiHistory();
|
|
||||||
|
|
||||||
setupConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::loadEmojis()
|
|
||||||
{
|
|
||||||
QFile file(":/data/emojis.json"_ls);
|
|
||||||
file.open(QFile::ReadOnly);
|
|
||||||
Q_ASSERT(file.isOpen());
|
|
||||||
auto data = QJsonDocument::fromJson(file.readAll()).array();
|
|
||||||
|
|
||||||
for (const auto &emoji : data) {
|
|
||||||
// TODO
|
|
||||||
// m_emojiPacks += ImagePackDescription{
|
|
||||||
// .description = parts[1],
|
|
||||||
// .attribution = {},
|
|
||||||
// .icon = parts[0],
|
|
||||||
// .type = ImagePackDescription::Emoji,
|
|
||||||
// .roomId = {},
|
|
||||||
// .stateKey = parts[2],
|
|
||||||
// };
|
|
||||||
|
|
||||||
m_emojis[u"TODO"_s] += Emoji{
|
|
||||||
.text = emoji[u"icon"_s].toString(),
|
|
||||||
.displayName = emoji[u"label"_s].toString(),
|
|
||||||
.shortName = emoji[u"label"_s].toString(), // TODO
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::loadEmojiHistory()
|
|
||||||
{
|
|
||||||
auto config = KSharedConfig::openStateConfig();
|
|
||||||
auto group = config->group("RecentEmojis"_ls);
|
|
||||||
for (const auto &key : group.keyList()) {
|
|
||||||
m_usages[key] = group.readEntry(key).toInt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::setupConnection()
|
|
||||||
{
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, [this](const QString &type) {
|
|
||||||
if (type == "im.ponies.user_emotes"_ls) {
|
|
||||||
loadAccountImages();
|
|
||||||
}
|
|
||||||
if (type == "im.ponies.emote_rooms"_ls) {
|
|
||||||
loadGlobalPacks();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loadAccountImages();
|
|
||||||
loadGlobalPacks();
|
|
||||||
|
|
||||||
m_roomPacks.clear();
|
|
||||||
|
|
||||||
for (const auto &room : connection->allRooms()) {
|
|
||||||
setupRoom(static_cast<NeoChatRoom *>(room));
|
|
||||||
}
|
|
||||||
connect(connection, &Connection::joinedRoom, this, [this](const auto &room) {
|
|
||||||
setupRoom(static_cast<NeoChatRoom *>(room));
|
|
||||||
});
|
|
||||||
connect(connection, &Connection::leftRoom, this, [this](const auto &room) {
|
|
||||||
cleanupRoom(static_cast<NeoChatRoom *>(room));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVector<ImagePackDescription> &ImageContentManager::emojiPacks() const
|
|
||||||
{
|
|
||||||
return m_emojiPacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QHash<QString, QVector<Emoji>> &ImageContentManager::emojis() const
|
|
||||||
{
|
|
||||||
return m_emojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::loadAccountImages()
|
|
||||||
{
|
|
||||||
m_accountImages.clear();
|
|
||||||
if (connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
|
||||||
m_accountImages = ImagePackEventContent(connection->accountData("im.ponies.user_emotes"_ls)->contentJson()).images;
|
|
||||||
}
|
|
||||||
Q_EMIT accountImagesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVector<ImagePackEventContent::ImagePackImage> &ImageContentManager::accountImages() const
|
|
||||||
{
|
|
||||||
return m_accountImages;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::emojiUsed(const QString &text)
|
|
||||||
{
|
|
||||||
if (!m_usages.contains(text)) {
|
|
||||||
m_usages[text] = 0;
|
|
||||||
}
|
|
||||||
m_usages[text]++;
|
|
||||||
Q_EMIT recentEmojisChanged();
|
|
||||||
auto config = KSharedConfig::openStateConfig();
|
|
||||||
auto group = config->group("RecentEmojis"_ls);
|
|
||||||
for (const auto &key : m_usages.keys()) {
|
|
||||||
group.writeEntry(key, m_usages[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Emoji ImageContentManager::emojiForText(const QString &text)
|
|
||||||
{
|
|
||||||
for (const auto &category : m_emojis.values()) {
|
|
||||||
for (const auto &emoji : category) {
|
|
||||||
if (emoji.text == text) {
|
|
||||||
return emoji;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const auto &withSelector = QString::fromUtf8(text.toUtf8() + QByteArrayLiteral("\xEF\xB8\x8F"));
|
|
||||||
for (const auto &category : m_emojis.values()) {
|
|
||||||
for (const auto &emoji : category) {
|
|
||||||
if (emoji.text == withSelector) {
|
|
||||||
return emoji;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const QMap<QString, uint32_t> &ImageContentManager::recentEmojis() const
|
|
||||||
{
|
|
||||||
return m_usages;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QMap<QString, QMap<QString, ImagePackDescription>> &ImageContentManager::roomImagePacks() const
|
|
||||||
{
|
|
||||||
return m_roomPacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::loadRoomImagePacks(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
const auto &events = room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
|
|
||||||
m_roomPacks[room->id()].clear();
|
|
||||||
for (const auto &event : events) {
|
|
||||||
auto content = ImagePackEventContent(event->contentJson());
|
|
||||||
auto avatarMxc = event->contentPart<QJsonObject>("pack"_ls)["avatar_url"_ls].toString();
|
|
||||||
if (avatarMxc.isEmpty()) {
|
|
||||||
const auto &images = event->contentPart<QJsonObject>("images"_ls);
|
|
||||||
if (images.size() > 0) {
|
|
||||||
avatarMxc = images[images.keys()[0]]["url"_ls].toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const auto &avatarUrl = avatarMxc.isEmpty() ? QString() : Controller::instance().activeConnection()->makeMediaUrl(QUrl(avatarMxc)).toString();
|
|
||||||
|
|
||||||
ImagePackDescription::Type type = ImagePackDescription::Both;
|
|
||||||
if (!content.pack || !content.pack->usage || content.pack->usage->isEmpty()
|
|
||||||
|| (content.pack->usage->contains("emoticon"_ls) && content.pack->usage->contains("sticker"_ls))) {
|
|
||||||
type = ImagePackDescription::Both;
|
|
||||||
} else if (content.pack->usage->contains("sticker"_ls)) {
|
|
||||||
type = ImagePackDescription::Sticker;
|
|
||||||
} else {
|
|
||||||
type = ImagePackDescription::CustomEmoji;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_roomPacks[room->id()][event->stateKey()] = ImagePackDescription{
|
|
||||||
.description = event->contentPart<QJsonObject>("pack"_ls)["display_name"_ls].toString(),
|
|
||||||
.attribution = {},
|
|
||||||
.icon = QStringLiteral("<img src=\"%1\" width=\"32\" height=\"32\"/>").arg(avatarUrl),
|
|
||||||
.type = type,
|
|
||||||
.roomId = room->id(),
|
|
||||||
.stateKey = event->stateKey(),
|
|
||||||
};
|
|
||||||
m_roomImages[{room->id(), event->stateKey()}] = content.images;
|
|
||||||
}
|
|
||||||
Q_EMIT roomImagePacksChanged(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoomImages &ImageContentManager::roomImages() const
|
|
||||||
{
|
|
||||||
return m_roomImages;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QVector<std::pair<QString, QString>> &ImageContentManager::globalPacks() const
|
|
||||||
{
|
|
||||||
return m_globalPacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::loadGlobalPacks()
|
|
||||||
{
|
|
||||||
if (!connection->hasAccountData("im.ponies.emote_rooms"_ls)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_globalPacks.clear();
|
|
||||||
const auto &rooms = Controller::instance().activeConnection()->accountData("im.ponies.emote_rooms"_ls)->contentPart<QJsonObject>("rooms"_ls);
|
|
||||||
for (const auto &roomId : rooms.keys()) {
|
|
||||||
for (const auto &stateKey : rooms[roomId].toObject().keys()) {
|
|
||||||
m_globalPacks += {roomId, stateKey};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Q_EMIT globalPacksChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::setupRoom(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
connect(room, &Room::changed, this, [this, room]() {
|
|
||||||
loadRoomImagePacks(room);
|
|
||||||
});
|
|
||||||
loadRoomImagePacks(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentManager::cleanupRoom(NeoChatRoom *room)
|
|
||||||
{
|
|
||||||
m_roomPacks.remove(room->id());
|
|
||||||
Q_EMIT roomImagePacksChanged(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentManager::mxcForShortCode(const QString &shortcode) const
|
|
||||||
{
|
|
||||||
for (const auto &image : m_accountImages) {
|
|
||||||
if (image.shortcode == shortcode) {
|
|
||||||
return Controller::instance().activeConnection()->makeMediaUrl(image.url).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &id : m_roomImages.keys()) {
|
|
||||||
for (const auto &image : m_roomImages[id]) {
|
|
||||||
if (image.shortcode == shortcode) {
|
|
||||||
return Controller::instance().activeConnection()->makeMediaUrl(image.url).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentManager::bodyForShortCode(const QString &shortcode) const
|
|
||||||
{
|
|
||||||
for (const auto &image : m_accountImages) {
|
|
||||||
if (image.shortcode == shortcode) {
|
|
||||||
return image.body.value_or(QString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &id : m_roomImages.keys()) {
|
|
||||||
for (const auto &image : m_roomImages[id]) {
|
|
||||||
if (image.shortcode == shortcode) {
|
|
||||||
return image.body.value_or(QString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImageContentManager::isEmojiShortCode(const QString &shortCode) const
|
|
||||||
{
|
|
||||||
for (const auto &image : m_accountImages) {
|
|
||||||
if (image.shortcode == shortCode) {
|
|
||||||
return !image.usage || image.usage->isEmpty() || image.usage->contains("emoticon"_ls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &id : m_roomImages.keys()) {
|
|
||||||
for (const auto &image : m_roomImages[id]) {
|
|
||||||
if (image.shortcode == shortCode) {
|
|
||||||
const auto pack = m_roomPacks[id.first][id.second];
|
|
||||||
return pack.type == ImagePackDescription::Emoji || pack.type == ImagePackDescription::Both;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImageContentManager::isStickerShortCode(const QString &shortCode) const
|
|
||||||
{
|
|
||||||
for (const auto &image : m_accountImages) {
|
|
||||||
if (image.shortcode == shortCode) {
|
|
||||||
return !image.usage || image.usage->isEmpty() || image.usage->contains("sticker"_ls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &id : m_roomImages.keys()) {
|
|
||||||
for (const auto &image : m_roomImages[id]) {
|
|
||||||
if (image.shortcode == shortCode) {
|
|
||||||
const auto pack = m_roomPacks[id.first][id.second];
|
|
||||||
return pack.type == ImagePackDescription::Sticker || pack.type == ImagePackDescription::Both;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentManager::accountImagesAvatar() const
|
|
||||||
{
|
|
||||||
if (!connection->hasAccountData("im.ponies.user_emotes"_ls)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const auto &event = ImagePackEventContent(connection->accountData("im.ponies.user_emotes"_ls)->contentJson());
|
|
||||||
QString avatarUrl;
|
|
||||||
if (event.pack) {
|
|
||||||
avatarUrl = event.pack->avatarUrl.value_or(QUrl()).toString();
|
|
||||||
}
|
|
||||||
if (avatarUrl.isEmpty()) {
|
|
||||||
//TODO avatarUrl = Controller::instance().activeConnection()->user()->avatarUrl().toString();
|
|
||||||
}
|
|
||||||
if (avatarUrl.isEmpty()) {
|
|
||||||
avatarUrl = event.images[0].url.toString();
|
|
||||||
}
|
|
||||||
return QStringLiteral("👤");
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QHash>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
#include "events/imagepackevent.h"
|
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
#define imageContentManager ImageContentManager::instance()
|
|
||||||
|
|
||||||
class ImageContentRole : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
enum ImageRoles {
|
|
||||||
DisplayNameRole = Qt::DisplayRole, /**< The name of the emoji. */
|
|
||||||
EmojiRole, /**< The unicode character of the emoji. */
|
|
||||||
ShortCodeRole,
|
|
||||||
IsCustomRole,
|
|
||||||
IsStickerRole,
|
|
||||||
IsEmojiRole,
|
|
||||||
UsageCountRole,
|
|
||||||
HasTonesRole,
|
|
||||||
};
|
|
||||||
Q_ENUM(ImageRoles);
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImageContentPackRole : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
QML_UNCREATABLE("")
|
|
||||||
|
|
||||||
public:
|
|
||||||
//! Roles for the various models providing image packs.
|
|
||||||
enum ImagePackRoles {
|
|
||||||
DisplayNameRole = Qt::DisplayRole, //! Textual desription of the pack.
|
|
||||||
IconRole, //! Icon for the pack. For emojis, this is a unicode emoji; For custom emojis and stickers, this is a HTML image.
|
|
||||||
IdentifierRole, //! An internal, mostly opaque identifier for the model.
|
|
||||||
IsEmojiRole, //! Whether this pack contains emojis (including custom). For the account pack, this is true if the pack contains any emojis; for room
|
|
||||||
//! packs, this *only* considers the pack-level usage parameter
|
|
||||||
IsStickerRole, //! Equivalent to IsEmojiRole, but for stickers.
|
|
||||||
IsEmptyRole, //! Whether this image pack is empty.
|
|
||||||
IsGlobalPackRole, //! Whether this pack is enabled globally.
|
|
||||||
};
|
|
||||||
Q_ENUM(ImagePackRoles);
|
|
||||||
};
|
|
||||||
|
|
||||||
using RoomImages = QMap<std::pair<QString, QString>, QVector<Quotient::ImagePackEventContent::ImagePackImage>>;
|
|
||||||
|
|
||||||
struct Emoji {
|
|
||||||
Q_GADGET
|
|
||||||
Q_PROPERTY(QString text MEMBER text)
|
|
||||||
Q_PROPERTY(QString displayName MEMBER displayName)
|
|
||||||
Q_PROPERTY(QString shortName MEMBER shortName)
|
|
||||||
public:
|
|
||||||
QString text;
|
|
||||||
QString displayName;
|
|
||||||
QString shortName;
|
|
||||||
};
|
|
||||||
Q_DECLARE_METATYPE(Emoji)
|
|
||||||
|
|
||||||
struct ImagePackDescription {
|
|
||||||
enum Type {
|
|
||||||
Emoji,
|
|
||||||
CustomEmoji,
|
|
||||||
Sticker,
|
|
||||||
Both,
|
|
||||||
};
|
|
||||||
QString description;
|
|
||||||
QString attribution;
|
|
||||||
QString icon;
|
|
||||||
Type type;
|
|
||||||
// Only relevant for packs coming from rooms
|
|
||||||
QString roomId;
|
|
||||||
QString stateKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class manages emojis, custom emojis, and stickers. Because naming things is hard, it has the most generic name possible.
|
|
||||||
*/
|
|
||||||
class ImageContentManager : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
QML_SINGLETON
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Returns the global instance of ImageContentManager.
|
|
||||||
static ImageContentManager &instance()
|
|
||||||
{
|
|
||||||
static ImageContentManager _instance;
|
|
||||||
return _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
//! Returns a list of emoji packs (categories, e.g., food, smileys, etc.)
|
|
||||||
const QVector<ImagePackDescription> &emojiPacks() const;
|
|
||||||
//! Returns a map roomId -> stateKey -> description for all image packs that exist in rooms.
|
|
||||||
const QMap<QString, QMap<QString, ImagePackDescription>> &roomImagePacks() const;
|
|
||||||
//! Returns an list (roomId, stateKey) for all globally enabled room packs.
|
|
||||||
//! This is not filtered for rooms or stateKeys that do not exist. This is left to ImagePacksProxyModel
|
|
||||||
const QVector<std::pair<QString, QString>> &globalPacks() const;
|
|
||||||
|
|
||||||
//! Returns a map pack key -> [emoji] for all (normal) emojis.
|
|
||||||
const QHash<QString, QVector<Emoji>> &emojis() const;
|
|
||||||
|
|
||||||
//! Returns a list of all account images.
|
|
||||||
const QVector<Quotient::ImagePackEventContent::ImagePackImage> &accountImages() const;
|
|
||||||
|
|
||||||
//! Returns a map roomId -> stateKey -> [image] of all images part of a room image pack.
|
|
||||||
const RoomImages &roomImages() const;
|
|
||||||
|
|
||||||
//! Returns a map emoji -> usage count to be used as an emoji history.
|
|
||||||
const QMap<QString, uint32_t> &recentEmojis() const;
|
|
||||||
|
|
||||||
//! Returns the emoji object for the given unicode symbol.
|
|
||||||
Emoji emojiForText(const QString &text);
|
|
||||||
|
|
||||||
//! Updates the history when an emoji is used.
|
|
||||||
Q_INVOKABLE void emojiUsed(const QString &text);
|
|
||||||
|
|
||||||
QString mxcForShortCode(const QString &shortcode) const;
|
|
||||||
QString bodyForShortCode(const QString &shortcode) const;
|
|
||||||
bool isEmojiShortCode(const QString &shortCode) const;
|
|
||||||
bool isStickerShortCode(const QString &shortCode) const;
|
|
||||||
|
|
||||||
QString accountImagesAvatar() const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void accountImagesChanged();
|
|
||||||
void recentEmojisChanged();
|
|
||||||
void roomImagePacksChanged(NeoChatRoom *room);
|
|
||||||
void globalPacksChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Packs
|
|
||||||
QVector<ImagePackDescription> m_emojiPacks;
|
|
||||||
// [roomId, stateKey]
|
|
||||||
QVector<std::pair<QString, QString>> m_globalPacks;
|
|
||||||
// roomId -> stateKey -> description
|
|
||||||
QMap<QString, QMap<QString, ImagePackDescription>> m_roomPacks;
|
|
||||||
|
|
||||||
// Emojis
|
|
||||||
// pack name -> emojis
|
|
||||||
QHash<QString, QVector<Emoji>> m_emojis;
|
|
||||||
QVector<Quotient::ImagePackEventContent::ImagePackImage> m_accountImages;
|
|
||||||
RoomImages m_roomImages;
|
|
||||||
|
|
||||||
// History
|
|
||||||
// emoji -> usage count
|
|
||||||
QMap<QString, uint32_t> m_usages;
|
|
||||||
|
|
||||||
// Loads both emojis and emoji packs
|
|
||||||
void loadEmojis();
|
|
||||||
void loadGlobalPacks();
|
|
||||||
void loadRoomImagePacks(NeoChatRoom *room);
|
|
||||||
|
|
||||||
void loadEmojiHistory();
|
|
||||||
|
|
||||||
void loadAccountImages();
|
|
||||||
void loadRoomImages();
|
|
||||||
|
|
||||||
ImageContentManager(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
void setupConnection();
|
|
||||||
void setupRoom(NeoChatRoom *room);
|
|
||||||
void cleanupRoom(NeoChatRoom *room);
|
|
||||||
};
|
|
||||||
@@ -13,7 +13,6 @@ 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,7 +7,6 @@ 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
|
||||||
@@ -91,27 +90,11 @@ Kirigami.Page {
|
|||||||
id: loadedAccounts
|
id: loadedAccounts
|
||||||
model: AccountRegistry
|
model: AccountRegistry
|
||||||
delegate: FormCard.FormButtonDelegate {
|
delegate: FormCard.FormButtonDelegate {
|
||||||
id: delegate
|
text: model.userId
|
||||||
|
|
||||||
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 = delegate.connection;
|
Controller.activeConnection = model.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 {
|
||||||
|
|||||||
@@ -62,8 +62,6 @@
|
|||||||
#include "fakerunner.h"
|
#include "fakerunner.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
#ifdef Q_OS_WINDOWS
|
#ifdef Q_OS_WINDOWS
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
104
src/models/accountemoticonmodel.h
Normal file
104
src/models/accountemoticonmodel.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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();
|
||||||
|
};
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "accountimagepackmodel.h"
|
|
||||||
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
QVariant AccountImagePackModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
if (role == ImageContentPackRole::DisplayNameRole) {
|
|
||||||
return i18n("Your Emojis");
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IconRole) {
|
|
||||||
return imageContentManager.accountImagesAvatar();
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IdentifierRole) {
|
|
||||||
return QStringLiteral("account");
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmojiRole) {
|
|
||||||
for (const auto &image : imageContentManager.accountImages()) {
|
|
||||||
if (!image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("emoticon"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsStickerRole) {
|
|
||||||
for (const auto &image : imageContentManager.accountImages()) {
|
|
||||||
if (!image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("sticker"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmptyRole) {
|
|
||||||
return imageContentManager.accountImages().size() == 0;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int AccountImagePackModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return ImageContentManager::instance().accountImages().size() > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> AccountImagePackModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentPackRole::IconRole, "emoji"},
|
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountImagePackModel::AccountImagePackModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
connect(&ImageContentManager::instance(), &ImageContentManager::accountImagesChanged, this, [this]() {
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
class AccountImagePackModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: This model uses the ImagePackRoles from ImageContentManager as roles.
|
|
||||||
*/
|
|
||||||
public:
|
|
||||||
explicit AccountImagePackModel(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;
|
|
||||||
};
|
|
||||||
@@ -600,19 +600,14 @@ 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(), {}, replaceId);
|
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
|
||||||
} 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(),
|
||||||
{},
|
{},
|
||||||
replaceId);
|
event->id());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "allimagecontentmodel.h"
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
// TODO custom emojis
|
|
||||||
|
|
||||||
AllImageContentModel::AllImageContentModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
// TODO connect to custom emojis changing;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant AllImageContentModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
auto row = index.row();
|
|
||||||
for (const auto &category : ImageContentManager::instance().emojis()) {
|
|
||||||
if (row >= category.size()) {
|
|
||||||
row -= category.size();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::DisplayNameRole) {
|
|
||||||
return category[row].displayName;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::EmojiRole) {
|
|
||||||
return category[row].text;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsStickerRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsEmojiRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int AllImageContentModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
auto sum = 0;
|
|
||||||
for (const auto &category : ImageContentManager::instance().emojis()) {
|
|
||||||
sum += category.size();
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> AllImageContentModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentRole::EmojiRole, "text"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
class AllImageContentModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AllImageContentModel(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;
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
#include "actionsmodel.h"
|
#include "actionsmodel.h"
|
||||||
#include "completionproxymodel.h"
|
#include "completionproxymodel.h"
|
||||||
// #include "emojimodel.h"
|
#include "customemojimodel.h"
|
||||||
|
#include "emojimodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
#include "roommanager.h"
|
#include "roommanager.h"
|
||||||
#include "userlistmodel.h"
|
#include "userlistmodel.h"
|
||||||
@@ -15,13 +16,11 @@ 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);
|
||||||
connect(this, &CompletionModel::roomChanged, this, [this]() {
|
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
|
||||||
m_userListModel->setRoom(m_room);
|
m_emojiModel->addSourceModel(&EmojiModel::instance());
|
||||||
});
|
|
||||||
// TODO m_emojiModel->addSourceModel(&EmojiModel::instance());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CompletionModel::text() const
|
QString CompletionModel::text() const
|
||||||
@@ -86,23 +85,29 @@ 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) {
|
||||||
return m_filterModel->data(filterIndex, RoomListModel::AvatarRole).toString();
|
auto mediaId = 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 {};
|
||||||
}
|
}
|
||||||
@@ -148,8 +153,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);
|
||||||
// TODO m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
|
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();
|
||||||
|
|||||||
116
src/models/customemojimodel.h
Normal file
116
src/models/customemojimodel.h
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// 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,56 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "emojipacksmodel.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
EmojiPacksModel::EmojiPacksModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant EmojiPacksModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
const auto row = index.row();
|
|
||||||
|
|
||||||
const auto &category = ImageContentManager::instance().emojiPacks()[row];
|
|
||||||
if (role == ImageContentPackRole::DisplayNameRole) {
|
|
||||||
return category.description;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IconRole) {
|
|
||||||
return category.icon;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IdentifierRole) {
|
|
||||||
return category.stateKey;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsStickerRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmojiRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmptyRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int EmojiPacksModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return ImageContentManager::instance().emojiPacks().count();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> EmojiPacksModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentPackRole::IconRole, "icon"},
|
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
class EmojiPacksModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EmojiPacksModel(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;
|
|
||||||
};
|
|
||||||
57
src/models/emoticonfiltermodel.cpp
Normal file
57
src/models/emoticonfiltermodel.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// 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,53 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "historyimagepackmodel.h"
|
|
||||||
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
QVariant HistoryImagePackModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
if (role == ImageContentPackRole::DisplayNameRole) {
|
|
||||||
return i18n("History");
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IconRole) {
|
|
||||||
return QStringLiteral("⌛");
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IdentifierRole) {
|
|
||||||
return QStringLiteral("history");
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsStickerRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmojiRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmptyRole) {
|
|
||||||
//TODO listen?
|
|
||||||
return imageContentManager.recentEmojis().size() == 0;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int HistoryImagePackModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> HistoryImagePackModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentPackRole::IconRole, "emoji"},
|
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryImagePackModel::HistoryImagePackModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
class HistoryImagePackModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit HistoryImagePackModel(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;
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagecontentfiltermodel.h"
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
ImageContentFilterModel::ImageContentFilterModel(QObject *parent)
|
|
||||||
: QSortFilterProxyModel(parent)
|
|
||||||
{
|
|
||||||
updateSourceModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImageContentFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(sourceParent);
|
|
||||||
auto index = sourceModel()->index(sourceRow, 0);
|
|
||||||
return ((index.data(ImageContentRole::IsEmojiRole).toBool() && emojis()) || (index.data(ImageContentRole::IsStickerRole).toBool() && stickers()))
|
|
||||||
&& sourceModel()->index(sourceRow, 0).data(ImageContentRole::DisplayNameRole).toString().contains(m_searchText, Qt::CaseInsensitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImageContentFilterModel::stickers() const
|
|
||||||
{
|
|
||||||
return m_stickers;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentFilterModel::setStickers(bool stickers)
|
|
||||||
{
|
|
||||||
m_stickers = stickers;
|
|
||||||
Q_EMIT stickersChanged();
|
|
||||||
invalidateFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImageContentFilterModel::emojis() const
|
|
||||||
{
|
|
||||||
return m_emojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentFilterModel::setEmojis(bool emojis)
|
|
||||||
{
|
|
||||||
m_emojis = emojis;
|
|
||||||
Q_EMIT emojisChanged();
|
|
||||||
invalidateFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentFilterModel::setCategory(const QString &category)
|
|
||||||
{
|
|
||||||
if (category == m_category) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_category = category;
|
|
||||||
Q_EMIT categoryChanged();
|
|
||||||
updateSourceModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentFilterModel::category() const
|
|
||||||
{
|
|
||||||
return m_category;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentFilterModel::setSearchText(const QString &searchText)
|
|
||||||
{
|
|
||||||
if (searchText == m_searchText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_searchText = searchText;
|
|
||||||
Q_EMIT searchTextChanged();
|
|
||||||
invalidateFilter();
|
|
||||||
updateSourceModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentFilterModel::searchText() const
|
|
||||||
{
|
|
||||||
return m_searchText;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentFilterModel::updateSourceModel()
|
|
||||||
{
|
|
||||||
if (!m_searchText.isEmpty()) {
|
|
||||||
if (sourceModel() != &m_allImageContentModel) {
|
|
||||||
setSourceModel(&m_allImageContentModel);
|
|
||||||
}
|
|
||||||
} else if (m_category == QStringLiteral("history")) {
|
|
||||||
setSourceModel(&m_recentImageContentProxyModel);
|
|
||||||
} else {
|
|
||||||
if (sourceModel() != &m_imageContentModel) {
|
|
||||||
setSourceModel(&m_imageContentModel);
|
|
||||||
}
|
|
||||||
m_imageContentModel.setCategory(m_category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QSortFilterProxyModel>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
#include "allimagecontentmodel.h"
|
|
||||||
#include "imagecontentmodel.h"
|
|
||||||
#include "recentimagecontentproxymodel.h"
|
|
||||||
|
|
||||||
class ImageContentFilterModel : public QSortFilterProxyModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(bool stickers READ stickers WRITE setStickers NOTIFY stickersChanged)
|
|
||||||
Q_PROPERTY(bool emojis READ emojis WRITE setEmojis NOTIFY emojisChanged)
|
|
||||||
Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged)
|
|
||||||
Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ImageContentFilterModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
|
||||||
|
|
||||||
[[nodiscard]] bool stickers() const;
|
|
||||||
void setStickers(bool stickers);
|
|
||||||
|
|
||||||
[[nodiscard]] bool emojis() const;
|
|
||||||
void setEmojis(bool emojis);
|
|
||||||
|
|
||||||
QString category() const;
|
|
||||||
void setCategory(const QString &category);
|
|
||||||
|
|
||||||
QString searchText() const;
|
|
||||||
void setSearchText(const QString &text);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void stickersChanged();
|
|
||||||
void emojisChanged();
|
|
||||||
void categoryChanged();
|
|
||||||
void searchTextChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_stickers = true;
|
|
||||||
bool m_emojis = true;
|
|
||||||
QString m_category;
|
|
||||||
QString m_searchText;
|
|
||||||
|
|
||||||
AllImageContentModel m_allImageContentModel;
|
|
||||||
RecentImageContentProxyModel m_recentImageContentProxyModel;
|
|
||||||
ImageContentModel m_imageContentModel;
|
|
||||||
|
|
||||||
void updateSourceModel();
|
|
||||||
};
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagecontentmodel.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include <Quotient/connection.h>
|
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
ImageContentModel::ImageContentModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
int ImageContentModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(parent);
|
|
||||||
if (m_category == QStringLiteral("account")) {
|
|
||||||
return imageContentManager.accountImages().size();
|
|
||||||
}
|
|
||||||
if (m_category.contains(u'@')) {
|
|
||||||
return imageContentManager.roomImages()[{m_roomId, m_stateKey}].size();
|
|
||||||
}
|
|
||||||
return imageContentManager.emojis()[m_category].count();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImageContentModel::emojiData(int row, int role) const
|
|
||||||
{
|
|
||||||
const auto emoji = imageContentManager.emojis()[m_category][row];
|
|
||||||
if (role == ImageContentRole::DisplayNameRole) {
|
|
||||||
return emoji.displayName;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::EmojiRole) {
|
|
||||||
return emoji.text;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsCustomRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsEmojiRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsStickerRole) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::HasTonesRole) {
|
|
||||||
return true; // TODO
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImageContentModel::accountData(int row, int role) const
|
|
||||||
{
|
|
||||||
const auto &image = imageContentManager.accountImages()[row];
|
|
||||||
if (role == ImageContentRole::DisplayNameRole) {
|
|
||||||
return image.shortcode;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::EmojiRole) {
|
|
||||||
return QStringLiteral("<img src=\"%1\" height=\"32\" width=\"32\"/>")
|
|
||||||
.arg(Controller::instance().activeConnection()->makeMediaUrl(image.url).toString());
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::ShortCodeRole) {
|
|
||||||
return image.shortcode;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsCustomRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsEmojiRole) {
|
|
||||||
return !image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("emoticon"));
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsStickerRole) {
|
|
||||||
return !image.usage || image.usage->isEmpty() || image.usage->contains(QStringLiteral("sticker"));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImageContentModel::roomData(int row, int role) const
|
|
||||||
{
|
|
||||||
const auto image = imageContentManager.roomImages()[{m_roomId, m_stateKey}][row];
|
|
||||||
if (role == ImageContentRole::DisplayNameRole) {
|
|
||||||
return image.shortcode;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::EmojiRole) {
|
|
||||||
return QStringLiteral("<img src=\"%1\" height=\"32\" width=\"32\"/>")
|
|
||||||
.arg(Controller::instance().activeConnection()->makeMediaUrl(image.url).toString());
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::ShortCodeRole) {
|
|
||||||
return image.shortcode;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsCustomRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsEmojiRole) {
|
|
||||||
return true; // For room image packs, we're ignoring the usage of the individual images.
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsStickerRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImageContentModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
const auto &row = index.row();
|
|
||||||
if (m_category == QStringLiteral("account")) {
|
|
||||||
return accountData(row, role);
|
|
||||||
}
|
|
||||||
if (m_category.contains(u'@')) {
|
|
||||||
return roomData(row, role);
|
|
||||||
}
|
|
||||||
return emojiData(row, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> ImageContentModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentRole::EmojiRole, "text"},
|
|
||||||
{ImageContentRole::ShortCodeRole, "shortCode"},
|
|
||||||
{ImageContentRole::IsCustomRole, "isCustom"},
|
|
||||||
{ImageContentRole::IsStickerRole, "isSticker"},
|
|
||||||
{ImageContentRole::IsEmojiRole, "isEmoji"},
|
|
||||||
{ImageContentRole::HasTonesRole, "hasTones"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ImageContentModel::category() const
|
|
||||||
{
|
|
||||||
return m_category;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageContentModel::setCategory(const QString &category)
|
|
||||||
{
|
|
||||||
if (category == m_category) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
beginResetModel();
|
|
||||||
m_category = category;
|
|
||||||
if (m_category.contains(u'@')) {
|
|
||||||
const auto &split = m_category.split(u'@');
|
|
||||||
m_roomId = split[0];
|
|
||||||
m_stateKey = split[1];
|
|
||||||
} else {
|
|
||||||
m_roomId = QString();
|
|
||||||
m_stateKey = QString();
|
|
||||||
}
|
|
||||||
endResetModel();
|
|
||||||
|
|
||||||
if (m_category == QStringLiteral("account")) {
|
|
||||||
connect(&ImageContentManager::instance(), &ImageContentManager::accountImagesChanged, this, [this]() {
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
disconnect(&ImageContentManager::instance(), &ImageContentManager::accountImagesChanged, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_EMIT categoryChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "moc_imagecontentmodel.cpp"
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class ImageContentModel
|
|
||||||
*
|
|
||||||
* This class defines the model for visualising a list of emojis.
|
|
||||||
*/
|
|
||||||
class ImageContentModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
ImageContentModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
|
|
||||||
[[nodiscard]] QString category() const;
|
|
||||||
void setCategory(const QString &category);
|
|
||||||
|
|
||||||
QVariant emojiData(int row, int role) const;
|
|
||||||
QVariant accountData(int row, int role) const;
|
|
||||||
QVariant roomData(int row, int role) const;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void categoryChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_category;
|
|
||||||
QString m_roomId;
|
|
||||||
QString m_stateKey;
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagepackroomsmodel.h"
|
|
||||||
|
|
||||||
#include <Quotient/connection.h>
|
|
||||||
#include <Quotient/room.h>
|
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
|
|
||||||
ImagePackRoomsModel::ImagePackRoomsModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
connect(&imageContentManager, &ImageContentManager::globalPacksChanged, this, [this]() {
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ImagePackRoomsModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
const auto &row = index.row();
|
|
||||||
const auto &packKey = imageContentManager.globalPacks()[row];
|
|
||||||
if (!imageContentManager.roomImagePacks().contains(packKey.first) || !imageContentManager.roomImagePacks()[packKey.first].contains(packKey.second)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto &pack = imageContentManager.roomImagePacks()[packKey.first][packKey.second];
|
|
||||||
if (role == ImageContentPackRole::DisplayNameRole) {
|
|
||||||
return pack.description;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IconRole) {
|
|
||||||
return pack.icon;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IdentifierRole) {
|
|
||||||
return QStringLiteral("%1@%2").arg(pack.roomId, pack.stateKey);
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsStickerRole) {
|
|
||||||
return pack.type == ImagePackDescription::Sticker;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmojiRole) {
|
|
||||||
return pack.type == ImagePackDescription::Emoji || pack.type == ImagePackDescription::CustomEmoji;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmptyRole) {
|
|
||||||
return imageContentManager.roomImages()[packKey].size() == 0;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsGlobalPackRole) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int ImagePackRoomsModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return imageContentManager.globalPacks().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> ImagePackRoomsModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentPackRole::IconRole, "emoji"}, // TODO rename
|
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the custom emoji/sticker packs from other rooms as marked in the account data.
|
|
||||||
* Not to be confused with the packs for this room (-> RoomEmoticonsCategoryModel)
|
|
||||||
*
|
|
||||||
* Note: This model uses the ImagePackRoles from ImageContentManager as roles.
|
|
||||||
*/
|
|
||||||
class ImagePackRoomsModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ImagePackRoomsModel(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;
|
|
||||||
};
|
|
||||||
@@ -1,43 +1,170 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
#include "imagepacksmodel.h"
|
#include "imagepacksmodel.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
|
||||||
#include "models/accountimagepackmodel.h"
|
#include <KLocalizedString>
|
||||||
#include "models/emojipacksmodel.h"
|
|
||||||
#include "models/historyimagepackmodel.h"
|
using namespace Quotient;
|
||||||
#include "models/imagepackroomsmodel.h"
|
|
||||||
#include "models/roomimagepacksmodel.h"
|
|
||||||
|
|
||||||
ImagePacksModel::ImagePacksModel(QObject *parent)
|
ImagePacksModel::ImagePacksModel(QObject *parent)
|
||||||
: QConcatenateTablesProxyModel(parent)
|
: QAbstractListModel(parent)
|
||||||
{
|
{
|
||||||
addSourceModel(new HistoryImagePackModel(parent));
|
|
||||||
addSourceModel(new AccountImagePackModel(parent));
|
|
||||||
m_roomImagePacksModel = new RoomImagePacksModel(parent);
|
|
||||||
addSourceModel(m_roomImagePacksModel);
|
|
||||||
addSourceModel(new ImagePackRoomsModel(parent));
|
|
||||||
addSourceModel(new EmojiPacksModel(parent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO required?
|
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
|
QHash<int, QByteArray> ImagePacksModel::roleNames() const
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
{DisplayNameRole, "displayName"},
|
||||||
{ImageContentPackRole::IconRole, "icon"},
|
{AvatarUrlRole, "avatarUrl"},
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
{AttributionRole, "attribution"},
|
||||||
|
{IdRole, "id"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
NeoChatRoom *ImagePacksModel::currentRoom() const
|
NeoChatRoom *ImagePacksModel::room() const
|
||||||
{
|
{
|
||||||
return m_roomImagePacksModel->currentRoom();
|
return m_room;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImagePacksModel::setCurrentRoom(NeoChatRoom *currentRoom)
|
void ImagePacksModel::setRoom(NeoChatRoom *room)
|
||||||
{
|
{
|
||||||
m_roomImagePacksModel->setCurrentRoom(currentRoom);
|
if (m_room) {
|
||||||
Q_EMIT currentRoomChanged();
|
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,7 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
#include <QConcatenateTablesProxyModel>
|
#pragma once
|
||||||
|
|
||||||
#include "events/imagepackevent.h"
|
#include "events/imagepackevent.h"
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
@@ -9,26 +9,95 @@
|
|||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
#include "neochatroom.h"
|
class NeoChatRoom;
|
||||||
|
|
||||||
class RoomImagePacksModel;
|
/**
|
||||||
|
* @class ImagePacksModel
|
||||||
class ImagePacksModel : public QConcatenateTablesProxyModel
|
*
|
||||||
|
* 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
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
Q_PROPERTY(NeoChatRoom *currentRoom READ currentRoom WRITE setCurrentRoom NOTIFY currentRoomChanged)
|
/**
|
||||||
|
* @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:
|
public:
|
||||||
ImagePacksModel(QObject *parent = nullptr);
|
/**
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
* @brief Defines the model roles.
|
||||||
[[nodiscard]] NeoChatRoom *currentRoom() const;
|
*/
|
||||||
void setCurrentRoom(NeoChatRoom *currentRoom);
|
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:
|
Q_SIGNALS:
|
||||||
void currentRoomChanged();
|
void roomChanged();
|
||||||
|
void showStickersChanged();
|
||||||
|
void showEmoticonsChanged();
|
||||||
|
void imagesLoaded();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RoomImagePacksModel *m_roomImagePacksModel = nullptr;
|
QPointer<NeoChatRoom> m_room;
|
||||||
|
QList<Quotient::ImagePackEventContent> m_events;
|
||||||
|
bool m_showStickers = true;
|
||||||
|
bool m_showEmoticons = true;
|
||||||
|
void reloadImages();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "imagepacksproxymodel.h"
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
#include "imagepacksmodel.h"
|
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
ImagePacksProxyModel::ImagePacksProxyModel(QObject *parent)
|
|
||||||
: QSortFilterProxyModel(parent)
|
|
||||||
{
|
|
||||||
setSourceModel(new ImagePacksModel(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagePacksProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(sourceParent);
|
|
||||||
const auto &identifier = sourceModel()->data(sourceModel()->index(sourceRow, 0), ImageContentPackRole::IdentifierRole).toString();
|
|
||||||
if (identifier.contains(u'@')) {
|
|
||||||
const auto roomId = identifier.split(u'@')[0];
|
|
||||||
if (static_cast<ImagePacksModel *>(sourceModel())->currentRoom() && roomId == static_cast<ImagePacksModel *>(sourceModel())->currentRoom()->id()
|
|
||||||
&& sourceModel()->data(sourceModel()->index(sourceRow, 0), ImageContentPackRole::IsGlobalPackRole).toBool()) {
|
|
||||||
// Hide this pack, as it's already exposed as a global pack
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ((sourceModel()->data(sourceModel()->index(sourceRow, 0), ImageContentPackRole::IsEmojiRole).toBool() && emojis())
|
|
||||||
|| (sourceModel()->data(sourceModel()->index(sourceRow, 0), ImageContentPackRole::IsStickerRole).toBool() && stickers()));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagePacksProxyModel::stickers() const
|
|
||||||
{
|
|
||||||
return m_stickers;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksProxyModel::setStickers(bool stickers)
|
|
||||||
{
|
|
||||||
m_stickers = stickers;
|
|
||||||
Q_EMIT stickersChanged();
|
|
||||||
invalidateFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagePacksProxyModel::emojis() const
|
|
||||||
{
|
|
||||||
return m_emojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksProxyModel::setEmojis(bool emojis)
|
|
||||||
{
|
|
||||||
m_emojis = emojis;
|
|
||||||
Q_EMIT emojisChanged();
|
|
||||||
invalidateFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
NeoChatRoom *ImagePacksProxyModel::currentRoom() const
|
|
||||||
{
|
|
||||||
return static_cast<ImagePacksModel *>(sourceModel())->currentRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImagePacksProxyModel::setCurrentRoom(NeoChatRoom *currentRoom)
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
static_cast<ImagePacksModel *>(sourceModel())->setCurrentRoom(currentRoom);
|
|
||||||
endResetModel();
|
|
||||||
Q_EMIT currentRoomChanged();
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include <QSortFilterProxyModel>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
class NeoChatRoom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters image packs on whether they contain stickers or emojis, depending on the respective properties
|
|
||||||
*/
|
|
||||||
class ImagePacksProxyModel : public QSortFilterProxyModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(bool stickers READ stickers WRITE setStickers NOTIFY stickersChanged)
|
|
||||||
Q_PROPERTY(bool emojis READ emojis WRITE setEmojis NOTIFY emojisChanged)
|
|
||||||
Q_PROPERTY(NeoChatRoom *currentRoom READ currentRoom WRITE setCurrentRoom NOTIFY currentRoomChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ImagePacksProxyModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
|
||||||
|
|
||||||
[[nodiscard]] bool stickers() const;
|
|
||||||
void setStickers(bool stickers);
|
|
||||||
|
|
||||||
[[nodiscard]] bool emojis() const;
|
|
||||||
void setEmojis(bool emojis);
|
|
||||||
|
|
||||||
[[nodiscard]] NeoChatRoom *currentRoom() const;
|
|
||||||
void setCurrentRoom(NeoChatRoom *currentRoom);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void stickersChanged();
|
|
||||||
void emojisChanged();
|
|
||||||
void currentRoomChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_stickers = true;
|
|
||||||
bool m_emojis = true;
|
|
||||||
};
|
|
||||||
@@ -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_currentState(isPending ? Pending : Unknown)
|
, m_isPending(isPending)
|
||||||
, m_isReply(isReply)
|
, m_isReply(isReply)
|
||||||
{
|
{
|
||||||
initializeModel();
|
initializeModel();
|
||||||
@@ -45,27 +45,19 @@ void MessageContentModel::initializeModel()
|
|||||||
Q_ASSERT(m_room != nullptr);
|
Q_ASSERT(m_room != nullptr);
|
||||||
Q_ASSERT(!m_eventId.isEmpty());
|
Q_ASSERT(!m_eventId.isEmpty());
|
||||||
|
|
||||||
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
|
connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent);
|
||||||
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++) {
|
||||||
@@ -151,33 +143,20 @@ void MessageContentModel::initializeModel()
|
|||||||
});
|
});
|
||||||
|
|
||||||
initializeEvent();
|
initializeEvent();
|
||||||
if (m_currentState == Available || m_currentState == Pending) {
|
updateReplyModel();
|
||||||
updateReplyModel();
|
|
||||||
}
|
|
||||||
resetModel();
|
resetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageContentModel::initializeEvent()
|
void MessageContentModel::initializeEvent()
|
||||||
{
|
{
|
||||||
if (m_currentState == UnAvailable) {
|
const auto event = m_room->getEvent(m_eventId);
|
||||||
|
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 = eventResult.first->senderId();
|
auto senderId = event->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()) {
|
||||||
@@ -193,6 +172,7 @@ 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();
|
||||||
@@ -204,7 +184,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_currentState = UnAvailable;
|
m_notFound = true;
|
||||||
resetModel();
|
resetModel();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -257,7 +237,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.first == nullptr) {
|
if (event == nullptr) {
|
||||||
if (role == DisplayRole) {
|
if (role == DisplayRole) {
|
||||||
if (m_isReply) {
|
if (m_isReply) {
|
||||||
return i18n("Loading reply");
|
return i18n("Loading reply");
|
||||||
@@ -272,7 +252,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == DisplayRole) {
|
if (role == DisplayRole) {
|
||||||
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(m_eventSenderId)) {
|
if (m_notFound || 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));
|
||||||
|
|
||||||
@@ -296,7 +276,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.first);
|
return EventHandler::richBody(m_room, event);
|
||||||
}
|
}
|
||||||
if (role == ComponentTypeRole) {
|
if (role == ComponentTypeRole) {
|
||||||
return component.type;
|
return component.type;
|
||||||
@@ -305,53 +285,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.first);
|
return EventHandler::id(event);
|
||||||
}
|
}
|
||||||
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.first->transactionId() == pendingEvent->transactionId();
|
return event->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.first, m_currentState == Pending, lastUpdated);
|
return EventHandler::time(event, m_isPending, 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.first->transactionId() == pendingEvent->transactionId();
|
return event->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.first, QStringLiteral("hh:mm"), m_currentState == Pending, lastUpdated);
|
return EventHandler::timeString(event, QStringLiteral("hh:mm"), m_isPending, 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.first);
|
return EventHandler::mediaInfo(m_room, event);
|
||||||
}
|
}
|
||||||
if (role == FileTransferInfoRole) {
|
if (role == FileTransferInfoRole) {
|
||||||
return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first));
|
return QVariant::fromValue(m_room->cachedFileTransferInfo(event));
|
||||||
}
|
}
|
||||||
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.first);
|
return EventHandler::latitude(event);
|
||||||
}
|
}
|
||||||
if (role == LongitudeRole) {
|
if (role == LongitudeRole) {
|
||||||
return EventHandler::longitude(event.first);
|
return EventHandler::longitude(event);
|
||||||
}
|
}
|
||||||
if (role == AssetRole) {
|
if (role == AssetRole) {
|
||||||
return EventHandler::locationAssetType(event.first);
|
return EventHandler::locationAssetType(event);
|
||||||
}
|
}
|
||||||
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.first);
|
return EventHandler::replyId(event);
|
||||||
}
|
}
|
||||||
if (role == ReplyAuthorRole) {
|
if (role == ReplyAuthorRole) {
|
||||||
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
|
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event));
|
||||||
}
|
}
|
||||||
if (role == ReplyContentModelRole) {
|
if (role == ReplyContentModelRole) {
|
||||||
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
||||||
@@ -407,17 +387,18 @@ 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_currentState == UnAvailable) {
|
if (m_room->connection()->isIgnored(m_eventSenderId) || m_notFound) {
|
||||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||||
endResetModel();
|
endResetModel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto event = m_room->getEvent(m_eventId);
|
if (event == nullptr) {
|
||||||
if (event.first == nullptr) {
|
|
||||||
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
||||||
endResetModel();
|
endResetModel();
|
||||||
return;
|
return;
|
||||||
@@ -450,19 +431,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.first == nullptr) {
|
if (event == nullptr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessageComponent> newComponents;
|
QList<MessageComponent> newComponents;
|
||||||
|
|
||||||
if (eventCast<const Quotient::RoomMessageEvent>(event.first)
|
if (eventCast<const Quotient::RoomMessageEvent>(event)
|
||||||
&& eventCast<const Quotient::RoomMessageEvent>(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
&& eventCast<const Quotient::RoomMessageEvent>(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
|
||||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||||
return newComponents;
|
return newComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.first->isRedacted()) {
|
if (event->isRedacted()) {
|
||||||
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||||
return newComponents;
|
return newComponents;
|
||||||
}
|
}
|
||||||
@@ -474,7 +455,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.first)));
|
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_room->urlPreviewEnabled()) {
|
if (m_room->urlPreviewEnabled()) {
|
||||||
@@ -482,7 +463,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.first)) {
|
if (isThreading && !EventHandler::isThreaded(event)) {
|
||||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,11 +473,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.first == nullptr || m_isReply) {
|
if (event == nullptr || m_isReply) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EventHandler::hasReply(event.first) || (EventHandler::isThreaded(event.first) && NeoChatConfig::self()->threads())) {
|
if (!EventHandler::hasReply(event) || (EventHandler::isThreaded(event) && NeoChatConfig::self()->threads())) {
|
||||||
if (m_replyModel) {
|
if (m_replyModel) {
|
||||||
delete m_replyModel;
|
delete m_replyModel;
|
||||||
}
|
}
|
||||||
@@ -507,7 +488,7 @@ void MessageContentModel::updateReplyModel()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event.first), true, false, this);
|
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event), 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});
|
||||||
@@ -517,13 +498,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.first == nullptr) {
|
if (event == nullptr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageComponentType::Text: {
|
case MessageComponentType::Text: {
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||||
return TextHandler().textComponents(body,
|
return TextHandler().textComponents(body,
|
||||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||||
@@ -534,11 +515,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.first);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||||
|
|
||||||
if (m_emptyItinerary) {
|
if (m_emptyItinerary) {
|
||||||
if (!m_isReply) {
|
if (!m_isReply) {
|
||||||
auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first);
|
auto fileTransferInfo = m_room->cachedFileTransferInfo(event);
|
||||||
|
|
||||||
#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>());
|
||||||
@@ -586,24 +567,17 @@ 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.first->is<StickerEvent>()) {
|
if (!event->is<StickerEvent>()) {
|
||||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||||
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
|
QList<MessageComponent> components;
|
||||||
if (fileContent != nullptr) {
|
components += MessageComponent{type, QString(), {}};
|
||||||
const auto fileInfo = fileContent->commonInfo();
|
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||||
const auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
components += TextHandler().textComponents(body,
|
||||||
// Do not attach the description to the image, if it's the same as the original filename.
|
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||||
if (fileInfo.originalName != body) {
|
m_room,
|
||||||
QList<MessageComponent> components;
|
roomMessageEvent,
|
||||||
components += MessageComponent{type, QString(), {}};
|
roomMessageEvent->isReplaced());
|
||||||
components += TextHandler().textComponents(body,
|
return components;
|
||||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
|
||||||
m_room,
|
|
||||||
roomMessageEvent,
|
|
||||||
roomMessageEvent->isReplaced());
|
|
||||||
return components;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -679,13 +653,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.first == nullptr) {
|
if (m_room == nullptr || event == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
|
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
|
||||||
if (roomMessageEvent->has<EventContent::FileContent>()) {
|
if (roomMessageEvent->has<EventContent::FileContent>()) {
|
||||||
auto filePath = m_room->cachedFileTransferInfo(event.first).localPath;
|
auto filePath = m_room->cachedFileTransferInfo(event).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,14 +31,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -106,6 +98,7 @@ public:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void showAuthorChanged();
|
void showAuthorChanged();
|
||||||
|
void eventUnavailable();
|
||||||
void eventUpdated();
|
void eventUpdated();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -114,9 +107,10 @@ private:
|
|||||||
QString m_eventSenderId;
|
QString m_eventSenderId;
|
||||||
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
|
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
|
||||||
|
|
||||||
MessageState m_currentState = Unknown;
|
bool m_isPending;
|
||||||
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,21 +160,12 @@ 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, true);
|
createEventObjects(event);
|
||||||
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) {
|
||||||
@@ -627,7 +618,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, bool isPending)
|
void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
|
||||||
{
|
{
|
||||||
if (event == nullptr) {
|
if (event == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -650,7 +641,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, boo
|
|||||||
|
|
||||||
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, false, isPending));
|
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, eventId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, bool isPending = false);
|
void createEventObjects(const Quotient::RoomEvent *event);
|
||||||
// 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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "recentimagecontentmodel.h"
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
RecentImageContentModel::RecentImageContentModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
connect(&ImageContentManager::instance(), &ImageContentManager::recentEmojisChanged, this, [this]() {
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant RecentImageContentModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
const auto &recent = ImageContentManager::instance().recentEmojis();
|
|
||||||
const auto row = index.row();
|
|
||||||
const bool isCustom = recent.keys()[row].startsWith(QLatin1Char(':'));
|
|
||||||
|
|
||||||
if (role == ImageContentRole::DisplayNameRole) {
|
|
||||||
if (isCustom) {
|
|
||||||
return imageContentManager.bodyForShortCode(recent.keys()[row].mid(1).chopped(1));
|
|
||||||
}
|
|
||||||
return ImageContentManager::instance().emojiForText(recent.keys()[row]).displayName;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::EmojiRole) {
|
|
||||||
if (isCustom) {
|
|
||||||
return QStringLiteral("<img src=\"%1\" width=\"32\" height=\"32\"/>")
|
|
||||||
.arg(imageContentManager.mxcForShortCode(recent.keys()[row].mid(1).chopped(1)));
|
|
||||||
}
|
|
||||||
return recent.keys()[row];
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::UsageCountRole) {
|
|
||||||
return recent[recent.keys()[row]];
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::ShortCodeRole) {
|
|
||||||
return recent.keys()[row].mid(1).chopped(1);
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsCustomRole) {
|
|
||||||
return isCustom;
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsEmojiRole) {
|
|
||||||
if (!isCustom)
|
|
||||||
return true;
|
|
||||||
return imageContentManager.isEmojiShortCode(recent.keys()[row].mid(1).chopped(1));
|
|
||||||
}
|
|
||||||
if (role == ImageContentRole::IsStickerRole) {
|
|
||||||
if (!isCustom)
|
|
||||||
return false;
|
|
||||||
return imageContentManager.isStickerShortCode(recent.keys()[row].mid(1).chopped(1));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int RecentImageContentModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
return ImageContentManager::instance().recentEmojis().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> RecentImageContentModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentRole::EmojiRole, "text"},
|
|
||||||
{ImageContentRole::UsageCountRole, "usageCount"},
|
|
||||||
{ImageContentRole::ShortCodeRole, "shortCode"},
|
|
||||||
{ImageContentRole::IsCustomRole, "isCustom"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the previously used emojis in no specific order.
|
|
||||||
*/
|
|
||||||
class RecentImageContentModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit RecentImageContentModel(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;
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "recentimagecontentproxymodel.h"
|
|
||||||
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
#include "recentimagecontentmodel.h"
|
|
||||||
|
|
||||||
RecentImageContentProxyModel::RecentImageContentProxyModel(QObject *parent)
|
|
||||||
: QSortFilterProxyModel(parent)
|
|
||||||
{
|
|
||||||
setSourceModel(new RecentImageContentModel(this));
|
|
||||||
sort(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RecentImageContentProxyModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
|
|
||||||
{
|
|
||||||
return sourceLeft.data(ImageContentRole::UsageCountRole).toInt() > sourceRight.data(ImageContentRole::UsageCountRole).toInt();
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QSortFilterProxyModel>
|
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
class RecentImageContentProxyModel : public QSortFilterProxyModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit RecentImageContentProxyModel(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "roomimagepacksmodel.h"
|
|
||||||
#include "imagecontentmanager.h"
|
|
||||||
|
|
||||||
#include "neochatroom.h"
|
|
||||||
|
|
||||||
NeoChatRoom *RoomImagePacksModel::currentRoom() const
|
|
||||||
{
|
|
||||||
return m_currentRoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RoomImagePacksModel::setCurrentRoom(NeoChatRoom *currentRoom)
|
|
||||||
{
|
|
||||||
if (m_currentRoom == currentRoom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m_currentRoom) {
|
|
||||||
disconnect(m_currentRoom, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
beginResetModel();
|
|
||||||
m_currentRoom = currentRoom;
|
|
||||||
endResetModel();
|
|
||||||
Q_EMIT currentRoomChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant RoomImagePacksModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
if (!m_currentRoom) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const auto row = index.row();
|
|
||||||
const auto &packs = ImageContentManager::instance().roomImagePacks()[m_currentRoom->id()].values();
|
|
||||||
const auto &pack = packs[row];
|
|
||||||
if (role == ImageContentPackRole::DisplayNameRole) {
|
|
||||||
return pack.description;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IconRole) {
|
|
||||||
return pack.icon;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IdentifierRole) {
|
|
||||||
return QStringLiteral("%1@%2").arg(m_currentRoom->id(), pack.stateKey);
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsStickerRole) {
|
|
||||||
return pack.type == ImagePackDescription::Sticker || pack.type == ImagePackDescription::Both;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmojiRole) {
|
|
||||||
return pack.type == ImagePackDescription::Emoji || pack.type == ImagePackDescription::CustomEmoji || pack.type == ImagePackDescription::Both;
|
|
||||||
}
|
|
||||||
if (role == ImageContentPackRole::IsEmptyRole) {
|
|
||||||
return imageContentManager.roomImages()[{m_currentRoom->id(), pack.stateKey}].isEmpty();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int RoomImagePacksModel::rowCount(const QModelIndex &index) const
|
|
||||||
{
|
|
||||||
Q_UNUSED(index);
|
|
||||||
if (!m_currentRoom) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return ImageContentManager::instance().roomImagePacks()[m_currentRoom->id()].size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> RoomImagePacksModel::roleNames() const
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
{ImageContentPackRole::DisplayNameRole, "displayName"},
|
|
||||||
{ImageContentPackRole::IconRole, "emoji"},
|
|
||||||
{ImageContentPackRole::IdentifierRole, "identifier"},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomImagePacksModel::RoomImagePacksModel(QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
connect(&ImageContentManager::instance(), &ImageContentManager::roomImagePacksChanged, this, [this](NeoChatRoom *room) {
|
|
||||||
if (room != m_currentRoom) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
beginResetModel();
|
|
||||||
endResetModel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QPointer>
|
|
||||||
|
|
||||||
class NeoChatRoom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: This model uses the ImagePackRoles from ImageContentManager as roles.
|
|
||||||
*/
|
|
||||||
class RoomImagePacksModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit RoomImagePacksModel(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 *currentRoom() const;
|
|
||||||
void setCurrentRoom(NeoChatRoom *currentRoom);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void currentRoomChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QPointer<NeoChatRoom> m_currentRoom;
|
|
||||||
};
|
|
||||||
@@ -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->avatarMediaUrl();
|
return room->avatarMediaId();
|
||||||
}
|
}
|
||||||
if (role == CanonicalAliasRole) {
|
if (role == CanonicalAliasRole) {
|
||||||
return room->canonicalAlias();
|
return room->canonicalAlias();
|
||||||
|
|||||||
@@ -324,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->avatarMediaUrl();
|
return room->avatarMediaId();
|
||||||
}
|
}
|
||||||
if (role == CanonicalAliasRole) {
|
if (role == CanonicalAliasRole) {
|
||||||
return room->canonicalAlias();
|
return room->canonicalAlias();
|
||||||
|
|||||||
@@ -431,9 +431,9 @@ QDateTime NeoChatRoom::lastActiveTime()
|
|||||||
return messageEvents().rbegin()->get()->originTimestamp();
|
return messageEvents().rbegin()->get()->originTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl NeoChatRoom::avatarMediaUrl() const
|
QString NeoChatRoom::avatarMediaId() const
|
||||||
{
|
{
|
||||||
if (const auto avatar = Room::avatarUrl(); !avatar.isEmpty()) {
|
if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
|
||||||
return avatar;
|
return avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ QUrl NeoChatRoom::avatarMediaUrl() 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.avatarUrl();
|
return member.avatarMediaId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1749,31 +1749,25 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString &eventId) const
|
const RoomEvent *NeoChatRoom::getEvent(const QString &eventId) const
|
||||||
{
|
{
|
||||||
if (eventId.isEmpty()) {
|
if (eventId.isEmpty()) {
|
||||||
return {};
|
return nullptr;
|
||||||
}
|
}
|
||||||
const auto timelineIt = findInTimeline(eventId);
|
const auto timelineIt = findInTimeline(eventId);
|
||||||
if (timelineIt != historyEdge()) {
|
if (timelineIt != historyEdge()) {
|
||||||
return std::make_pair(timelineIt->get(), false);
|
return timelineIt->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto pendingIt = findPendingEvent(eventId);
|
const auto pendingIt = findPendingEvent(eventId);
|
||||||
if (pendingIt != pendingEvents().end()) {
|
if (pendingIt != pendingEvents().end()) {
|
||||||
return std::make_pair(pendingIt->event(), true);
|
return pendingIt->event();
|
||||||
}
|
|
||||||
// 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 std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false);
|
return extraIt != m_extraEvents.end() ? extraIt->get() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user