Add menu to enable or disable stickers globally

This commit is contained in:
Nicolas Werner 2021-07-21 13:37:57 +02:00
parent 0c798554b5
commit eafbab6ae1
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
16 changed files with 674 additions and 17 deletions

View file

@ -355,6 +355,8 @@ set(SRC_FILES
src/RegisterPage.cpp
src/SSOHandler.cpp
src/CombinedImagePackModel.cpp
src/SingleImagePackModel.cpp
src/ImagePackListModel.cpp
src/TrayIcon.cpp
src/UserSettingsPage.cpp
src/UsersModel.cpp
@ -559,6 +561,8 @@ qt5_wrap_cpp(MOC_HEADERS
src/RegisterPage.h
src/SSOHandler.h
src/CombinedImagePackModel.h
src/SingleImagePackModel.h
src/ImagePackListModel.h
src/TrayIcon.h
src/UserSettingsPage.h
src/UsersModel.h

View file

@ -249,6 +249,17 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight
}
MatrixText {
text: qsTr("Sticker & Emote Settings")
}
Button {
text: qsTr("Change")
ToolTip.text: qsTr("Change what packs are enabled, remove packs or create new ones")
onClicked: TimelineManager.openImagePackSettings(roomSettings.roomId)
Layout.alignment: Qt.AlignRight
}
Item {
// for adding extra space between sections
Layout.fillWidth: true

View file

@ -4,6 +4,7 @@
import "./delegates"
import "./device-verification"
import "./dialogs"
import "./emoji"
import "./voip"
import Qt.labs.platform 1.1 as Platform
@ -87,6 +88,14 @@ Page {
}
Component {
id: packSettingsComponent
ImagePackSettingsDialog {
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: {
@ -120,6 +129,12 @@ Page {
});
userProfile.show();
}
onShowImagePackSettings: {
var packSet = packSettingsComponent.createObject(timelineRoot, {
"packlist": packlist
});
packSet.show();
}
}
Connections {

View file

@ -0,0 +1,309 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import ".."
import "../components"
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import im.nheko 1.0
ApplicationWindow {
id: win
property ImagePackListModel packlist
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
property SingleImagePackModel currentPack: packlist.packAt(currentPackIndex)
property int currentPackIndex: 0
readonly property int stickerDim: 128
readonly property int stickerDimPad: 128 + Nheko.paddingSmall
title: qsTr("Image pack settings")
x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
height: 400
width: 600
palette: Nheko.colors
color: Nheko.colors.base
modality: Qt.NonModal
flags: Qt.Dialog
AdaptiveLayout {
id: adaptiveView
anchors.fill: parent
singlePageMode: false
pageIndex: 0
AdaptiveLayoutElement {
id: packlistC
visible: Settings.groupView
minimumWidth: 200
collapsedWidth: 200
preferredWidth: 300
maximumWidth: 300
ListView {
model: packlist
clip: true
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
delegate: Rectangle {
id: packItem
property color background: Nheko.colors.window
property color importantText: Nheko.colors.text
property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
required property string displayName
required property string avatarUrl
required property bool fromAccountData
required property bool fromCurrentRoom
required property int index
color: background
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width
state: "normal"
states: [
State {
name: "highlight"
when: hovered.hovered && !(index == currentPackIndex)
PropertyChanges {
target: packItem
background: Nheko.colors.dark
importantText: Nheko.colors.brightText
unimportantText: Nheko.colors.brightText
bubbleBackground: Nheko.colors.highlight
bubbleText: Nheko.colors.highlightedText
}
},
State {
name: "selected"
when: index == currentPackIndex
PropertyChanges {
target: packItem
background: Nheko.colors.highlight
importantText: Nheko.colors.highlightedText
unimportantText: Nheko.colors.highlightedText
bubbleBackground: Nheko.colors.highlightedText
bubbleText: Nheko.colors.highlight
}
}
]
TapHandler {
margin: -Nheko.paddingSmall
onSingleTapped: currentPackIndex = index
}
HoverHandler {
id: hovered
}
RowLayout {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
Avatar {
// In the future we could show an online indicator by setting the userid for the avatar
//userid: Nheko.currentUser.userid
id: avatar
enabled: false
Layout.alignment: Qt.AlignVCenter
height: avatarSize
width: avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: packItem.displayName
}
ColumnLayout {
id: textContent
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: 100
width: parent.width - avatar.width
Layout.preferredWidth: parent.width - avatar.width
spacing: Nheko.paddingSmall
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
Layout.alignment: Qt.AlignBottom
color: packItem.importantText
elideWidth: textContent.width - Nheko.paddingMedium
fullText: displayName
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ElidedLabel {
color: packItem.unimportantText
font.pixelSize: fontMetrics.font.pixelSize * 0.9
elideWidth: textContent.width - Nheko.paddingSmall
fullText: {
if (fromAccountData)
return qsTr("Private pack");
else if (fromCurrentRoom)
return qsTr("Pack from this room");
else
return qsTr("Globally enabled pack");
}
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
}
}
}
}
}
}
AdaptiveLayoutElement {
id: packinfoC
Rectangle {
color: Nheko.colors.window
ColumnLayout {
id: packinfo
property string packName: currentPack ? currentPack.packname : ""
property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
anchors.fill: parent
anchors.margins: Nheko.paddingLarge
spacing: Nheko.paddingLarge
Avatar {
url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
displayName: packinfo.packName
height: 100
width: 100
Layout.alignment: Qt.AlignHCenter
enabled: false
}
MatrixText {
text: packinfo.packName
font.pixelSize: 24
Layout.alignment: Qt.AlignHCenter
}
GridLayout {
Layout.alignment: Qt.AlignHCenter
visible: currentPack && currentPack.roomid != ""
columns: 2
rowSpacing: Nheko.paddingMedium
MatrixText {
text: qsTr("Enable globally")
}
ToggleButton {
ToolTip.text: qsTr("Enables this pack to be used in all rooms")
checked: currentPack ? currentPack.isGloballyEnabled : false
onClicked: currentPack.isGloballyEnabled = !currentPack.isGloballyEnabled
Layout.alignment: Qt.AlignRight
}
}
GridView {
Layout.fillHeight: true
Layout.fillWidth: true
model: currentPack
cellWidth: stickerDimPad
cellHeight: stickerDimPad
boundsBehavior: Flickable.StopAtBounds
clip: true
currentIndex: -1 // prevent sorting from stealing focus
cacheBuffer: 500
ScrollHelper {
flickable: parent
anchors.fill: parent
enabled: !Settings.mobileMode
}
// Individual emoji
delegate: AbstractButton {
width: stickerDim
height: stickerDim
hoverEnabled: true
ToolTip.text: ":" + model.shortcode + ": - " + model.body
ToolTip.visible: hovered
contentItem: Image {
height: stickerDim
width: stickerDim
source: model.url.replace("mxc://", "image://MxcImage/")
fillMode: Image.PreserveAspectFit
}
background: Rectangle {
anchors.fill: parent
color: hovered ? Nheko.colors.highlight : 'transparent'
radius: 5
}
}
}
}
}
}
}
footer: DialogButtonBox {
id: buttons
Button {
text: qsTr("Close")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
onClicked: win.close()
}
}
}

View file

@ -160,6 +160,7 @@
<file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/Success.qml</file>
<file>qml/dialogs/InputDialog.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
<file>qml/ui/Ripple.qml</file>
<file>qml/ui/Spinner.qml</file>
<file>qml/ui/animations/BlinkAnimation.qml</file>

View file

@ -3383,26 +3383,30 @@ Cache::getChildRoomIds(const std::string &room_id)
}
std::vector<ImagePackInfo>
Cache::getImagePacks(const std::string &room_id, bool stickers)
Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
{
auto txn = ro_txn(env_);
std::vector<ImagePackInfo> infos;
auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack) {
if (!pack.pack || (stickers ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
auto addPack = [&infos, stickers](const mtx::events::msc2545::ImagePack &pack,
const std::string &source_room,
const std::string &state_key) {
if (!pack.pack || !stickers.has_value() ||
(stickers.value() ? pack.pack->is_sticker() : pack.pack->is_emoji())) {
ImagePackInfo info;
if (pack.pack)
info.packname = pack.pack->display_name;
info.source_room = source_room;
info.state_key = state_key;
info.pack.pack = pack.pack;
for (const auto &img : pack.images) {
if (img.second.overrides_usage() &&
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
continue;
info.images.insert(img);
info.pack.images.insert(img);
}
if (!info.images.empty())
if (!info.pack.images.empty())
infos.push_back(std::move(info));
}
};
@ -3414,7 +3418,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePack>>(
&*accountpack);
if (tmp)
addPack(tmp->content);
addPack(tmp->content, "", "");
}
// packs from rooms, that were enabled globally
@ -3433,7 +3437,7 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
if (auto pack =
getStateEvent<mtx::events::msc2545::ImagePack>(
txn, room_id2, state_id))
addPack(pack->content);
addPack(pack->content, room_id2, state_id);
}
}
}
@ -3441,16 +3445,23 @@ Cache::getImagePacks(const std::string &room_id, bool stickers)
// packs from current room
if (auto pack = getStateEvent<mtx::events::msc2545::ImagePack>(txn, room_id)) {
addPack(pack->content);
addPack(pack->content, room_id, "");
}
for (const auto &pack :
getStateEventsWithType<mtx::events::msc2545::ImagePack>(txn, room_id)) {
addPack(pack.content);
addPack(pack.content, room_id, pack.state_key);
}
return infos;
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(mtx::events::EventType type, const std::string &room_id)
{
auto txn = ro_txn(env_);
return getAccountData(txn, type, room_id);
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{

View file

@ -113,6 +113,7 @@ struct RoomSearchResult
struct ImagePackInfo
{
std::string packname;
std::map<std::string, mtx::events::msc2545::PackImage> images;
mtx::events::msc2545::ImagePack pack;
std::string source_room;
std::string state_key;
};

View file

@ -97,6 +97,12 @@ public:
return getStateEvent<T>(txn, room_id, state_key);
}
//! retrieve a specific event from account data
//! pass empty room_id for global account data
std::optional<mtx::events::collections::RoomAccountDataEvents> getAccountData(
mtx::events::EventType type,
const std::string &room_id = "");
//! Retrieve member info from a room.
std::vector<RoomMember> getMembers(const std::string &room_id,
std::size_t startIndex = 0,
@ -225,7 +231,8 @@ public:
std::vector<std::string> getParentRoomIds(const std::string &room_id);
std::vector<std::string> getChildRoomIds(const std::string &room_id);
std::vector<ImagePackInfo> getImagePacks(const std::string &room_id, bool stickers);
std::vector<ImagePackInfo> getImagePacks(const std::string &room_id,
std::optional<bool> stickers);
//! Mark a room that uses e2e encryption.
void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);

View file

@ -16,9 +16,10 @@ CombinedImagePackModel::CombinedImagePackModel(const std::string &roomId,
auto packs = cache::client()->getImagePacks(room_id, stickers);
for (const auto &pack : packs) {
QString packname = QString::fromStdString(pack.packname);
QString packname =
pack.pack.pack ? QString::fromStdString(pack.pack.pack->display_name) : "";
for (const auto &img : pack.images) {
for (const auto &img : pack.pack.images) {
ImageDesc i{};
i.shortcode = QString::fromStdString(img.first);
i.packname = packname;

View file

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ImagePackListModel.h"
#include <QQmlEngine>
#include "Cache_p.h"
#include "SingleImagePackModel.h"
ImagePackListModel::ImagePackListModel(const std::string &roomId, QObject *parent)
: QAbstractListModel(parent)
, room_id(roomId)
{
auto packs_ = cache::client()->getImagePacks(room_id, std::nullopt);
for (const auto &pack : packs_) {
packs.push_back(
QSharedPointer<SingleImagePackModel>(new SingleImagePackModel(pack)));
}
}
int
ImagePackListModel::rowCount(const QModelIndex &) const
{
return (int)packs.size();
}
QHash<int, QByteArray>
ImagePackListModel::roleNames() const
{
return {
{Roles::DisplayName, "displayName"},
{Roles::AvatarUrl, "avatarUrl"},
{Roles::FromAccountData, "fromAccountData"},
{Roles::FromCurrentRoom, "fromCurrentRoom"},
{Roles::StateKey, "statekey"},
{Roles::RoomId, "roomid"},
};
}
QVariant
ImagePackListModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
const auto &pack = packs.at(index.row());
switch (role) {
case Roles::DisplayName:
return pack->packname();
case Roles::AvatarUrl:
return pack->avatarUrl();
case Roles::FromAccountData:
return pack->roomid().isEmpty();
case Roles::FromCurrentRoom:
return pack->roomid().toStdString() == this->room_id;
case Roles::StateKey:
return pack->statekey();
case Roles::RoomId:
return pack->roomid();
default:
return {};
}
}
return {};
}
SingleImagePackModel *
ImagePackListModel::packAt(int row)
{
if (row < 0 || static_cast<size_t>(row) >= packs.size())
return {};
auto e = packs.at(row).get();
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
return e;
}

37
src/ImagePackListModel.h Normal file
View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
#include <QSharedPointer>
class SingleImagePackModel;
class ImagePackListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
DisplayName = Qt::UserRole,
AvatarUrl,
FromAccountData,
FromCurrentRoom,
StateKey,
RoomId,
};
ImagePackListModel(const std::string &roomId, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
Q_INVOKABLE SingleImagePackModel *packAt(int row);
private:
std::string room_id;
std::vector<QSharedPointer<SingleImagePackModel>> packs;
};

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SingleImagePackModel.h"
#include "Cache_p.h"
#include "MatrixClient.h"
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
: QAbstractListModel(parent)
, roomid_(std::move(pack_.source_room))
, statekey_(std::move(pack_.state_key))
, pack(std::move(pack_.pack))
{
if (!pack.pack)
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
for (const auto &e : pack.images)
shortcodes.push_back(e.first);
}
int
SingleImagePackModel::rowCount(const QModelIndex &) const
{
return (int)shortcodes.size();
}
QHash<int, QByteArray>
SingleImagePackModel::roleNames() const
{
return {
{Roles::Url, "url"},
{Roles::ShortCode, "shortCode"},
{Roles::Body, "body"},
{Roles::IsEmote, "isEmote"},
{Roles::IsSticker, "isSticker"},
};
}
QVariant
SingleImagePackModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
const auto &img = pack.images.at(shortcodes.at(index.row()));
switch (role) {
case Url:
return QString::fromStdString(img.url);
case ShortCode:
return QString::fromStdString(shortcodes.at(index.row()));
case Body:
return QString::fromStdString(img.body);
case IsEmote:
return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
case IsSticker:
return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
default:
return {};
}
}
return {};
}
bool
SingleImagePackModel::isGloballyEnabled() const
{
if (auto roomPacks =
cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
if (auto tmp = std::get_if<
mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
&*roomPacks)) {
if (tmp->content.rooms.count(roomid_) &&
tmp->content.rooms.at(roomid_).count(statekey_))
return true;
}
}
return false;
}
void
SingleImagePackModel::setGloballyEnabled(bool enabled)
{
mtx::events::msc2545::ImagePackRooms content{};
if (auto roomPacks =
cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
if (auto tmp = std::get_if<
mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
&*roomPacks)) {
content = tmp->content;
}
}
if (enabled)
content.rooms[roomid_][statekey_] = {};
else
content.rooms[roomid_].erase(statekey_);
http::client()->put_account_data(content, [this](mtx::http::RequestErr) {
// emit this->globallyEnabledChanged();
});
}

View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QAbstractListModel>
#include <mtx/events/mscs/image_packs.hpp>
#include "CacheStructs.h"
class SingleImagePackModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QString roomid READ roomid CONSTANT)
Q_PROPERTY(QString statekey READ statekey CONSTANT)
Q_PROPERTY(QString attribution READ statekey CONSTANT)
Q_PROPERTY(QString packname READ packname CONSTANT)
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
globallyEnabledChanged)
public:
enum Roles
{
Url = Qt::UserRole,
ShortCode,
Body,
IsEmote,
IsSticker,
};
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QString roomid() const { return QString::fromStdString(roomid_); }
QString statekey() const { return QString::fromStdString(statekey_); }
QString packname() const { return QString::fromStdString(pack.pack->display_name); }
QString attribution() const { return QString::fromStdString(pack.pack->attribution); }
QString avatarUrl() const { return QString::fromStdString(pack.pack->avatar_url); }
bool isStickerPack() const { return pack.pack->is_sticker(); }
bool isEmotePack() const { return pack.pack->is_emoji(); }
bool isGloballyEnabled() const;
void setGloballyEnabled(bool enabled);
signals:
void globallyEnabledChanged();
private:
std::string roomid_;
std::string statekey_;
mtx::events::msc2545::ImagePack pack;
std::vector<std::string> shortcodes;
};

View file

@ -20,12 +20,14 @@
#include "DelegateChooser.h"
#include "DeviceVerificationFlow.h"
#include "EventAccessors.h"
#include "ImagePackListModel.h"
#include "InviteesModel.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "RoomsModel.h"
#include "SingleImagePackModel.h"
#include "UserSettingsPage.h"
#include "UsersModel.h"
#include "dialogs/ImageOverlay.h"
@ -185,6 +187,18 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"Room Settings needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<TimelineModel>(
"im.nheko", 1, 0, "Room", "Room needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<ImagePackListModel>(
"im.nheko",
1,
0,
"ImagePackListModel",
"ImagePackListModel needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<SingleImagePackModel>(
"im.nheko",
1,
0,
"SingleImagePackModel",
"SingleImagePackModel needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<InviteesModel>(
"im.nheko",
1,
@ -436,6 +450,12 @@ TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId)
});
}
void
TimelineViewManager::openImagePackSettings(QString roomid)
{
emit showImagePackSettings(new ImagePackListModel(roomid.toStdString(), this));
}
void
TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
{

View file

@ -33,6 +33,7 @@ class ColorImageProvider;
class UserSettings;
class ChatPage;
class DeviceVerificationFlow;
class ImagePackListModel;
class TimelineViewManager : public QObject
{
@ -57,6 +58,7 @@ public:
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isWindowFocused() const { return isWindowFocused_; }
Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId);
Q_INVOKABLE void openImagePackSettings(QString roomid);
Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString escapeEmoji(QString str) const;
Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); }
@ -93,6 +95,7 @@ signals:
void openRoomSettingsDialog(RoomSettings *settings);
void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile);
void showImagePackSettings(ImagePackListModel *packlist);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);

View file

@ -136,4 +136,4 @@ private:
RoomInfo info_;
int notifications_ = 0;
int accessRules_ = 0;
};
};