Add ability to redact messages

This commit is contained in:
Konstantinos Sideris 2018-03-17 21:23:46 +02:00
parent a6f867353f
commit a0ae6cf5d5
9 changed files with 216 additions and 79 deletions

View File

@ -72,6 +72,10 @@ public:
{ {
client_->readEvent(room_id, event_id); client_->readEvent(room_id, event_id);
} }
void redactEvent(const QString &room_id, const QString &event_id)
{
client_->redactEvent(room_id, event_id);
}
QSharedPointer<UserSettings> userSettings() { return userSettings_; } QSharedPointer<UserSettings> userSettings() { return userSettings_; }

View File

@ -86,6 +86,7 @@ public:
void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000); void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000);
void removeTypingNotification(const QString &roomid); void removeTypingNotification(const QString &roomid);
void readEvent(const QString &room_id, const QString &event_id); void readEvent(const QString &room_id, const QString &event_id);
void redactEvent(const QString &room_id, const QString &event_id);
void inviteUser(const QString &room_id, const QString &user); void inviteUser(const QString &room_id, const QString &user);
void createRoom(const mtx::requests::CreateRoom &request); void createRoom(const mtx::requests::CreateRoom &request);
@ -171,6 +172,9 @@ signals:
void leftRoom(const QString &room_id); void leftRoom(const QString &room_id);
void roomCreationFailed(const QString &msg); void roomCreationFailed(const QString &msg);
void redactionFailed(const QString &error);
void redactionCompleted(const QString &room_id, const QString &event_id);
private: private:
QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev); QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
QJsonObject getUploadReply(QNetworkReply *reply); QJsonObject getUploadReply(QNetworkReply *reply);

View File

@ -93,6 +93,9 @@ public:
ChatPage::instance()->readEvent(room_id_, event_id_); ChatPage::instance()->readEvent(room_id_, event_id_);
} }
//! Add a user avatar for this event.
void addAvatar();
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override;
@ -130,20 +133,18 @@ private:
QMenu *contextMenu_; QMenu *contextMenu_;
QAction *showReadReceipts_; QAction *showReadReceipts_;
QAction *markAsRead_; QAction *markAsRead_;
QAction *redactMsg_;
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_ = nullptr;
//! The message and the timestamp/checkmark. QHBoxLayout *messageLayout_ = nullptr;
QHBoxLayout *messageLayout_; QVBoxLayout *mainLayout_ = nullptr;
//! Avatar or Timestamp QVBoxLayout *headerLayout_ = nullptr;
QVBoxLayout *sideLayout_; QHBoxLayout *widgetLayout_ = nullptr;
//! Header & Message body
QVBoxLayout *mainLayout_;
QVBoxLayout *headerLayout_; // Username (&) Timestamp
Avatar *userAvatar_; Avatar *userAvatar_;
QFont font_; QFont font_;
QFont usernameFont_;
QLabel *timestamp_; QLabel *timestamp_;
QLabel *checkmark_; QLabel *checkmark_;
@ -169,26 +170,23 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget,
generateTimestamp(timestamp); generateTimestamp(timestamp);
auto widgetLayout = new QHBoxLayout(); widgetLayout_ = new QHBoxLayout;
widgetLayout->setContentsMargins(0, 5, 0, 0); widgetLayout_->setContentsMargins(0, 5, 0, 0);
widgetLayout->addWidget(widget); widgetLayout_->addWidget(widget);
widgetLayout->addStretch(1); widgetLayout_->addStretch(1);
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (withSender) { if (withSender) {
generateBody(displayName, ""); generateBody(displayName, "");
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
headerLayout_->addLayout(widgetLayout); headerLayout_->addLayout(widgetLayout_);
messageLayout_->addLayout(headerLayout_, 1); messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); }); AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); });
} else { } else {
setupSimpleLayout(); setupSimpleLayout();
messageLayout_->addLayout(widgetLayout, 1); messageLayout_->addLayout(widgetLayout_, 1);
} }
messageLayout_->addWidget(checkmark_); messageLayout_->addWidget(checkmark_);
@ -220,26 +218,23 @@ TimelineItem::setupWidgetLayout(Widget *widget,
generateTimestamp(timestamp); generateTimestamp(timestamp);
auto widgetLayout = new QHBoxLayout(); widgetLayout_ = new QHBoxLayout();
widgetLayout->setContentsMargins(0, 5, 0, 0); widgetLayout_->setContentsMargins(0, 5, 0, 0);
widgetLayout->addWidget(widget); widgetLayout_->addWidget(widget);
widgetLayout->addStretch(1); widgetLayout_->addStretch(1);
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (withSender) { if (withSender) {
generateBody(displayName, ""); generateBody(displayName, "");
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
headerLayout_->addLayout(widgetLayout); headerLayout_->addLayout(widgetLayout_);
messageLayout_->addLayout(headerLayout_, 1); messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); }); AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); });
} else { } else {
setupSimpleLayout(); setupSimpleLayout();
messageLayout_->addLayout(widgetLayout, 1); messageLayout_->addLayout(widgetLayout_, 1);
} }
messageLayout_->addWidget(checkmark_); messageLayout_->addWidget(checkmark_);

View File

@ -101,6 +101,9 @@ public:
void scrollDown(); void scrollDown();
QLabel *createDateSeparator(QDateTime datetime); QLabel *createDateSeparator(QDateTime datetime);
//! Remove an item from the timeline with the given Event ID.
void removeEvent(const QString &event_id);
public slots: public slots:
void sliderRangeChanged(int min, int max); void sliderRangeChanged(int min, int max);
void sliderMoved(int position); void sliderMoved(int position);
@ -128,6 +131,8 @@ protected:
private: private:
using TimelineEvent = mtx::events::collections::TimelineEvents; using TimelineEvent = mtx::events::collections::TimelineEvents;
QWidget *relativeWidget(TimelineItem *item, int dt) const;
//! HACK: Fixing layout flickering when adding to the bottom //! HACK: Fixing layout flickering when adding to the bottom
//! of the timeline. //! of the timeline.
void pushTimelineItem(TimelineItem *item) void pushTimelineItem(TimelineItem *item)
@ -232,7 +237,7 @@ private:
inline bool isNotifiable(const TimelineEvent &event) const; inline bool isNotifiable(const TimelineEvent &event) const;
// The events currently rendered. Used for duplicate detection. // The events currently rendered. Used for duplicate detection.
QMap<QString, bool> eventIds_; QMap<QString, TimelineItem *> eventIds_;
QQueue<PendingMessage> pending_msgs_; QQueue<PendingMessage> pending_msgs_;
QList<PendingMessage> pending_sent_msgs_; QList<PendingMessage> pending_sent_msgs_;
QSharedPointer<MatrixClient> client_; QSharedPointer<MatrixClient> client_;
@ -295,13 +300,9 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
const auto event_id = QString::fromStdString(event.event_id); const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender); const auto sender = QString::fromStdString(event.sender);
if (isDuplicate(event_id))
return nullptr;
eventIds_[event_id] = true;
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) { if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txnid); removePendingMessage(txnid);
return nullptr; return nullptr;
} }
@ -310,7 +311,11 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
updateLastSender(sender, direction); updateLastSender(sender, direction);
return createTimelineItem<Event>(event, with_sender); auto item = createTimelineItem<Event>(event, with_sender);
eventIds_[event_id] = item;
return item;
} }
template<class Event, class Widget> template<class Event, class Widget>
@ -320,13 +325,9 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
const auto event_id = QString::fromStdString(event.event_id); const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender); const auto sender = QString::fromStdString(event.sender);
if (isDuplicate(event_id))
return nullptr;
eventIds_[event_id] = true;
const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id);
if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) { if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txnid); removePendingMessage(txnid);
return nullptr; return nullptr;
} }
@ -335,5 +336,9 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
updateLastSender(sender, direction); updateLastSender(sender, direction);
return createTimelineItem<Event, Widget>(event, with_sender); auto item = createTimelineItem<Event, Widget>(event, with_sender);
eventIds_[event_id] = item;
return item;
} }

View File

@ -342,6 +342,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
emit showNotification(QString("Room %1 created").arg(room_id)); emit showNotification(QString("Room %1 created").arg(room_id));
}); });
connect(client_.data(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom); connect(client_.data(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom);
connect(client_.data(), &MatrixClient::redactionFailed, this, [this](const QString &error) {
emit showNotification(QString("Message redaction failed: %1").arg(error));
});
showContentTimer_ = new QTimer(this); showContentTimer_ = new QTimer(this);
showContentTimer_->setSingleShot(true); showContentTimer_->setSingleShot(true);

View File

@ -1281,3 +1281,47 @@ MatrixClient::getUploadReply(QNetworkReply *reply)
return object; return object;
} }
void
MatrixClient::redactEvent(const QString &room_id, const QString &event_id)
{
QUrlQuery query;
query.addQueryItem("access_token", token_);
QUrl endpoint(server_);
endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/redact/%2/%3")
.arg(room_id)
.arg(event_id)
.arg(incrementTransactionId()));
endpoint.setQuery(query);
QNetworkRequest request(QString(endpoint.toEncoded()));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
// TODO: no reason specified
QJsonObject body{};
auto reply = put(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, this, [reply, this, room_id, event_id]() {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
auto data = reply->readAll();
if (status == 0 || status >= 400) {
try {
mtx::errors::Error res = nlohmann::json::parse(data);
emit redactionFailed(QString::fromStdString(res.error));
return;
} catch (const std::exception &) {
}
}
try {
mtx::responses::EventId res = nlohmann::json::parse(data);
emit redactionCompleted(room_id, event_id);
} catch (const std::exception &e) {
emit redactionFailed(QString::fromStdString(e.what()));
}
});
}

View File

@ -40,29 +40,39 @@ TimelineItem::init()
body_ = nullptr; body_ = nullptr;
font_.setPixelSize(conf::fontSize); font_.setPixelSize(conf::fontSize);
usernameFont_ = font_;
usernameFont_.setWeight(60);
QFontMetrics fm(font_); QFontMetrics fm(font_);
contextMenu_ = new QMenu(this); contextMenu_ = new QMenu(this);
showReadReceipts_ = new QAction("Read receipts", this); showReadReceipts_ = new QAction("Read receipts", this);
markAsRead_ = new QAction("Mark as read", this); markAsRead_ = new QAction("Mark as read", this);
redactMsg_ = new QAction("Redact message", this);
contextMenu_->addAction(showReadReceipts_); contextMenu_->addAction(showReadReceipts_);
contextMenu_->addAction(markAsRead_); contextMenu_->addAction(markAsRead_);
contextMenu_->addAction(redactMsg_);
connect(showReadReceipts_, &QAction::triggered, this, [this]() { connect(showReadReceipts_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty()) if (!event_id_.isEmpty())
ChatPage::instance()->showReadReceipts(event_id_); ChatPage::instance()->showReadReceipts(event_id_);
}); });
connect(redactMsg_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
ChatPage::instance()->redactEvent(room_id_, event_id_);
});
connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); }); connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
mainLayout_ = new QVBoxLayout; mainLayout_ = new QVBoxLayout;
messageLayout_ = new QHBoxLayout; messageLayout_ = new QHBoxLayout;
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0); topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
topLayout_->setSpacing(0); topLayout_->setSpacing(0);
topLayout_->addLayout(mainLayout_, 1); topLayout_->addLayout(mainLayout_, 1);
mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
@ -73,7 +83,7 @@ TimelineItem::init()
// Setting fixed width for checkmark because systems may have a differing width for a // Setting fixed width for checkmark because systems may have a differing width for a
// space and the Unicode checkmark. // space and the Unicode checkmark.
checkmark_ = new QLabel(" ", this); checkmark_ = new QLabel(this);
checkmark_->setFont(checkmarkFont); checkmark_->setFont(checkmarkFont);
checkmark_->setFixedWidth(QFontMetrics{checkmarkFont}.width(CHECKMARK)); checkmark_->setFixedWidth(QFontMetrics{checkmarkFont}.width(CHECKMARK));
} }
@ -106,9 +116,6 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty,
body.replace("\n", "<br/>"); body.replace("\n", "<br/>");
generateTimestamp(timestamp); generateTimestamp(timestamp);
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (withSender) { if (withSender) {
generateBody(displayName, body); generateBody(displayName, body);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
@ -240,9 +247,6 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
body.replace("\n", "<br/>"); body.replace("\n", "<br/>");
body = "<i>" + body + "</i>"; body = "<i>" + body + "</i>";
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (with_sender) { if (with_sender) {
auto displayName = TimelineViewManager::displayName(sender); auto displayName = TimelineViewManager::displayName(sender);
@ -289,9 +293,6 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote>
emoteMsg.replace(conf::strings::url_regex, conf::strings::url_html); emoteMsg.replace(conf::strings::url_regex, conf::strings::url_html);
emoteMsg.replace("\n", "<br/>"); emoteMsg.replace("\n", "<br/>");
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (with_sender) { if (with_sender) {
generateBody(displayName, emoteMsg); generateBody(displayName, emoteMsg);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
@ -341,9 +342,6 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text>
body.replace(conf::strings::url_regex, conf::strings::url_html); body.replace(conf::strings::url_regex, conf::strings::url_html);
body.replace("\n", "<br/>"); body.replace("\n", "<br/>");
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
if (with_sender) { if (with_sender) {
generateBody(displayName, body); generateBody(displayName, body);
setupAvatarLayout(displayName); setupAvatarLayout(displayName);
@ -400,25 +398,13 @@ TimelineItem::generateBody(const QString &userid, const QString &body)
sender = userid.split(":")[0].split("@")[1]; sender = userid.split(":")[0].split("@")[1];
} }
QFont usernameFont = font_; QFontMetrics fm(usernameFont_);
usernameFont.setWeight(60);
QFontMetrics fm(usernameFont);
userName_ = new QLabel(this); userName_ = new QLabel(this);
userName_->setFont(usernameFont); userName_->setFont(usernameFont_);
userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500)); userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500));
if (body.isEmpty()) generateBody(body);
return;
body_ = new QLabel(this);
body_->setFont(font_);
body_->setWordWrap(true);
body_->setText(QString("<span>%1</span>").arg(replaceEmoji(body)));
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
body_->setOpenExternalLinks(true);
body_->setMargin(0);
} }
void void
@ -474,12 +460,8 @@ TimelineItem::setupAvatarLayout(const QString &userName)
if (userName[0] == '@' && userName.size() > 1) if (userName[0] == '@' && userName.size() > 1)
userAvatar_->setLetter(QChar(userName[1]).toUpper()); userAvatar_->setLetter(QChar(userName[1]).toUpper());
sideLayout_ = new QVBoxLayout; topLayout_->insertWidget(0, userAvatar_);
sideLayout_->setMargin(0); topLayout_->setAlignment(userAvatar_, Qt::AlignTop);
sideLayout_->setSpacing(0);
sideLayout_->addWidget(userAvatar_);
sideLayout_->addStretch(1);
topLayout_->insertLayout(0, sideLayout_);
headerLayout_ = new QVBoxLayout; headerLayout_ = new QVBoxLayout;
headerLayout_->setMargin(0); headerLayout_->setMargin(0);
@ -492,8 +474,8 @@ TimelineItem::setupAvatarLayout(const QString &userName)
void void
TimelineItem::setupSimpleLayout() TimelineItem::setupSimpleLayout()
{ {
topLayout_->setContentsMargins(conf::timeline::avatarSize + conf::timeline::msgMargin + 1, topLayout_->setContentsMargins(conf::timeline::msgMargin + conf::timeline::avatarSize + 2,
conf::timeline::msgMargin / 3, conf::timeline::msgMargin,
0, 0,
0); 0);
} }
@ -533,3 +515,48 @@ TimelineItem::addSaveImageAction(ImageItem *image)
connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);
} }
} }
void
TimelineItem::addAvatar()
{
if (userAvatar_)
return;
// TODO: should be replaced with the proper event struct.
auto userid = descriptionMsg_.userid;
auto displayName = TimelineViewManager::displayName(userid);
QFontMetrics fm(usernameFont_);
userName_ = new QLabel(this);
userName_->setFont(usernameFont_);
userName_->setText(fm.elidedText(displayName, Qt::ElideRight, 500));
QWidget *widget = nullptr;
// Extract the widget before we delete its layout.
if (widgetLayout_)
widget = widgetLayout_->itemAt(0)->widget();
// Remove all items from the layout.
QLayoutItem *item;
while ((item = messageLayout_->takeAt(0)) != 0)
delete item;
setupAvatarLayout(displayName);
// Restore widget's layout.
if (widget) {
widgetLayout_ = new QHBoxLayout();
widgetLayout_->setContentsMargins(0, 5, 0, 0);
widgetLayout_->addWidget(widget);
widgetLayout_->addStretch(1);
headerLayout_->addLayout(widgetLayout_);
}
messageLayout_->addLayout(headerLayout_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); });
}

View File

@ -491,6 +491,7 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id)
if (msg.widget) { if (msg.widget) {
msg.widget->setEventId(event_id); msg.widget->setEventId(event_id);
msg.widget->markReceived(); msg.widget->markReceived();
eventIds_[event_id] = msg.widget;
} }
pending_sent_msgs_.append(msg); pending_sent_msgs_.append(msg);
@ -591,6 +592,9 @@ TimelineView::isPendingMessage(const QString &txnid,
void void
TimelineView::removePendingMessage(const QString &txnid) TimelineView::removePendingMessage(const QString &txnid)
{ {
if (txnid.isEmpty())
return;
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) { if (QString::number(it->txn_id) == txnid) {
int index = std::distance(pending_sent_msgs_.begin(), it); int index = std::distance(pending_sent_msgs_.begin(), it);
@ -739,3 +743,44 @@ TimelineView::toggleScrollDownButton()
scrollDownBtn_->hide(); scrollDownBtn_->hide();
} }
} }
void
TimelineView::removeEvent(const QString &event_id)
{
if (!eventIds_.contains(event_id)) {
qWarning() << "unknown event_id couldn't be removed:" << event_id;
return;
}
auto removedItem = eventIds_[event_id];
// Find the next and the previous widgets in the timeline
auto prevItem = qobject_cast<TimelineItem *>(relativeWidget(removedItem, -1));
auto nextItem = qobject_cast<TimelineItem *>(relativeWidget(removedItem, 1));
// If it's a TimelineItem add an avatar.
if (prevItem)
prevItem->addAvatar();
if (nextItem)
nextItem->addAvatar();
// Finally remove the event.
removedItem->deleteLater();
eventIds_.remove(event_id);
}
QWidget *
TimelineView::relativeWidget(TimelineItem *item, int dt) const
{
int pos = scroll_layout_->indexOf(item);
if (pos == -1)
return nullptr;
pos = pos + dt;
bool isOutOfBounds = (pos <= 0 || pos > scroll_layout_->count() - 1);
return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget();
}

View File

@ -44,6 +44,16 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<MatrixClient> client, QW
&MatrixClient::messageSendFailed, &MatrixClient::messageSendFailed,
this, this,
&TimelineViewManager::messageSendFailed); &TimelineViewManager::messageSendFailed);
connect(client_.data(),
&MatrixClient::redactionCompleted,
this,
[this](const QString &room_id, const QString &event_id) {
auto view = views_[room_id];
if (view)
view->removeEvent(event_id);
});
} }
void void