Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85804285
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
78 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment