Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711147
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/contents/ui/StickerPicker.qml b/src/contents/ui/StickerPicker.qml
index f026017..1d6d656 100644
--- a/src/contents/ui/StickerPicker.qml
+++ b/src/contents/ui/StickerPicker.qml
@@ -1,93 +1,87 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
ColumnLayout {
id: stickerPicker
property var stickerPackList
spacing: 0
+ property var currentIndex: 0
signal sendMessageRequested(var eventJson)
- ScrollView {
- id: packListScrollView
+ TabBar {
+ id: packListView
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 2
- Layout.preferredHeight: packListView.height
-
- ListView {
- id: packListView
- orientation: ListView.Horizontal
- height: contentHeight
- anchors.fill: parent
- currentIndex: 0
+ Repeater {
model: stickerPackList
- delegate: ToolButton {
+ delegate: TabButton {
objectName: `stickerPack${index}`
property var pack: stickerPackList.at(index)
icon.name: 'smiley'
- text: l10n.get('sticker-picker-user-stickers')
- checked: ListView.currentIndex === index
- onClicked: ListView.currentIndex = index
+ text: pack.packName ? pack.packName : l10n.get('sticker-picker-user-stickers')
+ checked: stickerPicker.currentIndex === index
+ onClicked: stickerPicker.currentIndex = index
}
}
}
- property var currentPack: packListView.currentItem && packListView.currentItem.pack
+ property var currentPack: stickerPackList.at(currentIndex)
property var stickerSize: Kirigami.Units.iconSizes.enormous
property var stickerMargin: Kirigami.Units.smallSpacing
GridView {
id: stickersGridView
Layout.fillWidth: true
Layout.minimumHeight: stickerPicker.stickerSize * 5
Layout.preferredHeight: stickersGridView.height
model: currentPack
cellWidth: stickerPicker.stickerSize + stickerPicker.stickerMargin
cellHeight: stickerPicker.stickerSize + stickerPicker.stickerMargin
delegate: MouseArea {
objectName: `sticker${index}`
property var sticker: currentPack.at(index)
height: stickerPicker.stickerSize + stickerPicker.stickerMargin
width: stickerPicker.stickerSize + stickerPicker.stickerMargin
Image {
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
height: stickerPicker.stickerSize
width: stickerPicker.stickerSize
source: matrixSdk.mxcUriToHttp(sticker.mxcUri)
}
Rectangle {
visible: hoverHandler.hovered
z: -1
anchors.fill: parent
color: Kirigami.Theme.activeBackgroundColor
}
HoverHandler {
id: hoverHandler
}
onClicked: stickerPicker.sendMessageRequested(sticker.makeEventJson())
}
}
Component.onCompleted: {
console.log('stickerpicker finished', stickerPackList.count);
}
}
diff --git a/src/matrix-sticker-pack-list.cpp b/src/matrix-sticker-pack-list.cpp
index 3380ce6..ed2b8d6 100644
--- a/src/matrix-sticker-pack-list.cpp
+++ b/src/matrix-sticker-pack-list.cpp
@@ -1,54 +1,80 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <lager/lenses/optional.hpp>
#include <lager/lenses/at.hpp>
#include "matrix-sticker-pack.hpp"
#include "matrix-sticker-pack-list.hpp"
using namespace Kazv;
static const std::string accountDataEventType = "im.ponies.user_emotes";
+static const std::string roomStateEventType = "im.ponies.room_emotes";
+static const std::string imagePackRoomsEventType = "im.ponies.emote_rooms";
+
+using ImagePackRoomMap = immer::map<std::string/* room id */, immer::map<std::string/* */, json>>;
lager::reader<immer::flex_vector<MatrixStickerPackSource>> getEventsFromClient(Client client)
{
lager::reader<Event> userEmotes = client.accountData()[accountDataEventType][lager::lenses::or_default];
- return userEmotes.map([](Event e) {
- return immer::flex_vector<MatrixStickerPackSource>{
- {MatrixStickerPackSource::AccountData, accountDataEventType, e},
- };
- });
+ return lager::with(
+ userEmotes,
+ client.accountData()[imagePackRoomsEventType][lager::lenses::or_default]
+ // The following jsonAtOr performs the validation for us:
+ // if the content does not conform to the string-to-string-to-anything map, return an empty one
+ .xform(eventContent | jsonAtOr("rooms", ImagePackRoomMap{})),
+ client.rooms()).map([](
+ const Event &userEmotes,
+ const ImagePackRoomMap &emoteRooms,
+ const immer::map<std::string, RoomModel> &rooms) {
+ // This transformation function takes O(|room sticker packs specified in account data|).
+ auto res = immer::flex_vector<MatrixStickerPackSource>{
+ {MatrixStickerPackSource::AccountData, accountDataEventType, userEmotes}
+ }.transient();
+
+ for (const auto &[roomId, stateKeyMap] : emoteRooms) {
+ for (const auto &[stateKey, _] : stateKeyMap) {
+ res.push_back(MatrixStickerPackSource{
+ MatrixStickerPackSource::RoomState,
+ roomStateEventType,
+ rooms[roomId].stateEvents[KeyOfState{roomStateEventType, stateKey}]
+ });
+ }
+ }
+
+ return res.persistent();
+ }).make();
}
MatrixStickerPackList::MatrixStickerPackList(Client client, QObject *parent)
: KazvAbstractListModel(parent)
, m_client(client)
, m_events(getEventsFromClient(m_client))
{
initCountCursor(m_events.map([](const auto &events) {
return static_cast<int>(events.size());
}));
}
MatrixStickerPackList::~MatrixStickerPackList() = default;
MatrixStickerPack *MatrixStickerPackList::at(int index) const
{
return new MatrixStickerPack(m_events.map([index](const auto &events) {
if (events.size() > std::size_t(index)) {
return events[index];
} else {
return MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
accountDataEventType,
Event(),
};
}
}));
}
diff --git a/src/matrix-sticker-pack.cpp b/src/matrix-sticker-pack.cpp
index e86f694..d48931e 100644
--- a/src/matrix-sticker-pack.cpp
+++ b/src/matrix-sticker-pack.cpp
@@ -1,114 +1,115 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <cursorutil.hpp>
-
+#include "helper.hpp"
#include "matrix-sticker.hpp"
#include "matrix-event.hpp"
#include "matrix-sticker-pack.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
MatrixStickerPack::MatrixStickerPack(lager::reader<MatrixStickerPackSource> source, QObject *parent)
: KazvAbstractListModel(parent)
, m_source(source)
, m_event(m_source[&MatrixStickerPackSource::event])
, m_images(m_event.xform(
eventContent
| zug::map([](const JsonWrap &content) {
if (content.get().contains("images")
&& content.get()["images"].is_object()) {
auto size = content.get()["images"].size();
auto items = content.get()["images"].items();
auto array = json(size, json::object());
std::transform(
items.begin(), items.end(),
array.begin(), [](const auto &it) {
return json::array({it.key(), it.value()});
}
);
return array;
} else {
return json::array();
}
})))
+ , LAGER_QT(packName)(m_event.xform(eventContent | jsonAtOr("/pack/display_name"_json_pointer, std::string()) | strToQt))
{
initCountCursor(m_images.map([](const JsonWrap &images) -> int {
return images.get().size();
}));
}
MatrixStickerPack::~MatrixStickerPack() = default;
MatrixSticker *MatrixStickerPack::at(int index) const
{
using namespace nlohmann::literals;
auto image = m_images.map([index](const json &j) {
if (j.size() > std::size_t(index)) {
return j[index];
} else {
return json::array();
}
}).make();
return new MatrixSticker(
image.xform(jsonAtOr("/0"_json_pointer, json(std::string())) | zug::map([](const json &j) {
return j.template get<std::string>();
})),
image.xform(jsonAtOr("/1"_json_pointer, json::object()))
);
}
bool MatrixStickerPack::hasShortCode(const QString &shortCode) const
{
return m_event.map([shortCode=shortCode.toStdString()](const Event &e) {
auto content = e.content();
return content.get().contains("images")
&& content.get()["images"].is_object()
&& content.get()["images"].contains(shortCode);
})
.make().get();
}
QVariant MatrixStickerPack::addSticker(const QString &shortCode, MatrixEvent *event) const
{
if (!event) {
return QVariant::fromValue(m_source.get());
}
auto source = m_source.get();
auto eventJson = source.event.raw().get();
eventJson.merge_patch(json{
{"content", {
{"images", {
{shortCode.toStdString(), json::object()},
}},
}},
});
auto &sticker = eventJson["content"]["images"][shortCode.toStdString()];
auto content = event->content();
auto body = event->content()[u"body"_s];
if (!body.isUndefined()) {
sticker["body"] = body;
}
auto url = event->content()[u"url"_s];
if (!url.isUndefined()) {
sticker["url"] = url;
}
auto info = event->content()[u"info"_s];
if (!info.isUndefined()) {
sticker["info"] = info;
}
source.event = Event(eventJson);
return QVariant::fromValue(source);
}
diff --git a/src/matrix-sticker-pack.hpp b/src/matrix-sticker-pack.hpp
index 2edd558..fd532ba 100644
--- a/src/matrix-sticker-pack.hpp
+++ b/src/matrix-sticker-pack.hpp
@@ -1,53 +1,55 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include <QObject>
#include <QQmlEngine>
#include <lager/reader.hpp>
#include <lager/extra/qt.hpp>
#include <base/event.hpp>
#include "kazv-abstract-list-model.hpp"
#include "matrix-sticker-pack-source.hpp"
#include "meta-types.hpp"
Q_MOC_INCLUDE("matrix-sticker.hpp")
Q_MOC_INCLUDE("matrix-event.hpp")
class MatrixSticker;
class MatrixEvent;
class MatrixStickerPack : public KazvAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
lager::reader<MatrixStickerPackSource> m_source;
lager::reader<Kazv::Event> m_event;
lager::reader<Kazv::json> m_images;
public:
explicit MatrixStickerPack(lager::reader<MatrixStickerPackSource> source, QObject *parent = 0);
~MatrixStickerPack() override;
Q_INVOKABLE MatrixSticker *at(int index) const;
+ LAGER_QT_READER(QString, packName);
+
Q_INVOKABLE bool hasShortCode(const QString &shortCode) const;
/**
* Add a sticker to the pack and return the source of the
* modified pack.
*
* @param shortCode The short code of the sticker.
* @param event The event of the sticker.
* @return The MatrixStickerPackSource of the modified pack.
*/
Q_INVOKABLE QVariant addSticker(const QString &shortCode, MatrixEvent *event) const;
};
diff --git a/src/tests/matrix-sticker-pack-test.cpp b/src/tests/matrix-sticker-pack-test.cpp
index 3b45760..9a5711d 100644
--- a/src/tests/matrix-sticker-pack-test.cpp
+++ b/src/tests/matrix-sticker-pack-test.cpp
@@ -1,154 +1,225 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <memory>
#include <QtTest>
#include <lager/state.hpp>
#include <base/event.hpp>
#include <testfixtures/factory.hpp>
#include "test-model.hpp"
#include "test-utils.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack.hpp"
#include "matrix-sticker.hpp"
#include "matrix-event.hpp"
#include "matrix-sdk.hpp"
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
using namespace Kazv::Factory;
class MatrixStickerPackTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testStickerPack();
void testStickerPackList();
void testAddToPack();
};
// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
static auto stickerPackEvent = Event{R"({
"content": {
"images": {
"myemote": {
"url": "mxc://example.org/blah"
},
"mysticker": {
"body": "my sticker",
"url": "mxc://example.org/sticker",
"usage": ["sticker"],
"info": {
"mimetype": "image/png"
}
}
},
"pack": {
"display_name": "Awesome Pack",
"usage": ["emoticon"]
}
},
"type": "im.ponies.user_emotes"
})"_json};
+static const auto imagePackRoomsEvent = Event(R"({
+ "content": {
+ "rooms": {
+ "!someroom:example.org": {
+ "": {},
+ "de.sorunome.mx-puppet-bridge.discord": {}
+ },
+ "!someotherroom:example.org": {
+ "": {}
+ }
+ }
+ },
+ "type": "im.ponies.emote_rooms"
+})"_json);
+
+static Event getRoomStickersEvent(std::string stateKey, std::string name)
+{
+ auto j = R"({
+ "content": {
+ "images": {
+ "myemote": {
+ "url": "mxc://example.org/blah"
+ },
+ "mysticker": {
+ "body": "my sticker",
+ "url": "mxc://example.org/sticker",
+ "usage": ["sticker"],
+ "info": {
+ "mimetype": "image/png"
+ }
+ }
+ },
+ "pack": {
+ "usage": ["emoticon"]
+ }
+ },
+ "type": "im.ponies.room_emotes"
+})"_json;
+ j["state_key"] = stateKey;
+ j["content"]["pack"]["display_name"] = name;
+ return Event(j);
+}
+
void MatrixStickerPackTest::testStickerPack()
{
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
QCOMPARE(stickerPack->rowCount(QModelIndex()), 2);
QCOMPARE(stickerPack->count(), 2);
QVERIFY(stickerPack->hasShortCode(QStringLiteral("myemote")));
QVERIFY(!stickerPack->hasShortCode(QStringLiteral("lolol")));
auto sticker0 = toUniquePtr(stickerPack->at(0));
QCOMPARE(sticker0->shortCode(), QStringLiteral("myemote"));
QCOMPARE(sticker0->body(), QStringLiteral("myemote"));
QCOMPARE(sticker0->mxcUri(), QStringLiteral("mxc://example.org/blah"));
QCOMPARE(sticker0->info(), QJsonObject());
QCOMPARE(sticker0->makeEventJson(), (QJsonObject{
{u"type"_s, u"m.sticker"_s},
{u"content"_s, QJsonObject{
{u"body"_s, u"myemote"_s},
{u"url"_s, u"mxc://example.org/blah"_s},
{u"info"_s, QJsonObject()},
}},
}));
auto sticker1 = toUniquePtr(stickerPack->at(1));
QCOMPARE(sticker1->shortCode(), QStringLiteral("mysticker"));
QCOMPARE(sticker1->body(), QStringLiteral("my sticker"));
QCOMPARE(sticker1->mxcUri(), QStringLiteral("mxc://example.org/sticker"));
QCOMPARE(sticker1->info(), (QJsonObject{{u"mimetype"_s, u"image/png"_s}}));
QCOMPARE(sticker1->makeEventJson(), (QJsonObject{
{u"type"_s, u"m.sticker"_s},
{u"content"_s, QJsonObject{
{u"body"_s, u"my sticker"_s},
{u"url"_s, u"mxc://example.org/sticker"_s},
{u"info"_s, QJsonObject{{u"mimetype"_s, u"image/png"_s}}},
}},
}));
}
void MatrixStickerPackTest::testStickerPackList()
{
- auto model = makeClient(withAccountData({stickerPackEvent}));
+ auto model = makeClient(
+ withAccountData({stickerPackEvent, imagePackRoomsEvent})
+ | withRoom(makeRoom(
+ withRoomId("!someroom:example.org")
+ | withRoomState({
+ getRoomStickersEvent("", "Pack 1"),
+ getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
+ })
+ ))
+ | withRoom(makeRoom(
+ withRoomId("!someotherroom:example.org")
+ | withRoomState({getRoomStickersEvent("", "Pack 3")})
+ ))
+ );
std::unique_ptr<MatrixSdk> sdk{makeTestSdk(SdkModel{model})};
auto stickerPackList = toUniquePtr(sdk->stickerPackList());
- QCOMPARE(stickerPackList->rowCount(QModelIndex()), 1);
- QCOMPARE(stickerPackList->count(), 1);
+ QCOMPARE(stickerPackList->rowCount(QModelIndex()), 4);
+ QCOMPARE(stickerPackList->count(), 4);
- auto stickerPack = toUniquePtr(stickerPackList->at(0));
- QCOMPARE(stickerPack->count(), 2);
+ auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 4>{
+ toUniquePtr(stickerPackList->at(0)),
+ toUniquePtr(stickerPackList->at(1)),
+ toUniquePtr(stickerPackList->at(2)),
+ toUniquePtr(stickerPackList->at(3)),
+ };
+
+ auto hasPack = [&packs](const QString &name) {
+ return std::any_of(packs.begin(), packs.end(), [&name](const auto &pack) {
+ return pack->packName() == name;
+ });
+ };
+
+ QVERIFY(hasPack(u"Awesome Pack"_s));
+ QVERIFY(hasPack(u"Pack 1"_s));
+ QVERIFY(hasPack(u"Pack 2"_s));
+ QVERIFY(hasPack(u"Pack 3"_s));
}
void MatrixStickerPackTest::testAddToPack()
{
using namespace nlohmann::literals;
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
auto contentJson = json{
{"url", "mxc://example.org/someotheremote"},
{"info", {{"mimetype", "image/png"}}},
{"body", "some other emote"},
};
auto eventCursor = lager::make_constant(makeEvent(
withEventContent(contentJson)
));
auto event = toUniquePtr(new MatrixEvent(eventCursor));
auto newSource = stickerPack->addSticker(u"someotheremote"_s, event.get()).template value<MatrixStickerPackSource>();
auto expected = sourceCursor.get().event.content().get();
expected["images"]["someotheremote"] = contentJson;
QVERIFY(newSource.event.content().get() == expected);
}
QTEST_MAIN(MatrixStickerPackTest)
#include "matrix-sticker-pack-test.moc"
diff --git a/src/tests/quick-tests/tst_StickerPicker.qml b/src/tests/quick-tests/tst_StickerPicker.qml
index a1f0043..e943d6d 100644
--- a/src/tests/quick-tests/tst_StickerPicker.qml
+++ b/src/tests/quick-tests/tst_StickerPicker.qml
@@ -1,97 +1,126 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtTest 1.0
import '../../contents/ui' as Kazv
import 'test-helpers.js' as Helpers
import 'test-helpers' as TestHelpers
import moe.kazv.mxc.kazv 0.0 as MK
Item {
id: item
width: 800
height: 600
property var l10n: Helpers.fluentMock
property var matrixSdk: TestHelpers.MatrixSdkMock {}
property var sdkVars: ({})
function makeSticker(sticker) {
return {
type: 'm.sticker',
content: {
body: sticker.body,
url: sticker.mxcUri,
info: sticker.info,
},
};
}
property list<ListModel> stickerPacks: [
ListModel {
id: stickerPack0
ListElement {
shortCode: 'some'
body: 'some'
mxcUri: 'mxc://example.org/some'
makeEventJson: () => makeSticker(stickerPack0.get(0))
}
ListElement {
shortCode: 'some1'
body: 'some1'
mxcUri: 'mxc://example.org/some1'
makeEventJson: () => makeSticker(stickerPack0.get(1))
}
function at(index) {
return stickerPack0.get(index);
}
+ },
+ ListModel {
+ id: stickerPack1
+ ListElement {
+ shortCode: 'some2'
+ body: 'some2'
+ mxcUri: 'mxc://example.org/some2'
+ makeEventJson: () => makeSticker(stickerPack1.get(0))
+ }
+
+ ListElement {
+ shortCode: 'some3'
+ body: 'some3'
+ mxcUri: 'mxc://example.org/some3'
+ makeEventJson: () => makeSticker(stickerPack1.get(1))
+ }
+
+ function at(index) {
+ return stickerPack1.get(index);
+ }
}
]
SignalSpy {
id: sendMessageRequestedSpy
signalName: 'sendMessageRequested'
}
Kazv.StickerPicker {
id: stickerPicker
stickerPackList: ListModel {
id: stickerPackListModel
ListElement {
}
+ ListElement {
+ }
function at(index) {
return stickerPacks[index];
}
}
}
TestCase {
id: stickerPickerTest
name: 'StickerPickerTest'
when: windowShown
function init() {
sendMessageRequestedSpy.clear();
sendMessageRequestedSpy.target = stickerPicker;
}
function test_stickerPicker() {
verify(findChild(stickerPicker, 'stickerPack0'));
+ verify(findChild(stickerPicker, 'stickerPack1'));
verify(findChild(stickerPicker, 'sticker0'));
verify(findChild(stickerPicker, 'sticker1'));
const stickerButton = findChild(stickerPicker, 'sticker1');
mouseClick(stickerButton);
- tryVerify(() => sendMessageRequestedSpy.count, 1000);
+ tryVerify(() => sendMessageRequestedSpy.count === 1, 1000);
verify(Helpers.deepEqual(sendMessageRequestedSpy.signalArguments[0][0], makeSticker(stickerPack0.get(1))));
+
+ mouseClick(findChild(stickerPicker, 'stickerPack1'));
+ tryVerify(() => findChild(stickerPicker, 'sticker0').sticker.mxcUri === 'mxc://example.org/some2');
+ mouseClick(findChild(stickerPicker, 'sticker0'));
+ tryVerify(() => sendMessageRequestedSpy.count === 2, 1000);
+ verify(Helpers.deepEqual(sendMessageRequestedSpy.signalArguments[1][0], makeSticker(stickerPack1.get(0))));
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 4:12 AM (1 d, 2 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768827
Default Alt Text
(22 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment