Page MenuHomePhorge

No OneTemporary

Size
22 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline