Page MenuHomePhorge

No OneTemporary

Size
78 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml b/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
index 6974671..574f6b6 100644
--- a/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
+++ b/src/contents/ui/room-settings/RoomStickerPackItemDelegate.qml
@@ -1,80 +1,80 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates 1.0 as KAD
import moe.kazv.mxc.kazv as MK
import '..' as Kazv
import '.' as RoomSettings
import '../matrix-helpers.js' as Helpers
Kirigami.SwipeListItem {
id: upper
property var stickerPack
property var stickerRoomsEvent
text: stickerPack.packName || stickerPack.stateKey
contentItem: KAD.SubtitleContentItem {
itemDelegate: upper
subtitle: stickerPack.packName ? stickerPack.stateKey : ''
}
actions: [
Kirigami.Action {
objectName: 'usePackAction'
text: l10n.get('room-sticker-packs-use-action')
checkable: true
checked: upper.getIsUsingPack()
icon.name: checked ? 'starred-symbolic' : 'non-starred-symbolic'
onTriggered: checked ? upper.usePack() : upper.disusePack()
}
]
function getIsUsingPack() {
return Object.entries(stickerRoomsEvent.content.rooms || {})
.some(([roomId, packs]) =>
roomId === stickerPack.roomId &&
Object.entries(packs).some(([stateKey, _]) =>
stateKey === stickerPack.stateKey
)
);
}
function usePack() {
const content = stickerRoomsEvent.content;
content.rooms = content.rooms || {};
content.rooms[stickerPack.roomId] = content.rooms[stickerPack.roomId] || {};
content.rooms[stickerPack.roomId][stickerPack.stateKey] = {};
send(content);
}
function disusePack() {
const content = stickerRoomsEvent.content;
if (!stickerPack.stateKey in content.rooms?.[stickerPack.roomId]) {
return;
}
delete content.rooms[stickerPack.roomId][stickerPack.stateKey];
send(content);
}
- property var sendAccountData: Kazv.AsyncHandler {
- trigger: () => matrixSession.sendAccountData(Helpers.imagePackRoomsEventType, content);
+ property var updateStickerRooms: Kazv.AsyncHandler {
+ trigger: () => matrixSession.updateStickerRooms(content);
property var content
onResolved: (success, data) => {
if (!success) {
showPassiveNotification(l10n.get('room-sticker-packs-use-failed', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function send(content) {
- sendAccountData.content = content;
- sendAccountData.call();
+ updateStickerRooms.content = content;
+ updateStickerRooms.call();
}
}
diff --git a/src/contents/ui/room-settings/RoomStickerPacksPage.qml b/src/contents/ui/room-settings/RoomStickerPacksPage.qml
index 41a5463..f9cf203 100644
--- a/src/contents/ui/room-settings/RoomStickerPacksPage.qml
+++ b/src/contents/ui/room-settings/RoomStickerPacksPage.qml
@@ -1,90 +1,84 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import moe.kazv.mxc.kazv as MK
import '..' as Kazv
import '.' as RoomSettings
import '../matrix-helpers.js' as Helpers
Kazv.ClosableScrollableOverlay {
id: roomStickerPacksPage
property var room
property var stickerPackList: room.stickerPackList()
property var roomNameProvider: Kazv.RoomNameProvider {
room: roomStickerPacksPage.room
}
title: l10n.get('room-sticker-packs-page-title', {
room: roomNameProvider.name,
})
ListView {
model: roomStickerPacksPage.stickerPackList
delegate: RoomSettings.RoomStickerPackItemDelegate {
required property int index
stickerRoomsEvent: matrixSession.stickerRoomsEvent()
stickerPack: stickerPackList.at(index)
}
}
actions: [
Kirigami.Action {
objectName: 'addStickerPackAction'
text: l10n.get('room-sticker-packs-page-add-action')
icon.name: 'list-add'
onTriggered: addStickerPackPopup.open()
}
]
property var addStickerPack: Kazv.AsyncHandler {
id: addStickerPack
trigger: () => {
- const type = Helpers.roomImagePackEventType;
- const stateKey = stickerPackStateKey.text;
- const state = room.state(type, stateKey);
- const content = state.content;
- content.pack = content.pack || {};
- content.pack.display_name = stickerPackName.text;
- return roomStickerPacksPage.room.sendStateEvent({
- type,
- state_key: stateKey,
- content,
- });
+ return matrixSession.addRoomStickerPack(
+ room.roomId,
+ stickerPackStateKey.text,
+ stickerPackName.text,
+ );
}
onResolved: (success, data) => {
if (success) {
showPassiveNotification(l10n.get('room-sticker-packs-page-add-success'));
} else {
showPassiveNotification(l10n.get('room-sticker-packs-page-add-failed', { errorMsg: data.error, errorCode: data.errorCode }));
}
}
}
property var addStickerPackPopup: Kirigami.PromptDialog {
id: addStickerPackPopup
title: l10n.get('room-sticker-packs-page-add-popup-title')
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: addStickerPack.call()
Kirigami.FormLayout {
TextField {
id: stickerPackName
objectName: 'stickerPackName'
Kirigami.FormData.label: l10n.get('room-sticker-packs-page-add-popup-name-prompt')
}
TextField {
id: stickerPackStateKey
objectName: 'stickerPackStateKey'
Kirigami.FormData.label: l10n.get('room-sticker-packs-page-add-popup-state-key-prompt')
}
}
}
}
diff --git a/src/js/matrix-helpers.js b/src/js/matrix-helpers.js
index 43842c6..9e56fa0 100644
--- a/src/js/matrix-helpers.js
+++ b/src/js/matrix-helpers.js
@@ -1,129 +1,127 @@
function roomAvatarPlaceholderName(room)
{
return room.name || room.heroNames[0] || room.roomId;
}
function roomNameOrHeroes(room, heroes, userNameOverrides, l10n)
{
if (room.name) {
return l10n.get(
'room-list-view-room-item-title-name',
{
name: room.name,
}
);
} else if (room.heroEvents && room.heroEvents.length) {
const heroNames = room.heroEvents.map(ev => {
const displayName = ev.content && ev.content.displayname || '';
const userId = ev.state_key;
return userDisplayName(displayName, userId, userNameOverrides, l10n);
});
return l10n.get(
'room-list-view-room-item-title-heroes',
{
hero: heroNames[0],
secondHero: heroNames[1],
otherNum: heroNames.length - 1,
}
);
} else {
return l10n.get(
'room-list-view-room-item-title-id',
{
roomId: room.roomId,
}
);
}
}
function userNameWithId(user, l10n)
{
if (user.name) {
return l10n.get('user-name-with-id', { name: user.name, userId: user.userId });
} else {
return user.userId;
}
}
/**
* Check whether the event is read by a user.
*
* @param event The event to check.
* @param userId The id of the user to check.
* @return Whether @c event is read by the user whose id is @c userId.
*/
function isEventReadBy(event, userId)
{
const readers = event.readers();
for (let i = 0; i < readers.count; ++i) {
const reader = readers.at(i);
if (reader.userId === userId) {
return true;
}
}
return false;
}
/**
* Get the event body for editing.
*
* This takes into account the replied-to content.
* As Matrix spec has nothing for the source content of a message,
* we can only use some heuristics to determine what really is
* what the user has typed.
*
* @param event The event to get the body of.
* @return The body for editing.
*/
function getEventBodyForEditing(event) {
if (
event.replyingToEventId
// Element will put the quotation inside the body and it follows the
// following format:
// ```
// > <@userid:domain.ltd> whatever quoted content
// > other lines
// > other lines
//
// actual content
// ```
&& event.content.body.startsWith('> <@')
) {
const body = event.content.body;
const lines = body.split('\n');
const lastLineOfQuote = lines.findIndex((line, index) => {
// the current line is a quote but the next line is not
return line.startsWith('>') && !(lines[index + 1] || '').startsWith('>');
});
// Considering the empty line may not exist, strip the empty line from the beginning
const firstLineOfContent = lines[lastLineOfQuote + 1] ? lastLineOfQuote + 1 : lastLineOfQuote + 2
return lines.slice(firstLineOfContent).join('\n');
}
return event.content.body;
}
/**
* Get the display name for a user, possibly overrided.
*
* @param name The user's global display name.
* @param userId The user's id.
* @param overrides A map from user id to overrided names.
* @param l10n The fluent object.
* @return A calculated display name for the user.
*/
function userDisplayName(name, userId, overrides = {}, l10n)
{
const overridedName = overrides[userId];
const globalName = name || userId;
if (overridedName) {
return l10n.get('user-name-overrided', { overridedName, globalName });
} else {
return globalName;
}
}
const toolTipTimeout = 5000;
-const imagePackRoomsEventType = 'im.ponies.emote_rooms';
-const roomImagePackEventType = 'im.ponies.room_emotes';
diff --git a/src/matrix-session.cpp b/src/matrix-session.cpp
index 04f638f..b4af0e1 100644
--- a/src/matrix-session.cpp
+++ b/src/matrix-session.cpp
@@ -1,631 +1,680 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "matrix-session.hpp"
#include "matrix-utils.hpp"
#include "matrix-room-list.hpp"
#include "matrix-promise.hpp"
#include "matrix-event.hpp"
#include "device-mgmt/matrix-device-list.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack-list-p.hpp"
#include "matrix-user-given-attrs-map.hpp"
#include "matrix-verification-list.hpp"
#include "sso-login-process.hpp"
#include "db-store.hpp"
#include "helper.hpp"
#include "kazv-log.hpp"
#include <csapi/login.hpp>
#include <csapi/directory.hpp>
#include <client/alias.hpp>
#include <QFile>
using namespace Qt::Literals::StringLiterals;
using namespace Kazv;
static const std::string clientName = "kazv";
MatrixSession::MatrixSession(
MatrixSessionContextT context,
Client client,
LagerStoreEventEmitter::Watchable watchable,
lager::reader<VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent
)
: QObject(parent)
, m_context(std::move(context))
, m_clientOnSecondaryRoot(std::move(client))
, m_watchable(std::move(watchable))
, m_notificationHandler(m_clientOnSecondaryRoot.notificationHandler())
, m_verificationTrackerState(std::move(verificationTrackerState))
, m_ssoLoginProcess()
, m_dbStoreGetter(std::move(dbStoreGetter))
, LAGER_QT(serverUrl)(m_clientOnSecondaryRoot.serverUrl().xform(strToQt))
, LAGER_QT(userId)(m_clientOnSecondaryRoot.userId().xform(strToQt))
, LAGER_QT(token)(m_clientOnSecondaryRoot.token().xform(strToQt))
, LAGER_QT(deviceId)(m_clientOnSecondaryRoot.deviceId().xform(strToQt))
, LAGER_QT(specVersions)(m_clientOnSecondaryRoot.supportVersions())
{
m_watchable.afterAll(
[this](KazvTrigger e) {
Q_EMIT this->trigger(e);
});
m_watchable.after<LoginSuccessful>(
[this](LoginSuccessful e) {
Q_EMIT this->loginSuccessful(e);
});
m_watchable.after<LoginFailed>(
[this](LoginFailed e) {
Q_EMIT this->loginFailed(
QString::fromStdString(e.errorCode),
QString::fromStdString(e.error)
);
});
m_watchable.after<ReceivingRoomTimelineEvent>(
[this](ReceivingRoomTimelineEvent e) {
Q_EMIT this->receivedMessage(
QString::fromStdString(e.roomId),
QString::fromStdString(e.event.id())
);
});
}
MatrixSession::~MatrixSession() = default;
QString MatrixSession::mxcUriToHttp(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
}
QString MatrixSession::mxcUriToHttpAuthenticatedV1(QString mxcUri) const
{
return QString::fromStdString(m_clientOnSecondaryRoot.mxcUriToHttpV1(mxcUri.toStdString()));
}
MatrixDeviceList *MatrixSession::devicesOfUser(QString userId) const
{
return new MatrixDeviceList(m_clientOnSecondaryRoot.devicesOfUser(userId.toStdString()));
}
bool isIllFormatSpecVersion(const QString &version)
{
const bool isLegacy = version.startsWith(u"r"_s);
if (isLegacy && version.split(u'.').size() == 3) {
return false;
}
if (!isLegacy && version.split(u'.').size() == 2) {
return false;
}
return true;
}
// Return true if v1 is at least as new as v2, false if v1 is older than v2
// Return false if v1 or v2 is ill-format
bool compareSpecVersion(QString v1, QString v2)
{
// Check parameters format
if (isIllFormatSpecVersion(v1) || isIllFormatSpecVersion(v2)) {
return false;
}
const bool v1IsLegacy = v1.startsWith(u"r"_s);
const bool v2IsLegacy = v2.startsWith(u"r"_s);
if (v1IsLegacy != v2IsLegacy) {
return v2IsLegacy;
}
v1.remove(0, 1);
v2.remove(0, 1);
auto v1VersionNumbers = v1.split(u'.');
auto v2VersionNumbers = v2.split(u'.');
for (int i = 0; i < v1VersionNumbers.size(); i++) {
auto v1VerNum = v1VersionNumbers[i].toInt();
auto v2VerNum = v2VersionNumbers[i].toInt();
if (v1VerNum != v2VerNum) {
return v1VerNum > v2VerNum;
}
}
// v1 is equal to v2
return true;
}
// Return true if version in the range [minVer, maxVer]
// Return false if any parameter is ill-format
bool compareSpecVersionRange(const QString &version,
const QString &minVer, const QString &maxVer)
{
if (isIllFormatSpecVersion(version)
|| isIllFormatSpecVersion(minVer)
|| isIllFormatSpecVersion(maxVer)) {
return false;
}
if (compareSpecVersion(version, minVer) && compareSpecVersion(maxVer, version)) {
return true;
}
return false;
}
bool MatrixSession::checkSpecVersion(QString version) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&version](auto v) {
return compareSpecVersion(QString::fromStdString(v), version);
}) != specVersions().end();
}
bool MatrixSession::checkSpecVersionRange(QString minVer, QString maxVer) const
{
return std::find_if(specVersions().begin(), specVersions().end(),
[&minVer, maxVer](auto v) {
return compareSpecVersionRange(QString::fromStdString(v), minVer, maxVer);
}) != specVersions().end();
}
QStringList MatrixSession::directRoomIds(QString userId) const
{
auto content = m_clientOnSecondaryRoot.accountData().get()["m.direct"].content().get();
auto roomIds = QStringList{};
for (auto i : content[userId.toStdString()]) {
roomIds.push_back(QString::fromStdString(i.get<std::string>()));
}
return roomIds;
}
MatrixVerificationList *MatrixSession::verificationList() const
{
return new MatrixVerificationList(m_clientOnSecondaryRoot, m_verificationTrackerState);
}
std::string MatrixSession::validateHomeserverUrl(const QString &url)
{
if (url.isEmpty()) {
return std::string();
}
auto u = QUrl::fromUserInput(url);
if (!u.isValid()) {
return std::string();
}
if (u.scheme() == u"http"_s) {
qCInfo(kazvLog) << "url" << u << "is http. Force switching to https.";
u.setScheme(u"https"_s);
} else if (u.scheme() != u"https"_s) {
qCWarning(kazvLog) << "url" << u << "is not http/https.";
return std::string();
}
return u.toString().toStdString();
}
void MatrixSession::login(const QString &userId, const QString &password, const QString &homeserverUrl)
{
auto loginFunc = [userId, password](const Client &client, const std::string &serverUrl) {
client.passwordLogin(
serverUrl,
userId.toStdString(),
password.toStdString(),
clientName,
/* startSyncingOnSuccess = */ true
);
};
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
loginFunc(m_clientOnSecondaryRoot, validated);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString()) // autoDiscover() will dispatch GetVersionAction to get supported versions of the server
.then([
this,
client=m_clientOnSecondaryRoot.toEventLoop(),
userId,
password,
loginFunc
](auto res) {
if (!res.success()) {
// FIXME use real error codes and msgs when available in libkazv
Q_EMIT this->discoverFailed(u""_s, u""_s);
return res;
}
auto serverUrl = res.dataStr("homeserverUrl");
loginFunc(client, serverUrl);
return res;
});
}
}
void MatrixSession::discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl)
{
auto validated = validateHomeserverUrl(homeserverUrl);
if (!validated.empty()) {
getLoginFlows(homeserverUrl);
} else {
m_clientOnSecondaryRoot
.autoDiscover(userId.toStdString())
.then([
this
](const EffectStatus &res) {
if (!res.success()) {
Q_EMIT discoverFailed(
QString::fromStdString(res.dataStr("errorCode")),
QString::fromStdString(res.dataStr("error"))
);
return;
}
auto serverUrl = QString::fromStdString(res.dataStr("homeserverUrl"));
Q_EMIT discoverSuccessful(serverUrl);
getLoginFlows(serverUrl);
});
}
}
void MatrixSession::getLoginFlows(const QString &serverUrl)
{
lager::get<JobInterface &>(m_context).submit(
Api::GetLoginFlowsJob(serverUrl.toStdString()),
[this](Api::GetLoginFlowsResponse r) {
if (!r.success()) {
Q_EMIT getLoginFlowsFailed(
QString::fromStdString(r.errorCode()),
QString::fromStdString(r.errorMessage())
);
return;
}
auto v = std::move(r).jsonBody().get().at("flows").template get<QJsonValue>();
Q_EMIT getLoginFlowsSuccessful(v);
}
);
}
QUrl MatrixSession::ssoLoginStart(const QString &homeserverUrl)
{
if (m_ssoLoginProcess) {
m_ssoLoginProcess.reset();
}
m_ssoLoginProcess.reset(new SsoLoginProcess());
if (!m_ssoLoginProcess->startServer()) {
return QUrl();
}
auto link = m_ssoLoginProcess->getSsoLink(homeserverUrl);
connect(m_ssoLoginProcess.get(), &SsoLoginProcess::loginTokenAvailable,
this, [this, homeserverUrl](const QString &loginToken) {
Q_EMIT ssoLoginTokenAvailable();
qCInfo(kazvLog) << "Got SSO login token";
m_ssoLoginProcess.reset();
m_clientOnSecondaryRoot.mLoginTokenLogin(
homeserverUrl.toStdString(),
loginToken.toStdString(),
clientName,
/* startSyncingOnSuccess = */ true
);
});
return link;
}
void MatrixSession::logout()
{
m_clientOnSecondaryRoot.logout()
.then([&] (EffectStatus stat) {
if (stat.success()) {
Q_EMIT this->logoutSuccessful();
} else {
Q_EMIT this->logoutFailed(QString::fromStdString(stat.dataStr("errorCode")), QString::fromStdString(stat.dataStr("error")));
}
});
}
MatrixRoomList *MatrixSession::roomList() const
{
return new MatrixRoomList(m_clientOnSecondaryRoot);
}
static std::optional<std::string> optMaybe(QString s)
{
if (s.isEmpty()) {
return std::nullopt;
} else {
return s.toStdString();
}
}
MatrixPromise *MatrixSession::createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
Constants::CreateRoomPreset preset,
bool encrypted
)
{
immer::array<Event> initialState;
if (encrypted) {
initialState = {Event{json{
{"type", "m.room.encryption"},
{"state_key", ""},
{"content", {
{"algorithm", "m.megolm.v1.aes-sha2"},
}},
}}};
}
return new MatrixPromise(m_clientOnSecondaryRoot.createRoom(
isPrivate ? Kazv::RoomVisibility::Private : Kazv::RoomVisibility::Public,
optMaybe(name),
optMaybe(alias),
qStringListToStdF(invite),
isDirect,
allowFederate,
optMaybe(topic),
nlohmann::json(powerLevelContentOverride),
static_cast<Kazv::CreateRoomPreset>(preset),
initialState
));
}
MatrixPromise *MatrixSession::joinRoom(const QString &idOrAlias, const QStringList &servers)
{
return new MatrixPromise(m_clientOnSecondaryRoot.joinRoom(
idOrAlias.toStdString(),
qStringListToStdF(servers)
));
}
MatrixPromise *MatrixSession::setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDeviceTrustLevel(
userId.toStdString(),
deviceId.toStdString(),
qStringToTrustLevelFunc(trustLevel)
)
);
}
MatrixPromise *MatrixSession::getSelfProfile()
{
return new MatrixPromise(
m_clientOnSecondaryRoot.getProfile(userId().toStdString())
);
}
MatrixPromise *MatrixSession::setDisplayName(QString displayName)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setDisplayName(
displayName.isEmpty() ? std::nullopt : std::optional<std::string>(displayName.toStdString())
)
);
}
MatrixPromise *MatrixSession::setAvatarUrl(QString avatarUrl)
{
return new MatrixPromise(
m_clientOnSecondaryRoot.setAvatarUrl(
avatarUrl.isEmpty() ? std::nullopt : std::optional<std::string>(avatarUrl.toStdString())
)
);
}
bool MatrixSession::shouldNotify(MatrixEvent *event) const
{
// Do not notify own event
if (event->sender() == userId()) {
return false;
}
return m_notificationHandler.handleNotification(event->underlyingEvent()).shouldNotify;
}
bool MatrixSession::shouldPlaySound(MatrixEvent *event) const
{
return m_notificationHandler.handleNotification(event->underlyingEvent()).sound.has_value();
}
MatrixStickerPackList *MatrixSession::stickerPackList() const
{
return new MatrixStickerPackList(m_clientOnSecondaryRoot);
}
MatrixEvent *MatrixSession::stickerRoomsEvent() const
{
- return new MatrixEvent(m_clientOnSecondaryRoot.accountData()[imagePackRoomsEventType][lager::lenses::or_default]);
+ return new MatrixEvent(m_clientOnSecondaryRoot.accountData().map(getCanonicalImagePackRoomsEvent));
+}
+
+MatrixPromise *MatrixSession::updateStickerRooms(const QJsonObject &content)
+{
+ auto promise = m_context.createResolvedPromise({});
+ for (const auto &type : imagePackRoomsEventTypes) {
+ auto event = Event(json{
+ {"type", type},
+ {"content", content},
+ });
+ promise = promise.then([client=m_clientOnSecondaryRoot.toEventLoop(), event, ctx=m_context](const auto &stat) {
+ if (stat.success()) {
+ return client.setAccountData(event);
+ }
+ return ctx.createResolvedPromise(stat);
+ });
+ }
+ return new MatrixPromise(promise);
}
MatrixPromise *MatrixSession::updateStickerPack(MatrixStickerPackSource source)
{
if (source.source == MatrixStickerPackSource::AccountData) {
auto eventJson = std::move(source.event).raw().get();
eventJson["type"] = source.eventType;
return sendAccountDataImpl(Event(std::move(eventJson)));
} else if (source.source == MatrixStickerPackSource::RoomState) {
- auto eventJson = std::move(source.event).raw().get();
- eventJson["type"] = source.eventType;
- eventJson["state_key"] = source.stateKey;
- return new MatrixPromise(
- m_clientOnSecondaryRoot
- .room(source.roomId)
- .sendStateEvent(Event(std::move(eventJson))));
+ auto room = m_clientOnSecondaryRoot
+ .room(source.roomId);
+ auto promise = m_context.createResolvedPromise({});
+ for (const auto &type : roomStateEventTypes) {
+ auto eventJson = std::move(source.event).raw().get();
+ eventJson["type"] = type;
+ eventJson["state_key"] = source.stateKey;
+ promise = promise.then([room=room.toEventLoop(), eventJson, ctx=m_context](const auto &stat) {
+ if (stat.success()) {
+ return room.sendStateEvent(Event(eventJson));
+ }
+ return ctx.createResolvedPromise(stat);
+ });
+ }
+
+ return new MatrixPromise(promise);
} else {
return 0;
}
}
+MatrixPromise *MatrixSession::addRoomStickerPack(const QString &roomId, const QString &stateKey, const QString &displayName)
+{
+ auto stateKeyStd = stateKey.toStdString();
+ auto roomIdStd = roomId.toStdString();
+ auto room = m_clientOnSecondaryRoot.room(roomIdStd);
+ auto eventJson = getCanonicalImagePackForRoom(room.stateEvents().make().get(), stateKeyStd).originalJson().get();
+ if (!eventJson.contains("content")) {
+ eventJson["content"] = json::object();
+ }
+ if (!eventJson["content"].contains("pack")) {
+ eventJson["content"]["pack"] = json::object();
+ }
+ eventJson["content"]["pack"]["display_name"] = displayName.toStdString();
+ return updateStickerPack(MatrixStickerPackSource{
+ MatrixStickerPackSource::RoomState,
+ roomStateEventTypes[0],
+ Event(eventJson),
+ roomIdStd,
+ stateKeyStd,
+ });
+}
+
MatrixUserGivenAttrsMap *MatrixSession::userGivenNicknameMap() const
{
return new MatrixUserGivenAttrsMap(
userGivenNicknameMapFor(m_clientOnSecondaryRoot),
[client=m_clientOnSecondaryRoot](json content) {
return client.setAccountData(json{
{"type", USER_GIVEN_NICKNAME_EVENT_TYPES[0]},
{"content", std::move(content)},
});
}
);
}
MatrixPromise *MatrixSession::sendAccountData(const QString &type, const QJsonObject &content)
{
Event e = json{
{"type", type.toStdString()},
{"content", content},
};
return sendAccountDataImpl(std::move(e));
}
MatrixPromise *MatrixSession::sendAccountDataImpl(Event event)
{
return new MatrixPromise(m_clientOnSecondaryRoot.setAccountData(event));
}
MatrixPromise *MatrixSession::getSpecVersions()
{
return new MatrixPromise(m_clientOnSecondaryRoot.getVersions(LAGER_QT(serverUrl).get().toStdString()));
}
MatrixPromise *MatrixSession::addDirectRoom(const QString &userId, const QString &roomId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.addDirectRoom(userId.toStdString(), roomId.toStdString()));
}
MatrixPromise *MatrixSession::getRoomIdByAlias(const QString &roomAlias)
{
auto job = m_clientOnSecondaryRoot
.getRoomIdByAliasJob(roomAlias.toStdString());
return new MatrixPromise(
m_context.createPromise([ctx=m_context, job](auto resolve) {
lager::get<JobInterface &>(ctx).submit(job, [resolve](GetRoomIdByAliasResponse r) {
resolve(parseGetRoomIdByAliasResponse(r));
});
}));
}
inline constexpr std::size_t purgeEventsKeepNumber = 5;
MatrixPromise *MatrixSession::purgeEventsExceptRooms(const QStringList &roomIds)
{
return new MatrixPromise(
m_context.createResolvedPromise({})
.then([client=m_clientOnSecondaryRoot.toEventLoop(), roomIds=qStringListToStdF(roomIds)
]([[maybe_unused]] auto &&stat) {
auto map = intoImmer(immer::map<std::string, std::size_t>{},
zug::filter([&roomIds](const std::string &roomId) {
return std::find(roomIds.begin(), roomIds.end(), roomId) == roomIds.end();
})
| zug::map([](const std::string &roomId) {
return std::make_pair(roomId, purgeEventsKeepNumber);
}),
client.roomIds().make().get()
);
return client.purgeRoomEvents(map);
}).then([](const auto &stat) {
qCDebug(kazvLog) << "Purge room events stat:" << !!stat;
return stat;
})
);
}
MatrixPromise *MatrixSession::backfillRoomFromEvent(const QString &roomId, const QString &eventId)
{
return new MatrixPromise(
m_context.createPromise([
dbStoreGetter=m_dbStoreGetter,
client=m_clientOnSecondaryRoot.toEventLoop(),
roomId,
eventId
](auto resolve) {
dbStoreGetter().getEventsBefore(roomId, eventId).then([client, resolve, roomId](auto &&res) {
auto [timelineEvents, relatedEvents] = std::move(res);
auto loadedCount = timelineEvents[roomId.toStdString()].size();
client.loadEventsFromStorage(timelineEvents, relatedEvents)
.then([resolve, loadedCount](auto &&) {
resolve(EffectStatus(
!!loadedCount,
json{{"loadedCount", loadedCount}}
));
});
});
})
);
}
MatrixPromise *MatrixSession::loadEvent(const QString &roomId, const QString &eventId)
{
return new MatrixPromise(
m_context.createPromise([
dbStoreGetter=m_dbStoreGetter,
client=m_clientOnSecondaryRoot.toEventLoop(),
roomId,
eventId
](auto resolve) {
dbStoreGetter().getEventById(roomId, eventId).then([client, resolve, roomId](auto &&res) {
if (!res.has_value()) {
resolve(EffectStatus(/* succ = */ false));
return;
}
auto [event, _isInTimeline] = std::move(res).value();
// Disregard isInTimeline because when we load one single event,
// we do not want to add it to the timeline. If we add it,
// we lose info about the which events are before/after it,
// causing the remaining timeline to be gapped without a way
// to refill it.
immer::map<std::string, Kazv::EventList> relatedEvents = {
{roomId.toStdString(), {event}},
};
client.loadEventsFromStorage({}, relatedEvents)
.then([resolve](auto &&) {
resolve(EffectStatus(/* succ = */ true));
});
});
})
);
}
MatrixPromise *MatrixSession::importFromKeyBackupFile(QUrl fileUrl, QString password)
{
if (!fileUrl.isLocalFile()) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "Not a local file"}, {"errorCode", "NOT_LOCAL_FILE"}},
}));
}
auto filename = fileUrl.toLocalFile();
auto f = QFile(filename);
if (!f.open(QFile::ReadOnly)) {
return new MatrixPromise(m_context.createResolvedPromise({
/* succ = */ false,
json{{"error", "File open failed"}, {"errorCode", "FILE_OPEN_FAILED"}},
}));
}
auto ba = f.readAll();
auto content = std::string(ba.begin(), ba.end());
auto passwordStd = std::move(password).toStdString();
return new MatrixPromise(m_clientOnSecondaryRoot.importFromKeyBackupFile(
std::move(content), std::move(passwordStd)
));
}
MatrixPromise *MatrixSession::requestVerifyDevice(QString userId, QString deviceId)
{
return new MatrixPromise(m_clientOnSecondaryRoot.requestOutgoingToDeviceVerification(
std::move(userId).toStdString(),
std::move(deviceId).toStdString()
));
}
diff --git a/src/matrix-session.hpp b/src/matrix-session.hpp
index 8d55095..fe28523 100644
--- a/src/matrix-session.hpp
+++ b/src/matrix-session.hpp
@@ -1,311 +1,331 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "helper.hpp"
#include "meta-types.hpp"
#include "matrix-session-types.hpp"
#include "constants.hpp"
#include <client.hpp>
#include <notification-handler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <lager/extra/qt.hpp>
#include <QObject>
#include <QQmlEngine>
Q_MOC_INCLUDE("matrix-room-list.hpp")
Q_MOC_INCLUDE("matrix-device-list.hpp")
Q_MOC_INCLUDE("matrix-promise.hpp")
Q_MOC_INCLUDE("matrix-event.hpp")
Q_MOC_INCLUDE("matrix-sticker-pack-list.hpp")
Q_MOC_INCLUDE("matrix-user-given-attrs-map.hpp")
Q_MOC_INCLUDE("matrix-verification-list.hpp")
class MatrixRoomList;
class MatrixDeviceList;
class MatrixPromise;
class MatrixEvent;
class MatrixStickerPackList;
class MatrixUserGivenAttrsMap;
class MatrixVerificationList;
class SsoLoginProcess;
class DbStore;
/**
* Represent all operations that can be taken within a Matrix session.
*/
class MatrixSession : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
MatrixSessionContextT m_context;
Kazv::Client m_clientOnSecondaryRoot;
Kazv::LagerStoreEventEmitter::Watchable m_watchable;
Kazv::NotificationHandler m_notificationHandler;
lager::reader<Kazv::VerificationTrackerModel> m_verificationTrackerState;
UniquePtrDL<SsoLoginProcess> m_ssoLoginProcess;
std::function<DbStore &()> m_dbStoreGetter;
public:
MatrixSession(
MatrixSessionContextT context,
Kazv::Client client,
Kazv::LagerStoreEventEmitter::Watchable watchable,
lager::reader<Kazv::VerificationTrackerModel> verificationTrackerState,
std::function<DbStore &()> dbStoreGetter,
QObject *parent = nullptr
);
~MatrixSession() override;
static std::string validateHomeserverUrl(const QString &url);
LAGER_QT_READER(QString, serverUrl);
LAGER_QT_READER(QString, userId);
LAGER_QT_READER(QString, token);
LAGER_QT_READER(QString, deviceId);
LAGER_QT_READER(immer::array<std::string>, specVersions); // The versions of the Matrix Spec supported by the server.
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
Q_INVOKABLE QString mxcUriToHttpAuthenticatedV1(QString mxcUri) const;
Q_INVOKABLE MatrixDeviceList *devicesOfUser(QString userId) const;
// Return true if version is at least as new as the spec version of server
Q_INVOKABLE bool checkSpecVersion(QString version) const;
// Return true if the spec version of server in the range [minVer, maxVer]
Q_INVOKABLE bool checkSpecVersionRange(QString minVer, QString maxVer) const;
Q_INVOKABLE QStringList directRoomIds(QString userId) const;
Q_INVOKABLE MatrixVerificationList *verificationList() const;
Q_SIGNALS:
void trigger(Kazv::KazvTrigger e);
void loginSuccessful(Kazv::KazvTrigger e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void discoverSuccessful(QString serverUrl);
void getLoginFlowsFailed(QString errorCode, QString errorMsg);
void getLoginFlowsSuccessful(QJsonValue flows);
void ssoLoginTokenAvailable();
void logoutSuccessful();
void logoutFailed(QString errorCode, QString errorMsg);
void receivedMessage(QString roomId, QString eventId);
public Q_SLOTS:
void login(const QString &userId, const QString &password, const QString &homeserverUrl);
/**
* Auto-discover the server url and then get the login flows from the server.
*
* If homeserverUrl is not provided, try to get it from auto-discovery.
* If discovery is successful, or it is already provided, get the login flows
* by calling getLoginFlows.
*/
void discoverAndGetLoginFlows(const QString &userId, const QString &homeserverUrl);
void getLoginFlows(const QString &serverUrl);
/**
* Start SSO login flow.
*
* It will start an http server on a local port and pass it as
* the redirect url to the SSO login link.
*
* When the user completes the SSO login flow, we get the login token
* for us to login via the token flow.
*
* @return The SSO login link for the user to open.
*/
QUrl ssoLoginStart(const QString &homeserverUrl);
void logout();
/**
* Create a new room.
*
* @param isPrivate Whether the room is private.
* @param name The room's name.
* @param alias The alias of the room.
* @param invite List of matrix ids of users to invite.
* @param isDirect Whether it is a direct message room.
* @param allowFederate Whether to allow users on other servers to join.
* @param topic The topic of the room.
* @param powerLevelContentOverride The content to override m.room.power_levels event.
* @param preset The preset to create the room with.
* @param encrypted Whether to enable encryption for this room.
*/
MatrixPromise *createRoom(
bool isPrivate,
const QString &name,
const QString &alias,
const QStringList &invite,
bool isDirect,
bool allowFederate,
const QString &topic,
const QJsonValue &powerLevelContentOverride,
Constants::CreateRoomPreset preset,
bool encrypted
);
/**
* Join a room.
* @param idOrAlias The id or alias of the room to join.
* @param servers The servers to use when joining the room.
*/
MatrixPromise *joinRoom(
const QString &idOrAlias,
const QStringList &servers
);
/**
* Change the trust level of a device.
*
* @param userId The user id that owns the device.
* @param deviceId The device id to set the trust level.
* @param trustLevel The trust level.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDeviceTrustLevel(QString userId, QString deviceId, QString trustLevel);
/**
* Get the profile of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *getSelfProfile();
/**
* Set the display name of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setDisplayName(QString displayName);
/**
* Set the avatar url of the current user.
*
* @return A MatrixPromise representing the progress.
*/
MatrixPromise *setAvatarUrl(QString avatarUrl);
/**
* Check if an event should be notified.
*
* @param event The event to check.
* @return Whether `event` should be notified.
*/
bool shouldNotify(MatrixEvent *event) const;
/**
* Check if an event should be notified with sound.
*
* You should only call this method when `shouldNotify(event)`
* returns true.
*
* @param event The event to check.
* @return Whether `event` should be notified with sound.
*/
bool shouldPlaySound(MatrixEvent *event) const;
/**
* Get the sticker pack list for the current account.
*
* @return A list of sticker packs associated with the current account.
*/
MatrixStickerPackList *stickerPackList() const;
/**
* Get the sticker rooms account data event for the current account.
*
* @return A MatrixEvent representing the sticker rooms account data event.
*/
MatrixEvent *stickerRoomsEvent() const;
+ /**
+ * Update the sticker rooms for this account.
+ *
+ * @param content The content object for the sticker rooms account data event.
+ * @return A promise that resolves when the sticker rooms account data is updated,
+ * or when there is an error.
+ */
+ MatrixPromise *updateStickerRooms(const QJsonObject &content);
+
/**
* Update the sticker pack from source.
*
* @param source The source of the sticker pack to update.
* @return A promise that resolves when the sticker pack is updated,
* or when there is an error.
*/
MatrixPromise *updateStickerPack(MatrixStickerPackSource source);
+ /**
+ * Create a new sticker pack in a room.
+ *
+ * @param roomId The id of the room.
+ * @param stateKey The state key of the pack.
+ * @param displayName Thi display name of the pack.
+ * @return A promise that resolves when the sticker pack is added,
+ * or when there is an error.
+ */
+ MatrixPromise *addRoomStickerPack(const QString &roomId, const QString &stateKey, const QString &displayName);
+
MatrixUserGivenAttrsMap *userGivenNicknameMap() const;
MatrixPromise *sendAccountData(const QString &type, const QJsonObject &content);
/**
* Get all Matrix Spec versions supported by the server.
* Use MatrixSdk::supportSpecVersion() to check if a version is supported.
*/
MatrixPromise *getSpecVersions();
MatrixPromise *addDirectRoom(const QString &userId, const QString &roomId);
MatrixPromise *getRoomIdByAlias(const QString &roomAlias);
/**
* Purge events in all rooms except those specified in roomIds
*
* @param roomIds The ids for rooms to NOT be purged. This allows
* keeping the rooms that are already open intact.
* @return A MatrixPromise that resolves when the events are purged.
*/
MatrixPromise *purgeEventsExceptRooms(const QStringList &roomIds);
/**
* Backfill from the event storage from an existing event in a room.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to backfill from.
* @return A MatrixPromise that resolves when the events are backfilled.
* The Promise is considered successful if and only if at least one event
* is loaded.
*/
MatrixPromise *backfillRoomFromEvent(const QString &roomId, const QString &eventId);
/**
* Load one event from the store.
*
* @param roomId The id of the room to operate on.
* @param eventId The id of the event to load.
* @return A MatrixPromise that resolves when loading is finished.
* The Promise is considered successful if and only if the event is loaded.
*/
MatrixPromise *loadEvent(const QString &roomId, const QString &eventId);
/**
* Import from key backup file.
*
* @param fileUrl The url of the key backup file. Will be passed to
* QUrl::toLocalFile(), and must be a local file.
* @param password The password to decrypt the key backup file.
* @return A MatrixPromise that resolves when the keys are imported.
* If successful, `data["imported"]` contains the number of imported keys.
* If unsuccessful, `data` contains a standard error structure.
*/
MatrixPromise *importFromKeyBackupFile(QUrl fileUrl, QString password);
/**
* Send an outbound device verification request.
*
* @param userId The user id of the device to verify.
* @param deviceId The device id of the device to verify.
*/
MatrixPromise *requestVerifyDevice(QString userId, QString deviceId);
private:
MatrixPromise *sendAccountDataImpl(Kazv::Event event);
};
diff --git a/src/matrix-sticker-pack-list-p.hpp b/src/matrix-sticker-pack-list-p.hpp
index 0613cf1..5f9dd77 100644
--- a/src/matrix-sticker-pack-list-p.hpp
+++ b/src/matrix-sticker-pack-list-p.hpp
@@ -1,13 +1,29 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
+#include <event.hpp>
+#include <clientutil.hpp>
+#include <immer/array.hpp>
+#include <immer/map.hpp>
#include <string>
inline const std::string accountDataEventType = "im.ponies.user_emotes";
-inline const std::string roomStateEventType = "im.ponies.room_emotes";
-inline const std::string imagePackRoomsEventType = "im.ponies.emote_rooms";
+/// types for room state event image packs, primary then secondary
+inline const immer::array<std::string> roomStateEventTypes = {
+ "m.room.image_pack",
+ "im.ponies.room_emotes",
+};
+/// types for image pack rooms in account data, primary then secondary
+inline const immer::array<std::string> imagePackRoomsEventTypes = {
+ "m.image_pack.rooms",
+ "im.ponies.emote_rooms",
+};
+
+Kazv::Event getCanonicalImagePackRoomsEvent(immer::map<std::string, Kazv::Event> accountData);
+
+Kazv::Event getCanonicalImagePackForRoom(immer::map<Kazv::KeyOfState, Kazv::Event> state, const std::string &stateKey);
diff --git a/src/matrix-sticker-pack-list.cpp b/src/matrix-sticker-pack-list.cpp
index 869dcfa..4aa2551 100644
--- a/src/matrix-sticker-pack-list.cpp
+++ b/src/matrix-sticker-pack-list.cpp
@@ -1,171 +1,214 @@
/*
* 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 <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <lager/lenses/optional.hpp>
#include <lager/lenses/at.hpp>
#include "matrix-sticker-pack.hpp"
#include "matrix-sticker-pack-list.hpp"
#include "matrix-sticker-pack-list-p.hpp"
using namespace Kazv;
using namespace Qt::Literals::StringLiterals;
using ImagePackRoomMap = immer::map<std::string/* room id */, immer::map<std::string/* */, json>>;
+Event getCanonicalImagePackRoomsEvent(immer::map<std::string, Event> accountData)
+{
+ for (const auto &type: imagePackRoomsEventTypes) {
+ if (accountData.count(type)) {
+ return accountData[type];
+ }
+ }
+ return Event();
+}
+
+Event getCanonicalImagePackForRoom(immer::map<Kazv::KeyOfState, Kazv::Event> state, const std::string &stateKey)
+{
+ for (const auto &type: roomStateEventTypes) {
+ auto ks = KeyOfState{type, stateKey};
+ if (state.count(ks)) {
+ return state[ks];
+ }
+ }
+ return Event();
+}
+
+Event getCanonicalRoomPack(immer::map<KeyOfState, Event> evs, const std::string &stateKey)
+{
+ for (const auto &type: roomStateEventTypes) {
+ auto ks = KeyOfState{type, stateKey};
+ if (evs.count(ks)) {
+ return evs[ks];
+ }
+ }
+ return Event();
+}
+
lager::reader<immer::flex_vector<MatrixStickerPackSource>> getEventsFromClient(Client client)
{
lager::reader<Event> userEmotes = client.accountData()[accountDataEventType][lager::lenses::or_default];
return lager::with(
userEmotes,
- client.accountData()[imagePackRoomsEventType][lager::lenses::or_default]
+ client.accountData()
+ .map(getCanonicalImagePackRoomsEvent)
// 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}],
+ roomStateEventTypes[0],
+ getCanonicalRoomPack(rooms[roomId].stateEvents, stateKey),
roomId,
stateKey,
});
}
}
return res.persistent();
}).make();
}
lager::reader<immer::flex_vector<MatrixStickerPackSource>> getEventsFromRoom(Room room)
{
return lager::with(room.roomId(), room.stateEvents())
.map([](const auto &roomId, const auto &state) {
return intoImmer(
immer::flex_vector<MatrixStickerPackSource>{},
- zug::filter([](const auto &pair) {
- return pair.first.type == roomStateEventType;
+ zug::filter([state](const auto &pair) {
+ auto it = std::find(roomStateEventTypes.begin(), roomStateEventTypes.end(), pair.first.type);
+ if (it == roomStateEventTypes.end()) {
+ return false;
+ }
+ // This is one of image pack types. Now check if it is the stable type
+ if (it == roomStateEventTypes.begin()) {
+ return true;
+ }
+ // Here, it is not a stable type.
+ // Only return true if there is no corresponding stable type for the same state key
+ return !state.count(KeyOfState{roomStateEventTypes[0], pair.first.stateKey});
})
| zug::map([roomId](const auto &pair) {
return MatrixStickerPackSource{
MatrixStickerPackSource::RoomState,
- roomStateEventType,
+ pair.first.type,
pair.second,
roomId,
pair.first.stateKey,
};
}),
state
);
});
}
static QString getPackDisplayName(const MatrixStickerPackSource &source)
{
auto content = source.event.content().get();
if (content.contains("/pack/display_name"_json_pointer)
&& content["/pack/display_name"_json_pointer].is_string()) {
return QString::fromStdString(
content["/pack/display_name"_json_pointer]
.template get<std::string>());
}
return u""_s;
}
static QJsonValue sourceToPacks(const immer::flex_vector<MatrixStickerPackSource> &sources)
{
QJsonArray res;
for (const auto &source : sources) {
QJsonObject obj{
{u"source"_s, source.source},
{u"isAccountData"_s, source.source == MatrixStickerPackSource::AccountData},
{u"isState"_s, source.source == MatrixStickerPackSource::RoomState},
{u"eventType"_s, QString::fromStdString(source.eventType)},
{u"roomId"_s, QString::fromStdString(source.roomId)},
{u"stateKey"_s, QString::fromStdString(source.stateKey)},
{u"packName"_s, getPackDisplayName(source)},
};
res.append(obj);
}
return res;
}
static auto defaultSource = MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
accountDataEventType,
Event(),
"",
"",
};
MatrixStickerPackList::MatrixStickerPackList(Client client, QObject *parent)
: KazvAbstractListModel(parent)
, m_events(getEventsFromClient(client))
, LAGER_QT(packs)(m_events.map(sourceToPacks))
{
initCountCursor(m_events.map([](const auto &events) {
return static_cast<int>(events.size());
}));
}
MatrixStickerPackList::MatrixStickerPackList(Room room, QObject *parent)
: KazvAbstractListModel(parent)
, m_events(getEventsFromRoom(room))
, LAGER_QT(packs)(m_events.map(sourceToPacks))
{
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 defaultSource;
}
}));
}
MatrixStickerPack *MatrixStickerPackList::packFor(const QJsonValue &desc) const
{
auto source = MatrixStickerPackSource::Source(desc.toObject()[u"source"_s].toInt());
auto eventType = desc.toObject()[u"eventType"_s].toString().toStdString();
auto roomId = desc.toObject()[u"roomId"_s].toString().toStdString();
auto stateKey = desc.toObject()[u"stateKey"_s].toString().toStdString();
return new MatrixStickerPack(m_events.map([=](const auto &events) {
auto it = std::find_if(events.begin(), events.end(), [=](const auto &s) {
return s.source == source
&& s.eventType == eventType
&& s.roomId == roomId
&& s.stateKey == stateKey;
});
if (it == events.end()) {
return defaultSource;
} else {
return *it;
}
}));
}
diff --git a/src/tests/matrix-sticker-pack-test.cpp b/src/tests/matrix-sticker-pack-test.cpp
index 6b5a94f..ba51453 100644
--- a/src/tests/matrix-sticker-pack-test.cpp
+++ b/src/tests/matrix-sticker-pack-test.cpp
@@ -1,369 +1,450 @@
/*
* 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 <QSet>
#include <QJsonObject>
#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"
#include "matrix-room-list.hpp"
#include "matrix-room.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 testStickerPackListStable();
void testStickerPackListInRoom();
+ void testStickerPackListInRoomStable();
void testAddToPack();
void testRemoveFromPack();
};
// 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)
+static const auto imagePackRoomsEventStable = 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, bool stable = false)
{
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;
+ if (stable) {
+ j["type"] = "m.room.image_pack";
+ }
j["state_key"] = stateKey;
j["content"]["pack"]["display_name"] = name;
return Event(j);
}
template<class Range>
static bool hasPack(const Range &packs, const QString &name)
{
return std::any_of(packs.begin(), packs.end(), [&name](const auto &pack) {
return pack->packName() == name;
});
}
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, 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")})
))
);
SessionSetup s(SdkModel{model});
auto stickerPackList = toUniquePtr(s.session.stickerPackList());
QCOMPARE(stickerPackList->rowCount(QModelIndex()), 4);
QCOMPARE(stickerPackList->count(), 4);
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)),
};
QVERIFY(hasPack(packs, u"Awesome Pack"_s));
QVERIFY(hasPack(packs, u"Pack 1"_s));
QVERIFY(hasPack(packs, u"Pack 2"_s));
QVERIFY(hasPack(packs, u"Pack 3"_s));
auto expectedPacks = QSet<QJsonValue>{
QJsonObject{
{u"source"_s, MatrixStickerPackSource::AccountData},
{u"isAccountData"_s, true},
{u"isState"_s, false},
{u"eventType"_s, u"im.ponies.user_emotes"_s},
{u"roomId"_s, u""_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Awesome Pack"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
- {u"eventType"_s, u"im.ponies.room_emotes"_s},
+ {u"eventType"_s, u"m.room.image_pack"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 1"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
- {u"eventType"_s, u"im.ponies.room_emotes"_s},
+ {u"eventType"_s, u"m.room.image_pack"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u"de.sorunome.mx-puppet-bridge.discord"_s},
{u"packName"_s, u"Pack 2"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
- {u"eventType"_s, u"im.ponies.room_emotes"_s},
+ {u"eventType"_s, u"m.room.image_pack"_s},
{u"roomId"_s, u"!someotherroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 3"_s},
},
};
auto packsArr = stickerPackList->packs().toArray();
QCOMPARE(QSet<QJsonValue>(packsArr.begin(), packsArr.end()), expectedPacks);
auto p1 = toUniquePtr(stickerPackList->packFor(QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
- {u"eventType"_s, u"im.ponies.room_emotes"_s},
+ {u"eventType"_s, u"m.room.image_pack"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
}));
QCOMPARE(p1->packName(), u"Pack 1"_s);
auto p2 = toUniquePtr(stickerPackList->packFor(QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
- {u"eventType"_s, u"im.ponies.room_emotes"_s},
+ {u"eventType"_s, u"m.room.image_pack"_s},
{u"roomId"_s, u"!someotherroom:example.org"_s},
{u"stateKey"_s, u"somestatekey"_s},
}));
QVERIFY(p2->isAccountData());
}
+void MatrixStickerPackTest::testStickerPackListStable()
+{
+ auto model = makeClient(
+ withAccountData({stickerPackEvent, imagePackRoomsEvent, imagePackRoomsEventStable})
+ | withRoom(makeRoom(
+ withRoomId("!someroom:example.org")
+ | withRoomState({
+ getRoomStickersEvent("", "Pack 1"),
+ getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
+ getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2 stable", /* stable = */ true),
+ })
+ ))
+ | withRoom(makeRoom(
+ withRoomId("!someotherroom:example.org")
+ | withRoomState({getRoomStickersEvent("", "Pack 3")})
+ ))
+ );
+ SessionSetup s(SdkModel{model});
+ auto stickerPackList = toUniquePtr(s.session.stickerPackList());
+
+ // stable types takes over unstable types
+ QCOMPARE(stickerPackList->rowCount(QModelIndex()), 3);
+ QCOMPARE(stickerPackList->count(), 3);
+
+ auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 3>{
+ toUniquePtr(stickerPackList->at(0)),
+ toUniquePtr(stickerPackList->at(1)),
+ toUniquePtr(stickerPackList->at(2)),
+ };
+
+ QVERIFY(hasPack(packs, u"Pack 2 stable"_s));
+ QVERIFY(!hasPack(packs, u"Pack 2"_s));
+}
+
void MatrixStickerPackTest::testStickerPackListInRoom()
{
auto model = makeClient(
withRoom(makeRoom(
withRoomId("!someroom:example.org")
| withRoomState({
getRoomStickersEvent("", "Pack 1"),
getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
})
))
);
SessionSetup s(SdkModel{model});
auto roomList = toUniquePtr(s.session.roomList());
auto room = toUniquePtr(roomList->room(u"!someroom:example.org"_s));
auto stickerPackList = toUniquePtr(room->stickerPackList());
QCOMPARE(stickerPackList->count(), 2);
auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 2>{
toUniquePtr(stickerPackList->at(0)),
toUniquePtr(stickerPackList->at(1)),
};
QVERIFY(hasPack(packs, u"Pack 1"_s));
QVERIFY(hasPack(packs, u"Pack 2"_s));
auto expectedPacks = QSet<QJsonValue>{
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u""_s},
{u"packName"_s, u"Pack 1"_s},
},
QJsonObject{
{u"source"_s, MatrixStickerPackSource::RoomState},
{u"isAccountData"_s, false},
{u"isState"_s, true},
{u"eventType"_s, u"im.ponies.room_emotes"_s},
{u"roomId"_s, u"!someroom:example.org"_s},
{u"stateKey"_s, u"de.sorunome.mx-puppet-bridge.discord"_s},
{u"packName"_s, u"Pack 2"_s},
},
};
auto packsArr = stickerPackList->packs().toArray();
QCOMPARE(QSet<QJsonValue>(packsArr.begin(), packsArr.end()), expectedPacks);
}
+void MatrixStickerPackTest::testStickerPackListInRoomStable()
+{
+ auto model = makeClient(
+ withRoom(makeRoom(
+ withRoomId("!someroom:example.org")
+ | withRoomState({
+ getRoomStickersEvent("", "Pack 1"),
+ getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2"),
+ getRoomStickersEvent("de.sorunome.mx-puppet-bridge.discord", "Pack 2 stable", /* stable = */ true),
+ })
+ ))
+ );
+ SessionSetup s(SdkModel{model});
+ auto roomList = toUniquePtr(s.session.roomList());
+ auto room = toUniquePtr(roomList->room(u"!someroom:example.org"_s));
+ auto stickerPackList = toUniquePtr(room->stickerPackList());
+ QCOMPARE(stickerPackList->count(), 2);
+
+ auto packs = std::array<std::unique_ptr<MatrixStickerPack>, 2>{
+ toUniquePtr(stickerPackList->at(0)),
+ toUniquePtr(stickerPackList->at(1)),
+ };
+
+ QVERIFY(hasPack(packs, u"Pack 1"_s));
+ QVERIFY(hasPack(packs, u"Pack 2 stable"_s));
+ QVERIFY(!hasPack(packs, u"Pack 2"_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);
}
void MatrixStickerPackTest::testRemoveFromPack()
{
auto sourceCursor = lager::make_state(MatrixStickerPackSource{
MatrixStickerPackSource::AccountData,
"im.ponies.user_emotes",
stickerPackEvent,
"",
"",
}, lager::automatic_tag{});
auto stickerPack = toUniquePtr(new MatrixStickerPack(sourceCursor));
auto newSource = stickerPack->removeSticker(u"myemote"_s).template value<MatrixStickerPackSource>();
auto expected = sourceCursor.get().event.content().get();
expected["images"].erase("myemote");
QVERIFY(newSource.event.content().get() == expected);
QVERIFY(!newSource.event.content().get()["images"].contains("myemote"));
// Removing a non-existent sticker should leave the pack unchanged.
auto unchangedSource = stickerPack->removeSticker(u"doesnotexist"_s).template value<MatrixStickerPackSource>();
QVERIFY(unchangedSource.event.content().get() == sourceCursor.get().event.content().get());
}
QTEST_MAIN(MatrixStickerPackTest)
#include "matrix-sticker-pack-test.moc"
diff --git a/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml b/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
index d908ffd..2e8e6cf 100644
--- a/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
+++ b/src/tests/quick-tests/test-helpers/MatrixSessionMock.qml
@@ -1,84 +1,86 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023-2026 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
QtObject {
property var userId: ''
property var serverUrl: 'https://example.com'
property var token: 'token'
property var updateStickerPack: mockHelper.promise()
property var createRoom: mockHelper.promise([
'isPrivate',
'name',
'alias',
'invites',
'isDirect',
'allowFederate',
'topic',
'powerLevelContentOverride',
'preset',
'encrypted',
])
property var joinRoom: mockHelper.promise()
property var sendAccountData: mockHelper.promise()
+ property var updateStickerRooms: mockHelper.promise()
+ property var addRoomStickerPack: mockHelper.promise()
property var login: mockHelper.noop()
property var ssoLoginStart: mockHelper.func((serverUrl) => serverUrl + '/_matrix/client/v3/login/sso/redirect?redirectUrl=http://127.0.0.1:7456/sso-redirect/random')
property var discoverAndGetLoginFlows: mockHelper.noop()
property var addDirectRoom: mockHelper.promise([
'userId',
'roomId'
])
property var getRoomIdByAlias: mockHelper.promise([
'roomAlias'
])
property var importFromKeyBackupFile: mockHelper.promise([
'fileUrl',
'password',
])
property var requestVerifyDevice: mockHelper.promise([
'userId',
'deviceId',
])
property var specVersions: ["v1.11"]
signal discoverFailed(string errorCode, string errorMsg)
signal discoverSuccessful(string serverUrl)
signal getLoginFlowsFailed(string errorCode, string errorMsg)
signal getLoginFlowsSuccessful(var flows)
signal loginFailed(string errorCode, string errorMsg)
function mxcUriToHttp (uri) {
console.log('mxcUriToHttp');
return uri || '';
}
function mxcUriToHttpAuthenticatedV1 (uri) {
console.log('mxcUriToHttpAuthenticatedV1');
return uri || '';
}
function devicesOfUser (userId) {
return [];
}
function checkSpecVersion (ver) {
return !!specVersions.includes(ver);
}
function directRoomIds(userId) {
const directRoomMap = {
"@someonejoined:example.org": ["!joinedroomid:example.org"],
"@someoneinvited:example.org": ["!invitedroomid:example.org"],
"@someoneleft:example.org": ["!leftroomid:example.org"]
};
if (!directRoomMap[userId]) {
return [];
}
return directRoomMap[userId];
}
}
diff --git a/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml b/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
index db08303..d360b80 100644
--- a/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
+++ b/src/tests/quick-tests/tst_RoomStickerPackItemDelegate.qml
@@ -1,87 +1,87 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtTest
import moe.kazv.mxc.kazv as MK
import org.kde.kirigami as Kirigami
import '../../contents/ui/room-settings' as KazvRS
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
KazvRS.RoomStickerPackItemDelegate {
id: packItem
}
TestCase {
id: roomStickerPackItemDelegateTest
name: 'RoomStickerPackItemDelegateTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
packItem.stickerPack = {
packName: 'some pack',
stateKey: 'some-pack',
roomId: '!foo:example.com',
isRoomState: true,
};
packItem.stickerRoomsEvent = {
content: {
},
};
}
function test_usePack() {
const usePackAction = findChild(packItem, 'usePackAction');
verify(!usePackAction.checked);
usePackAction.trigger();
- tryVerify(() => matrixSession.sendAccountData.calledTimes() === 1);
+ tryVerify(() => matrixSession.updateStickerRooms.calledTimes() === 1);
verify(JsHelpers.deepEqual(
- matrixSession.sendAccountData.lastArgs()[1],
+ matrixSession.updateStickerRooms.lastArgs()[0],
{
rooms: {
'!foo:example.com': {
'some-pack': {},
},
},
},
));
}
function test_disusePack() {
packItem.stickerRoomsEvent = {
content: {
rooms: {
'!foo:example.com': {
'some-pack': {},
},
},
},
};
const usePackAction = findChild(packItem, 'usePackAction');
verify(usePackAction.checked);
usePackAction.trigger();
- tryVerify(() => matrixSession.sendAccountData.calledTimes() === 1);
+ tryVerify(() => matrixSession.updateStickerRooms.calledTimes() === 1);
verify(JsHelpers.deepEqual(
- matrixSession.sendAccountData.lastArgs()[1],
+ matrixSession.updateStickerRooms.lastArgs()[0],
{
rooms: {
'!foo:example.com': {
},
},
},
));
}
}
}
diff --git a/src/tests/quick-tests/tst_RoomStickerPacksPage.qml b/src/tests/quick-tests/tst_RoomStickerPacksPage.qml
index 583eb95..2016043 100644
--- a/src/tests/quick-tests/tst_RoomStickerPacksPage.qml
+++ b/src/tests/quick-tests/tst_RoomStickerPacksPage.qml
@@ -1,118 +1,94 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtTest
import moe.kazv.mxc.kazv as MK
import org.kde.kirigami as Kirigami
import '../../contents/ui/room-settings' as KazvRS
import '../../js/matrix-helpers.js' as MatrixHelpers
import 'test-helpers.js' as JsHelpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
KazvRS.RoomStickerPacksPage {
id: page
anchors.fill: parent
room: QtObject {
function state(type, stateKey) {
return {
type,
stateKey,
content: stateKey === 'foo' ? {
images: {
'mew': {},
},
} : stateKey === 'bar' ? {
pack: {
'some': 'others',
},
} : {},
};
}
property var sendStateEvent: mockHelper.promise()
}
}
TestCase {
id: roomStickerPacksPageTest
name: 'RoomStickerPacksPageTest'
when: windowShown
function init() {
item.mockHelper.clearAll();
}
function test_addPack_data() {
return [
{
succ: true,
stateKey: '',
- packContent: {
- pack: {
- display_name: 'some name',
- },
- },
},
{
succ: false,
stateKey: '',
- packContent: {
- pack: {
- display_name: 'some name',
- },
- },
},
{
succ: true,
stateKey: 'foo',
- packContent: {
- images: {
- 'mew': {},
- },
- pack: {
- display_name: 'some name',
- },
- },
},
{
succ: true,
stateKey: 'bar',
- packContent: {
- pack: {
- 'some': 'others',
- display_name: 'some name',
- },
- },
},
];
}
function test_addPack(data) {
findChild(page, 'addStickerPackAction').trigger();
tryVerify(() => findChild(page, 'stickerPackName').visible);
findChild(page, 'stickerPackName').text = 'some name';
findChild(page, 'stickerPackStateKey').text = data.stateKey;
page.addStickerPackPopup.accept();
- tryVerify(() => page.room.sendStateEvent.calledTimes() === 1);
+ tryVerify(() => matrixSession.addRoomStickerPack.calledTimes() === 1);
verify(JsHelpers.deepEqual(
- page.room.sendStateEvent.lastArgs(),
- [{
- type: MatrixHelpers.roomImagePackEventType,
- state_key: data.stateKey,
- content: data.packContent,
- }],
+ matrixSession.addRoomStickerPack.lastArgs(),
+ [
+ page.room.roomId,
+ data.stateKey,
+ 'some name',
+ ],
));
- page.room.sendStateEvent.lastRetVal().resolve(data.succ, {});
+ matrixSession.addRoomStickerPack.lastRetVal().resolve(data.succ, {});
compare(item.showPassiveNotification.calledTimes(), 1);
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 1:39 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784984
Default Alt Text
(78 KB)

Event Timeline