Compare commits

..

29 Commits

Author SHA1 Message Date
l10n daemon script
ebd38fb435 GIT_SILENT Sync po/docbooks with svn 2024-11-14 03:10:10 +00:00
l10n daemon script
a5b999e682 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-11-14 03:06:40 +00:00
l10n daemon script
41d34fc0e4 GIT_SILENT Sync po/docbooks with svn 2024-11-13 03:08:49 +00:00
l10n daemon script
b51194f90f SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-11-13 03:05:22 +00:00
l10n daemon script
80ac9e1ba7 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-11-12 03:11:52 +00:00
l10n daemon script
e3874c824a GIT_SILENT Sync po/docbooks with svn 2024-11-11 03:23:01 +00:00
l10n daemon script
6599c6b609 SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-11-11 03:15:47 +00:00
l10n daemon script
13d522221c GIT_SILENT made messages (after extraction) 2024-11-11 02:40:16 +00:00
Joshua Goins
dd8f926f32 PollHandler: Make sure it's not constructible from QML
(cherry picked from commit d6b780762e)
2024-11-10 10:29:26 -05:00
Joshua Goins
258312e798 PollHandler: Ensure that m_pollStartEvent is always initialized to null
Otherwise it may be undefined, and we DO create default-constructed
PollHandler. For example, one is used as a fallback poll object
in NeoChatRoom::poll.

This is blind fix for a pretty nasty poll-related crash we saw a few
months ago.

BUG: 493649
(cherry picked from commit 5ef66b5cf6)
2024-11-10 10:29:17 -05:00
Joshua Goins
43d40c7e75 Add m.room.create state events to sync_response
In case we need to access the creation state in an appium test in the
future.

(cherry picked from commit 85ee5084b6)
2024-11-10 07:40:41 -05:00
Joshua Goins
cbcc9a6514 Hide rooms that have a defined room type
I have a room with a custom type that's only for holding data, and
doesn't need to be shown in the room list. Currently the spec is a bit
vague about what clients should do, but hiding them is probably fine
for now.

(cherry picked from commit bb9ce117de)
2024-11-10 07:40:41 -05:00
l10n daemon script
625048610b GIT_SILENT Sync po/docbooks with svn 2024-11-10 03:30:48 +00:00
l10n daemon script
fa47b67e3d SVN_SILENT made messages (.desktop file) - always resolve ours
In case of conflict in i18n, keep the version of the branch "ours"
To resolve a particular conflict, "git checkout --ours path/to/file.desktop"
2024-11-10 03:16:42 +00:00
Carl Schwan
9347a66acf RoomGeneralPage: Add missing separator
And some other minor fixes

(cherry picked from commit 00c5aa26bb)
2024-11-09 18:12:25 -05:00
Joshua Goins
317df56ffa Make closing link previews instant, as it should be
We were missing a endResetModel() call, now with it added the removal
happens instantly.

(cherry picked from commit bae4de227c)
2024-11-09 18:12:25 -05:00
Joshua Goins
fed9197716 Stop being able to crash NeoChat by pressing a button repeatedly
If you spam click the "Close link preview" button, it's possible to
crash NeoChat. This is because the index check is wrong for the array
size.

It's possible to even do this due to a bug causing the removal to be
reflected visually too slowly, that's fixed in the next commit.

(cherry picked from commit 253f891c5a)
2024-11-09 18:12:25 -05:00
Joshua Goins
1e892599e9 Improve clicking link previews
First of all, clicking on them actually works - because we were missing
an import for RoomManager. Secondly, we use a dedicated TapHandler
since onLinkActivated sucks. We want to be able to click anywhere on the
preview to go to the website/room anyway.

(cherry picked from commit 6966159062)
2024-11-09 18:12:25 -05:00
Joshua Goins
b7229ca0cf Don't set isThread on the message and file delegate context menus
It doesn't have a property called isThread, and I don't know where it
went - if it ever existed?

(cherry picked from commit 07d3b80c3e)
2024-11-09 18:12:25 -05:00
Joshua Goins
953b711823 Make fullscreen images focused when they're opened
Otherwise keyboard shortcuts don't work until you tap the image, which
makes no sense.

BUG: 484322
(cherry picked from commit a41d0f3214)
2024-11-09 18:12:25 -05:00
Joshua Goins
01d903efd3 Fix viewing any kind of data in developer tools
Fix pageStack being undefined, so we're able to view event data again.

(cherry picked from commit 1ee15de78b)
2024-11-09 18:12:25 -05:00
Carl Schwan
241dd81932 Update checkbox of PollComponent
Use FormCheckDelegate instead of a CheckBox inside a RowLayout. This
increase the click area particularly on mobile.

(cherry picked from commit b044358970)
2024-11-09 18:12:24 -05:00
Oliver Beard
f6dfe0cbcf timeline: Round separators for replies and link previews
(cherry picked from commit d2e11bb3bb)
2024-11-09 18:12:24 -05:00
Joshua Goins
f10b97139c README: Change snap store badge to the one from apps.kde.org
It seems CORS is blocking access to the badge, but we have rehosted on
apps.kde.org.

(cherry picked from commit a55bac899c)
2024-11-09 18:12:24 -05:00
Joshua Goins
385c5b3405 Update network proxy page with the improved version from Tokodon
This functions the same, but looks a bit nicer.

(cherry picked from commit c2380fb8df)
2024-11-09 16:28:47 -05:00
Joshua Goins
7bc6f906f8 Update desktop file and app description to match AppStream data
This was updated to "Chat on Matrix" but in other places it was never
switched from "Matrix client" and the like. Now it should be more
consistent.

(cherry picked from commit f31c644b13)
2024-11-09 16:28:47 -05:00
Joshua Goins
b8b1434a95 Clarify that sorting rooms by activity isn't the only thing it does
Recently, it also sorts rooms based on unread notification count and
importance. This adds a clarification to the setting so users (like me)
aren't confused why it isn't sorting only by activity.

(cherry picked from commit 26cd621d0e)
2024-11-09 16:28:47 -05:00
l10n daemon script
85c7a4bcb3 GIT_SILENT Sync po/docbooks with svn 2024-11-09 03:10:22 +00:00
Albert Astals Cid
84b698a7e8 GIT_SILENT Upgrade release service version to 24.11.80. 2024-11-08 19:06:28 +01:00
149 changed files with 13078 additions and 67459 deletions

View File

@@ -7,9 +7,9 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION_MAJOR "24")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "80")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})

View File

@@ -11,6 +11,7 @@
#include <qnamespace.h>
#include "enums/messagecomponenttype.h"
#include "models/customemojimodel.h"
#include "neochatconnection.h"
#include "testutils.h"
@@ -76,6 +77,7 @@ void TextHandlerTest::initTestCase()
QJsonObject{{"body"_ls, "Test custom emoji"_ls},
{"url"_ls, "mxc://example.org/test"_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"));
}
@@ -533,7 +535,7 @@ void TextHandlerTest::componentOutput_data()
QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}};
QTest::newRow("quote") << QStringLiteral("<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>")
<< 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>")
<< QList<MessageComponent>{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}},
MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}};

View File

@@ -59,7 +59,6 @@
<summary xml:lang="eu">Berriketa Matrix-en</summary>
<summary xml:lang="fr">Discuter sur Matrix</summary>
<summary xml:lang="gl">Charlar en Matrix</summary>
<summary xml:lang="hu">Csevegés Matrixon</summary>
<summary xml:lang="ia">Conversation en ditecto sur Matrix</summary>
<summary xml:lang="it">Chat su 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::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::supporters">Tanguy Fardet</value>
</custom>
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
<screenshots>

View File

@@ -90,22 +90,16 @@ GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix
Comment[ca]=Xat a Matrix
Comment[ca@valencia]=Xat a Matrix
Comment[en_GB]=Chat on Matrix
Comment[es]=Chat en Matrix
Comment[eu]=Berriketa Matrix-en
Comment[fr]=Clavarder sur Matrix
Comment[gl]=Charle en Matrix
Comment[hu]=Csevegés Matrixon
Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[sl]=Klepet na Matrixu
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix Üzerinde Sohbet Et
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
MimeType=x-scheme-handler/matrix;
Exec=neochat %u
Terminal=false

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -10,6 +10,12 @@ endif()
add_library(neochat STATIC
controller.cpp
controller.h
models/emojimodel.cpp
models/emojimodel.h
emojitones.cpp
emojitones.h
models/customemojimodel.cpp
models/customemojimodel.h
clipboard.cpp
clipboard.h
models/messageeventmodel.cpp
@@ -20,6 +26,8 @@ add_library(neochat STATIC
models/roomlistmodel.h
models/sortfilterspacelistmodel.cpp
models/sortfilterspacelistmodel.h
models/accountemoticonmodel.cpp
models/accountemoticonmodel.h
spacehierarchycache.cpp
spacehierarchycache.h
roommanager.cpp
@@ -42,6 +50,8 @@ add_library(neochat STATIC
models/userdirectorylistmodel.h
models/pushrulemodel.cpp
models/pushrulemodel.h
models/emoticonfiltermodel.cpp
models/emoticonfiltermodel.h
notificationsmanager.cpp
notificationsmanager.h
models/sortfilterroomlistmodel.cpp
@@ -91,6 +101,10 @@ add_library(neochat STATIC
texthandler.h
logger.cpp
logger.h
models/stickermodel.cpp
models/stickermodel.h
models/imagepacksmodel.cpp
models/imagepacksmodel.h
events/imagepackevent.cpp
events/imagepackevent.h
events/joinrulesevent.cpp
@@ -180,32 +194,6 @@ add_library(neochat STATIC
models/threadmodel.h
enums/messagetype.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
@@ -294,9 +282,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/ConfirmLeaveDialog.qml
qml/CodeMaximizeComponent.qml
qml/EditStateDialog.qml
qml/EmojiPickerTypeHeader.qml
qml/EmojiPickerPackHeader.qml
qml/QuickReaction.qml
qml/ConsentDialog.qml
qml/AskDirectChatConfirmation.qml
qml/HoverLinkIndicator.qml
@@ -313,12 +298,6 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.chatbar
)
qt_add_resources(neochat "emoji"
PREFIX "/"
FILES
data/emojis.json
)
add_subdirectory(settings)
add_subdirectory(timeline)
add_subdirectory(devtools)

View File

@@ -176,14 +176,13 @@ QQC2.Control {
RowLayout {
QQC2.ScrollView {
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.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
QQC2.ScrollBar.vertical.policy: chatBarHeightAnimation.running && implicitHeight <= height ? QQC2.ScrollBar.AlwaysOff : QQC2.ScrollBar.AsNeeded
@@ -321,11 +320,12 @@ QQC2.Control {
id: actionsRow
spacing: 0
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: Kirigami.Units.smallSpacing * 4
Layout.bottomMargin: Kirigami.Units.smallSpacing * 1.5
Repeater {
model: root.actions
delegate: QQC2.ToolButton {
Layout.alignment: Qt.AlignVCenter
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
onClicked: modelData.trigger()
@@ -342,6 +342,7 @@ QQC2.Control {
}
}
}
DelegateSizeHelper {
id: chatBarSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
@@ -519,6 +520,7 @@ QQC2.Control {
y: -implicitHeight
modal: false
includeCustom: true
closeOnChosen: false
currentRoom: root.currentRoom

View File

@@ -5,30 +5,56 @@ import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
QQC2.Button {
QQC2.ItemDelegate {
id: root
required property string toolTip
property string name
property string emoji
property bool showTones: false
property bool isImage: false
QQC2.ToolTip.text: toolTip
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: root.name
QQC2.ToolTip.visible: hovered && root.name !== ""
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 {
text: root.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5
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 {
width: Kirigami.Units.gridUnit * 0.5
height: Kirigami.Units.gridUnit * 0.5
source: "arrow-down-symbolic"
anchors.bottom: parent.bottom
anchors.right: parent.right
visible: root.showTones
background: Rectangle {
color: root.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
Rectangle {
radius: Kirigami.Units.cornerRadius
anchors.fill: parent
color: Kirigami.Theme.highlightColor
opacity: root.hovered && !root.pressed ? 0.2 : 0
}
}
}

View File

@@ -16,7 +16,9 @@ QQC2.Popup {
*/
property NeoChatRoom currentRoom
property bool includeCustom: false
property bool closeOnChosen: true
property bool showQuickReaction: false
signal chosen(string emoji)
@@ -62,15 +64,15 @@ QQC2.Popup {
padding: 2
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 {
id: emojiPicker
height: 400
currentRoom: root.currentRoom
includeCustom: root.includeCustom
showQuickReaction: root.showQuickReaction
onChosen: emoji => {
root.chosen(emoji);
ImageContentManager.emojiUsed(emoji)
if (root.closeOnChosen) {
root.close();
}

View File

@@ -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
import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.textaddons.emoticons
QQC2.ScrollView {
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
property bool stickers: false
@@ -22,8 +25,6 @@ QQC2.ScrollView {
emojis.forceActiveFocus();
}
width: Kirigami.Units.gridUnit * 24
GridView {
id: emojis
@@ -40,9 +41,7 @@ QQC2.ScrollView {
onModelChanged: currentIndex = -1
cellWidth: emojis.width / root.emojisPerRow
cellHeight: Kirigami.Units.iconSizes.large
model: EmojiModelManager.emojiModel
cellHeight: root.targetIconSize
KeyNavigation.up: root.header
@@ -50,49 +49,49 @@ QQC2.ScrollView {
delegate: EmojiDelegate {
id: emojiDelegate
required property string unicode
required property string identifier
required property int index
text: emojiDelegate.unicode
toolTip: emojiDelegate.identifier
checked: emojis.currentIndex === emojiDelegate.index
checked: emojis.currentIndex === model.index
emoji: !!modelData ? modelData.unicode : model.url
name: !!modelData ? modelData.shortName : model.body
width: emojis.cellWidth
height: emojis.cellHeight
isImage: root.stickers
Keys.onEnterPressed: clicked()
Keys.onReturnPressed: clicked()
// onClicked: {
// if (root.stickers) {
// root.stickerChosen(model.index);
// }
// root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
// EmojiModel.emojiUsed(modelData);
// }
// Keys.onSpacePressed: pressAndHold()
// onPressAndHold: {
// if (!showTones) {
// return;
// }
// let tones = Qt.createComponent("org.kde.neochat", "EmojiTonesPicker").createObject(emojiDelegate, {
// shortName: modelData.shortName,
// unicode: modelData.unicode,
// categoryIconSize: root.targetIconSize,
// onChosen: root.chosen(emoji => root.chosen(emoji))
// });
// tones.open();
// tones.forceActiveFocus();
// }
// showTones: model.hasTones
onClicked: {
if (root.stickers) {
root.stickerChosen(model.index);
}
root.chosen(modelData.isCustom ? modelData.shortName : modelData.unicode);
EmojiModel.emojiUsed(modelData);
}
Keys.onSpacePressed: pressAndHold()
onPressAndHold: {
if (EmojiModel.tones(modelData.shortName).length === 0) {
return;
}
let tones = tonesPopupComponent.createObject(emojiDelegate, {
shortName: modelData.shortName,
unicode: modelData.unicode,
categoryIconSize: root.targetIconSize
});
tones.open();
tones.forceActiveFocus();
}
showTones: !!modelData && EmojiModel.tones(modelData.shortName).length > 0
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
text: root.stickers ? i18nc("@info", "No stickers") : i18nc("@info", "No emojis")
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
visible: emojis.count === 0
}
}
Component {
id: tonesPopupComponent
EmojiTonesPicker {
onChosen: root.chosen(emoji)
}
}
}

View File

@@ -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
import QtQuick
@@ -6,7 +6,6 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.neochat
import org.kde.textaddons.emoticons
ColumnLayout {
id: root
@@ -14,29 +13,86 @@ ColumnLayout {
/**
* @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)
spacing: 0
onActiveFocusChanged: if (activeFocus) {
searchField.forceActiveFocus();
}
EmojiPickerTypeHeader {
id: emoticonPickerTypeHeader
spacing: 0
Kirigami.NavigationTabBar {
id: types
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 {
id: emoticonPickerCategoryHeader
QQC2.ScrollView {
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 {
@@ -48,34 +104,114 @@ ColumnLayout {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
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: ""
}
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.fillHeight: true
withCustom: root.includeCustom
onChosen: unicode => root.chosen(unicode)
header: emoticonPickerCategoryHeader
header: categories
Keys.forwardTo: searchField
stickers: emoticonPickerTypeHeader.selectedType === EmojiPickerTypeHeader.EmoticonType.Sticker
stickers: root.selectedType === 1
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
}
Kirigami.Separator {
visible: showQuickReaction
Layout.fillWidth: true
Layout.preferredHeight: 1
}
QuickReaction {
id: quickReaction
onChosen: root.chosen(text)
QQC2.ScrollView {
visible: showQuickReaction
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() {
searchField.text = ""
searchField.text = "";
}
}

View File

@@ -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
import QtQuick
@@ -54,8 +54,8 @@ QQC2.Popup {
delegate: EmojiDelegate {
id: emojiDelegate
checked: tonesList.currentIndex === model.index
text: modelData.unicode
toolTip: modelData.shortName
emoji: modelData.unicode
name: modelData.shortName
width: root.categoryIconSize
height: width

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2024 Emojibase
SPDX-License-Identifier: MIT

1857
src/emojis.h Normal file

File diff suppressed because it is too large Load Diff

9
src/emojitones.cpp Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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("👤");
}

View File

@@ -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);
};

View File

@@ -13,7 +13,6 @@ LoginStep {
id: root
FormCard.FormTextDelegate {
textItem.wrapMode: Text.Wrap
text: i18n("Please wait while your messages are loaded from the server. This might take a little while.")
}
FormCard.AbstractFormDelegate {

View File

@@ -7,7 +7,6 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.settings
@@ -91,27 +90,11 @@ Kirigami.Page {
id: loadedAccounts
model: AccountRegistry
delegate: FormCard.FormButtonDelegate {
id: delegate
required property string userId
required property NeoChatConnection connection
text: QmlUtils.escapeString(connection.localUser.displayName)
description: connection.localUser.id
leadingPadding: Kirigami.Units.largeSpacing
text: model.userId
onClicked: {
Controller.activeConnection = delegate.connection;
Controller.activeConnection = model.connection;
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 {

View File

@@ -62,8 +62,6 @@
#include "fakerunner.h"
#endif
#include "imagecontentmanager.h"
#ifdef Q_OS_WINDOWS
#include <Windows.h>
#endif

View 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();
};

View File

@@ -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();
});
}

View File

@@ -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;
};

View File

@@ -600,19 +600,14 @@ bool ActionsModel::handleQuickEditAction(NeoChatRoom *room, const QString &messa
} else {
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) {
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, replaceId);
room->postHtmlMessage(messageText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
} else {
room->postHtmlMessage(messageText,
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
event->msgtype(),
{},
replaceId);
event->id());
}
return true;
}

View File

@@ -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"},
};
}

View File

@@ -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;
};

View File

@@ -6,7 +6,8 @@
#include "actionsmodel.h"
#include "completionproxymodel.h"
// #include "emojimodel.h"
#include "customemojimodel.h"
#include "emojimodel.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "userlistmodel.h"
@@ -15,13 +16,11 @@ CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel())
, 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::roomChanged, this, [this]() {
m_userListModel->setRoom(m_room);
});
// TODO m_emojiModel->addSourceModel(&EmojiModel::instance());
m_emojiModel->addSourceModel(&CustomEmojiModel::instance());
m_emojiModel->addSourceModel(&EmojiModel::instance());
}
QString CompletionModel::text() const
@@ -86,23 +85,29 @@ QVariant CompletionModel::data(const QModelIndex &index, int role) const
return m_filterModel->data(filterIndex, RoomListModel::CanonicalAliasRole);
}
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 {};
}
@@ -148,8 +153,8 @@ void CompletionModel::updateCompletion()
|| (m_fullText.indexOf(QLatin1Char(' ')) != -1 && m_fullText.indexOf(QLatin1Char(':'), 1) > m_fullText.indexOf(QLatin1Char(' '), 1)))) {
m_filterModel->setSourceModel(m_emojiModel);
m_autoCompletionType = Emoji;
// m_filterModel->setFilterRole(CustomEmojiModel::Name);
// TODO m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
m_filterModel->setFilterRole(CustomEmojiModel::Name);
m_filterModel->setSecondaryFilterRole(EmojiModel::DescriptionRole);
m_filterModel->setFullText(m_fullText);
m_filterModel->setFilterText(m_text);
m_filterModel->invalidate();

View 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();
};

View File

@@ -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"},
};
}

View File

@@ -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;
};

View 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"

View File

@@ -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)
{
}

View File

@@ -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;
};

View File

@@ -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);
}
}

View File

@@ -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();
};

View File

@@ -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"

View File

@@ -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;
};

View File

@@ -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"},
};
}

View File

@@ -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;
};

View File

@@ -1,43 +1,170 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "imagepacksmodel.h"
#include "neochatroom.h"
#include "models/accountimagepackmodel.h"
#include "models/emojipacksmodel.h"
#include "models/historyimagepackmodel.h"
#include "models/imagepackroomsmodel.h"
#include "models/roomimagepacksmodel.h"
#include <KLocalizedString>
using namespace Quotient;
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
{
return {
{ImageContentPackRole::DisplayNameRole, "displayName"},
{ImageContentPackRole::IconRole, "icon"},
{ImageContentPackRole::IdentifierRole, "identifier"},
{DisplayNameRole, "displayName"},
{AvatarUrlRole, "avatarUrl"},
{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);
Q_EMIT currentRoomChanged();
if (m_room) {
disconnect(m_room, nullptr, this, nullptr);
disconnect(m_room->connection(), nullptr, this, nullptr);
}
m_room = room;
if (m_room) {
connect(m_room->connection(), &Connection::accountDataChanged, this, [this](const QString &type) {
if (type == "im.ponies.user_emotes"_ls) {
reloadImages();
}
});
}
// TODO listen to packs changing
reloadImages();
Q_EMIT roomChanged();
}
void ImagePacksModel::reloadImages()
{
if (!m_room) {
return;
}
beginResetModel();
m_events.clear();
// Load emoticons from the account data
if (m_room->connection()->hasAccountData("im.ponies.user_emotes"_ls)) {
auto json = m_room->connection()->accountData("im.ponies.user_emotes"_ls)->contentJson();
json["pack"_ls] = QJsonObject{
{"display_name"_ls,
m_showStickers ? i18nc("As in 'The user's own Stickers'", "Own Stickers") : i18nc("As in 'The user's own emojis", "Own Emojis")},
};
const auto &content = ImagePackEventContent(json);
if (!content.images.isEmpty()) {
m_events += ImagePackEventContent(json);
}
}
// Load emoticons from the saved rooms
const auto &accountData = m_room->connection()->accountData("im.ponies.emote_rooms"_ls);
if (accountData) {
const auto &rooms = accountData->contentJson()["rooms"_ls].toObject();
for (const auto &roomId : rooms.keys()) {
if (roomId == m_room->id()) {
continue;
}
auto packs = rooms[roomId].toObject();
const auto &stickerRoom = m_room->connection()->room(roomId);
if (!stickerRoom) {
continue;
}
for (const auto &packKey : packs.keys()) {
if (const auto &pack = stickerRoom->currentState().get<ImagePackEvent>(packKey)) {
const auto packContent = pack->content();
if ((!packContent.pack || !packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers()))
&& !packContent.images.isEmpty()) {
m_events += packContent;
}
}
}
}
}
// Load emoticons from the current room
auto events = m_room->currentState().eventsOfType("im.ponies.room_emotes"_ls);
for (const auto &event : events) {
auto packContent = eventCast<const ImagePackEvent>(event)->content();
if (packContent.pack.has_value()) {
if (!packContent.pack->usage || (packContent.pack->usage->contains("emoticon"_ls) && showEmoticons())
|| (packContent.pack->usage->contains("sticker"_ls) && showStickers())) {
m_events += packContent;
}
}
}
Q_EMIT imagesLoaded();
endResetModel();
}
bool ImagePacksModel::showStickers() const
{
return m_showStickers;
}
void ImagePacksModel::setShowStickers(bool showStickers)
{
m_showStickers = showStickers;
Q_EMIT showStickersChanged();
}
bool ImagePacksModel::showEmoticons() const
{
return m_showEmoticons;
}
void ImagePacksModel::setShowEmoticons(bool showEmoticons)
{
m_showEmoticons = showEmoticons;
Q_EMIT showEmoticonsChanged();
}
QList<Quotient::ImagePackEventContent::ImagePackImage> ImagePacksModel::images(int index)
{
if (index < 0 || index >= m_events.size()) {
return {};
}
return m_events[index].images;
}
#include "moc_imagepacksmodel.cpp"

View File

@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QConcatenateTablesProxyModel>
#pragma once
#include "events/imagepackevent.h"
#include <QAbstractListModel>
@@ -9,26 +9,95 @@
#include <QPointer>
#include <QQmlEngine>
#include "neochatroom.h"
class NeoChatRoom;
class RoomImagePacksModel;
class ImagePacksModel : public QConcatenateTablesProxyModel
/**
* @class ImagePacksModel
*
* Defines the model for visualising image packs.
*
* See Matrix MSC2545 for more details on image packs.
* https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
*/
class ImagePacksModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
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:
ImagePacksModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] NeoChatRoom *currentRoom() const;
void setCurrentRoom(NeoChatRoom *currentRoom);
/**
* @brief Defines the model roles.
*/
enum Roles {
DisplayNameRole = Qt::DisplayRole, /**< The display name of the image pack. */
AvatarUrlRole, /**< The source mxc URL for the pack avatar. */
AttributionRole, /**< The attribution for the pack author(s). */
IdRole, /**< The ID of the image pack. */
};
Q_ENUM(Roles)
explicit ImagePacksModel(QObject *parent = nullptr);
/**
* @brief Get the given role value at the given index.
*
* @sa QAbstractItemModel::data
*/
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief Number of rows in the model.
*
* @sa QAbstractItemModel::rowCount
*/
[[nodiscard]] int rowCount(const QModelIndex &index) const override;
/**
* @brief Returns a mapping from Role enum values to role names.
*
* @sa Roles, QAbstractItemModel::roleNames()
*/
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[nodiscard]] bool showStickers() const;
void setShowStickers(bool showStickers);
[[nodiscard]] bool showEmoticons() const;
void setShowEmoticons(bool showEmoticons);
/**
* @brief Return a vector of the images in the pack at the given index.
*/
[[nodiscard]] QList<Quotient::ImagePackEventContent::ImagePackImage> images(int index);
Q_SIGNALS:
void currentRoomChanged();
void roomChanged();
void showStickersChanged();
void showEmoticonsChanged();
void imagesLoaded();
private:
RoomImagePacksModel *m_roomImagePacksModel = nullptr;
QPointer<NeoChatRoom> m_room;
QList<Quotient::ImagePackEventContent> m_events;
bool m_showStickers = true;
bool m_showEmoticons = true;
void reloadImages();
};

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -34,7 +34,7 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(eventId)
, m_currentState(isPending ? Pending : Unknown)
, m_isPending(isPending)
, m_isReply(isReply)
{
initializeModel();
@@ -45,27 +45,19 @@ void MessageContentModel::initializeModel()
Q_ASSERT(m_room != nullptr);
Q_ASSERT(!m_eventId.isEmpty());
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
if (m_room != nullptr && m_currentState == Unknown) {
initializeEvent();
updateReplyModel();
resetModel();
}
});
connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent);
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
beginResetModel();
m_isPending = false;
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) {
if (m_room != nullptr) {
for (int i = fromIndex; i <= toIndex; i++) {
@@ -151,33 +143,20 @@ void MessageContentModel::initializeModel()
});
initializeEvent();
if (m_currentState == Available || m_currentState == Pending) {
updateReplyModel();
}
updateReplyModel();
resetModel();
}
void MessageContentModel::initializeEvent()
{
if (m_currentState == UnAvailable) {
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
Q_EMIT eventUnavailable();
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) {
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
// be the local member.
if (senderId.isEmpty()) {
@@ -193,6 +172,7 @@ void MessageContentModel::getEvent()
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_notFound = false;
initializeEvent();
updateReplyModel();
resetModel();
@@ -204,7 +184,7 @@ void MessageContentModel::getEvent()
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
if (m_room != nullptr) {
if (eventId == m_eventId) {
m_currentState = UnAvailable;
m_notFound = true;
resetModel();
return true;
}
@@ -257,7 +237,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
const auto component = m_components[index.row()];
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
if (event == nullptr) {
if (role == DisplayRole) {
if (m_isReply) {
return i18n("Loading reply");
@@ -272,7 +252,7 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
}
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 =
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()) {
return component.content;
}
return EventHandler::richBody(m_room, event.first);
return EventHandler::richBody(m_room, event);
}
if (role == ComponentTypeRole) {
return component.type;
@@ -305,53 +285,53 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes;
}
if (role == EventIdRole) {
return EventHandler::id(event.first);
return EventHandler::id(event);
}
if (role == TimeRole) {
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();
return EventHandler::time(event.first, m_currentState == Pending, lastUpdated);
return EventHandler::time(event, m_isPending, lastUpdated);
}
if (role == TimeStringRole) {
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();
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) {
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
}
if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_room, event.first);
return EventHandler::mediaInfo(m_room, event);
}
if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first));
return QVariant::fromValue(m_room->cachedFileTransferInfo(event));
}
if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
}
if (role == LatitudeRole) {
return EventHandler::latitude(event.first);
return EventHandler::latitude(event);
}
if (role == LongitudeRole) {
return EventHandler::longitude(event.first);
return EventHandler::longitude(event);
}
if (role == AssetRole) {
return EventHandler::locationAssetType(event.first);
return EventHandler::locationAssetType(event);
}
if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
}
if (role == ReplyEventIdRole) {
return EventHandler::replyId(event.first);
return EventHandler::replyId(event);
}
if (role == ReplyAuthorRole) {
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event));
}
if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
@@ -407,17 +387,18 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
void MessageContentModel::resetModel()
{
const auto event = m_room->getEvent(m_eventId);
beginResetModel();
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(), {}};
endResetModel();
return;
}
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
if (event == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel();
return;
@@ -450,19 +431,19 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
if (event == nullptr) {
return {};
}
QList<MessageComponent> newComponents;
if (eventCast<const Quotient::RoomMessageEvent>(event.first)
&& eventCast<const Quotient::RoomMessageEvent>(event.first)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
if (eventCast<const Quotient::RoomMessageEvent>(event)
&& eventCast<const Quotient::RoomMessageEvent>(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents;
}
if (event.first->isRedacted()) {
if (event->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents;
}
@@ -474,7 +455,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event)));
}
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 (isThreading && !EventHandler::isThreaded(event.first)) {
if (isThreading && !EventHandler::isThreaded(event)) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
}
@@ -492,11 +473,11 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
void MessageContentModel::updateReplyModel()
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr || m_isReply) {
if (event == nullptr || m_isReply) {
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) {
delete m_replyModel;
}
@@ -507,7 +488,7 @@ void MessageContentModel::updateReplyModel()
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]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
@@ -517,13 +498,13 @@ void MessageContentModel::updateReplyModel()
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
{
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
if (event == nullptr) {
return {};
}
switch (type) {
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);
return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
@@ -534,11 +515,11 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
case MessageComponentType::File: {
QList<MessageComponent> components;
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_isReply) {
auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first);
auto fileTransferInfo = m_room->cachedFileTransferInfo(event);
#ifndef Q_OS_ANDROID
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has<EventContent::FileContent>());
@@ -586,24 +567,17 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
case MessageComponentType::Image:
case MessageComponentType::Audio:
case MessageComponentType::Video: {
if (!event.first->is<StickerEvent>()) {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
if (fileContent != nullptr) {
const auto fileInfo = fileContent->commonInfo();
const auto body = EventHandler::rawMessageBody(*roomMessageEvent);
// Do not attach the description to the image, if it's the same as the original filename.
if (fileInfo.originalName != body) {
QList<MessageComponent> components;
components += MessageComponent{type, QString(), {}};
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
}
if (!event->is<StickerEvent>()) {
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
QList<MessageComponent> components;
components += MessageComponent{type, QString(), {}};
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components;
}
}
default:
@@ -679,13 +653,13 @@ void MessageContentModel::closeLinkPreview(int row)
void MessageContentModel::updateItineraryModel()
{
const auto event = m_room->getEvent(m_eventId);
if (m_room == nullptr || event.first == nullptr) {
if (m_room == nullptr || event == nullptr) {
return;
}
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
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) {
delete m_itineraryModel;
m_itineraryModel = nullptr;

View File

@@ -31,14 +31,6 @@ class MessageContentModel : public QAbstractListModel
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged)
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.
*/
@@ -106,6 +98,7 @@ public:
Q_SIGNALS:
void showAuthorChanged();
void eventUnavailable();
void eventUpdated();
private:
@@ -114,9 +107,10 @@ private:
QString m_eventSenderId;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
MessageState m_currentState = Unknown;
bool m_isPending;
bool m_showAuthor = true;
bool m_isReply;
bool m_notFound = false;
void initializeModel();
void initializeEvent();

View File

@@ -160,21 +160,12 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
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) {
m_initialized = true;
createEventObjects(event, true);
createEventObjects(event);
beginInsertRows({}, 0, 0);
});
connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows);
#endif
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) {
Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole});
if (i == 0) {
@@ -627,7 +618,7 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
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) {
return;
@@ -650,7 +641,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, boo
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
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));
}
}

View File

@@ -136,7 +136,7 @@ private:
int refreshEventRoles(const QString &eventId, const QList<int> &roles = {});
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
bool m_initialized = false;

View File

@@ -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"},
};
}

View File

@@ -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;
};

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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();
});
}

View File

@@ -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;
};

View File

@@ -212,7 +212,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return room->displayName().toHtmlEscaped();
}
if (role == AvatarRole) {
return room->avatarMediaUrl();
return room->avatarMediaId();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();

View File

@@ -324,7 +324,7 @@ QVariant RoomTreeModel::data(const QModelIndex &index, int role) const
return room->displayName();
}
if (role == AvatarRole) {
return room->avatarMediaUrl();
return room->avatarMediaId();
}
if (role == CanonicalAliasRole) {
return room->canonicalAlias();

View File

@@ -431,9 +431,9 @@ QDateTime NeoChatRoom::lastActiveTime()
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;
}
@@ -441,7 +441,7 @@ QUrl NeoChatRoom::avatarMediaUrl() const
const auto directChatMembers = this->directChatMembers();
for (const auto member : directChatMembers) {
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()) {
return {};
return nullptr;
}
const auto timelineIt = findInTimeline(eventId);
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()) {
return std::make_pair(pendingIt->event(), true);
}
// findPendingEvent() searches by transaction ID, we also need to check event ID.
for (const auto &event : pendingEvents()) {
if (event->id() == eventId || event->transactionId() == eventId) {
return std::make_pair(event.event(), true);
}
return pendingIt->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 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

Some files were not shown because too many files have changed in this diff Show More