summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-03-17 21:23:46 +0200
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-03-17 21:23:46 +0200
commita0ae6cf5d583bb650b7164dbe602b7552a201814 (patch)
treec831eba0640000f1fd767122d35932d82a1f3987
parenta6f867353f179d63f367ada9f4fcf1812477d815 (diff)
Add ability to redact messages
-rw-r--r--include/ChatPage.h4
-rw-r--r--include/MatrixClient.h4
-rw-r--r--include/timeline/TimelineItem.h49
-rw-r--r--include/timeline/TimelineView.h35
-rw-r--r--src/ChatPage.cc3
-rw-r--r--src/MatrixClient.cc44
-rw-r--r--src/timeline/TimelineItem.cc101
-rw-r--r--src/timeline/TimelineView.cc45
-rw-r--r--src/timeline/TimelineViewManager.cc10
9 files changed, 216 insertions, 79 deletions
diff --git a/include/ChatPage.h b/include/ChatPage.h
index c753aa97..cd516a71 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -72,6 +72,10 @@ public:
{
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_; }
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 69fa72bc..3052a118 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -86,6 +86,7 @@ public:
void sendTypingNotification(const QString &roomid, int timeoutInMillis = 20000);
void removeTypingNotification(const QString &roomid);
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 createRoom(const mtx::requests::CreateRoom &request);
@@ -171,6 +172,9 @@ signals:
void leftRoom(const QString &room_id);
void roomCreationFailed(const QString &msg);
+ void redactionFailed(const QString &error);
+ void redactionCompleted(const QString &room_id, const QString &event_id);
+
private:
QNetworkReply *makeUploadRequest(QSharedPointer<QIODevice> iodev);
QJsonObject getUploadReply(QNetworkReply *reply);
diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h
index 7c04e167..ade2f834 100644
--- a/include/timeline/TimelineItem.h
+++ b/include/timeline/TimelineItem.h
@@ -93,6 +93,9 @@ public:
ChatPage::instance()->readEvent(room_id_, event_id_);
}
+ //! Add a user avatar for this event.
+ void addAvatar();
+
protected:
void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
@@ -130,20 +133,18 @@ private:
QMenu *contextMenu_;
QAction *showReadReceipts_;
QAction *markAsRead_;
+ QAction *redactMsg_;
- QHBoxLayout *topLayout_;
- //! The message and the timestamp/checkmark.
- QHBoxLayout *messageLayout_;
- //! Avatar or Timestamp
- QVBoxLayout *sideLayout_;
- //! Header & Message body
- QVBoxLayout *mainLayout_;
-
- QVBoxLayout *headerLayout_; // Username (&) Timestamp
+ QHBoxLayout *topLayout_ = nullptr;
+ QHBoxLayout *messageLayout_ = nullptr;
+ QVBoxLayout *mainLayout_ = nullptr;
+ QVBoxLayout *headerLayout_ = nullptr;
+ QHBoxLayout *widgetLayout_ = nullptr;
Avatar *userAvatar_;
QFont font_;
+ QFont usernameFont_;
QLabel *timestamp_;
QLabel *checkmark_;
@@ -169,26 +170,23 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget,
generateTimestamp(timestamp);
- auto widgetLayout = new QHBoxLayout();
- widgetLayout->setContentsMargins(0, 5, 0, 0);
- widgetLayout->addWidget(widget);
- widgetLayout->addStretch(1);
-
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
+ widgetLayout_ = new QHBoxLayout;
+ widgetLayout_->setContentsMargins(0, 5, 0, 0);
+ widgetLayout_->addWidget(widget);
+ widgetLayout_->addStretch(1);
if (withSender) {
generateBody(displayName, "");
setupAvatarLayout(displayName);
- headerLayout_->addLayout(widgetLayout);
+ headerLayout_->addLayout(widgetLayout_);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); });
} else {
setupSimpleLayout();
- messageLayout_->addLayout(widgetLayout, 1);
+ messageLayout_->addLayout(widgetLayout_, 1);
}
messageLayout_->addWidget(checkmark_);
@@ -220,26 +218,23 @@ TimelineItem::setupWidgetLayout(Widget *widget,
generateTimestamp(timestamp);
- auto widgetLayout = new QHBoxLayout();
- widgetLayout->setContentsMargins(0, 5, 0, 0);
- widgetLayout->addWidget(widget);
- widgetLayout->addStretch(1);
-
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
+ widgetLayout_ = new QHBoxLayout();
+ widgetLayout_->setContentsMargins(0, 5, 0, 0);
+ widgetLayout_->addWidget(widget);
+ widgetLayout_->addStretch(1);
if (withSender) {
generateBody(displayName, "");
setupAvatarLayout(displayName);
- headerLayout_->addLayout(widgetLayout);
+ headerLayout_->addLayout(widgetLayout_);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); });
} else {
setupSimpleLayout();
- messageLayout_->addLayout(widgetLayout, 1);
+ messageLayout_->addLayout(widgetLayout_, 1);
}
messageLayout_->addWidget(checkmark_);
diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h
index 2876cc60..78e092b3 100644
--- a/include/timeline/TimelineView.h
+++ b/include/timeline/TimelineView.h
@@ -101,6 +101,9 @@ public:
void scrollDown();
QLabel *createDateSeparator(QDateTime datetime);
+ //! Remove an item from the timeline with the given Event ID.
+ void removeEvent(const QString &event_id);
+
public slots:
void sliderRangeChanged(int min, int max);
void sliderMoved(int position);
@@ -128,6 +131,8 @@ protected:
private:
using TimelineEvent = mtx::events::collections::TimelineEvents;
+ QWidget *relativeWidget(TimelineItem *item, int dt) const;
+
//! HACK: Fixing layout flickering when adding to the bottom
//! of the timeline.
void pushTimelineItem(TimelineItem *item)
@@ -232,7 +237,7 @@ private:
inline bool isNotifiable(const TimelineEvent &event) const;
// The events currently rendered. Used for duplicate detection.
- QMap<QString, bool> eventIds_;
+ QMap<QString, TimelineItem *> eventIds_;
QQueue<PendingMessage> pending_msgs_;
QList<PendingMessage> pending_sent_msgs_;
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 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);
- if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) {
+ if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
+ isDuplicate(event_id)) {
removePendingMessage(txnid);
return nullptr;
}
@@ -310,7 +311,11 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
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>
@@ -320,13 +325,9 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
const auto event_id = QString::fromStdString(event.event_id);
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);
- if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) {
+ if ((!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) ||
+ isDuplicate(event_id)) {
removePendingMessage(txnid);
return nullptr;
}
@@ -335,5 +336,9 @@ TimelineView::processMessageEvent(const Event &event, TimelineDirection directio
updateLastSender(sender, direction);
- return createTimelineItem<Event, Widget>(event, with_sender);
+ auto item = createTimelineItem<Event, Widget>(event, with_sender);
+
+ eventIds_[event_id] = item;
+
+ return item;
}
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index fee4f982..158427fd 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -342,6 +342,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client,
emit showNotification(QString("Room %1 created").arg(room_id));
});
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_->setSingleShot(true);
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 4a4fc67c..5f9e1b86 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -1281,3 +1281,47 @@ MatrixClient::getUploadReply(QNetworkReply *reply)
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()));
+ }
+ });
+}
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index 2f04a1bd..371ced10 100644
--- a/src/timeline/TimelineItem.cc
+++ b/src/timeline/TimelineItem.cc
@@ -40,29 +40,39 @@ TimelineItem::init()
body_ = nullptr;
font_.setPixelSize(conf::fontSize);
+ usernameFont_ = font_;
+ usernameFont_.setWeight(60);
QFontMetrics fm(font_);
contextMenu_ = new QMenu(this);
showReadReceipts_ = new QAction("Read receipts", this);
markAsRead_ = new QAction("Mark as read", this);
+ redactMsg_ = new QAction("Redact message", this);
contextMenu_->addAction(showReadReceipts_);
contextMenu_->addAction(markAsRead_);
+ contextMenu_->addAction(redactMsg_);
connect(showReadReceipts_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
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(); });
topLayout_ = new QHBoxLayout(this);
mainLayout_ = new QVBoxLayout;
messageLayout_ = new QHBoxLayout;
+ messageLayout_->setContentsMargins(0, 0, 20, 4);
+ messageLayout_->setSpacing(20);
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
topLayout_->setSpacing(0);
-
topLayout_->addLayout(mainLayout_, 1);
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
// space and the Unicode checkmark.
- checkmark_ = new QLabel(" ", this);
+ checkmark_ = new QLabel(this);
checkmark_->setFont(checkmarkFont);
checkmark_->setFixedWidth(QFontMetrics{checkmarkFont}.width(CHECKMARK));
}
@@ -106,9 +116,6 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty,
body.replace("\n", "<br/>");
generateTimestamp(timestamp);
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
-
if (withSender) {
generateBody(displayName, body);
setupAvatarLayout(displayName);
@@ -240,9 +247,6 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice
body.replace("\n", "<br/>");
body = "<i>" + body + "</i>";
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
-
if (with_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("\n", "<br/>");
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
-
if (with_sender) {
generateBody(displayName, emoteMsg);
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("\n", "<br/>");
- messageLayout_->setContentsMargins(0, 0, 20, 4);
- messageLayout_->setSpacing(20);
-
if (with_sender) {
generateBody(displayName, body);
setupAvatarLayout(displayName);
@@ -400,25 +398,13 @@ TimelineItem::generateBody(const QString &userid, const QString &body)
sender = userid.split(":")[0].split("@")[1];
}
- QFont usernameFont = font_;
- usernameFont.setWeight(60);
-
- QFontMetrics fm(usernameFont);
+ QFontMetrics fm(usernameFont_);
userName_ = new QLabel(this);
- userName_->setFont(usernameFont);
+ userName_->setFont(usernameFont_);
userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500));
- if (body.isEmpty())
- 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);
+ generateBody(body);
}
void
@@ -474,12 +460,8 @@ TimelineItem::setupAvatarLayout(const QString &userName)
if (userName[0] == '@' && userName.size() > 1)
userAvatar_->setLetter(QChar(userName[1]).toUpper());
- sideLayout_ = new QVBoxLayout;
- sideLayout_->setMargin(0);
- sideLayout_->setSpacing(0);
- sideLayout_->addWidget(userAvatar_);
- sideLayout_->addStretch(1);
- topLayout_->insertLayout(0, sideLayout_);
+ topLayout_->insertWidget(0, userAvatar_);
+ topLayout_->setAlignment(userAvatar_, Qt::AlignTop);
headerLayout_ = new QVBoxLayout;
headerLayout_->setMargin(0);
@@ -492,8 +474,8 @@ TimelineItem::setupAvatarLayout(const QString &userName)
void
TimelineItem::setupSimpleLayout()
{
- topLayout_->setContentsMargins(conf::timeline::avatarSize + conf::timeline::msgMargin + 1,
- conf::timeline::msgMargin / 3,
+ topLayout_->setContentsMargins(conf::timeline::msgMargin + conf::timeline::avatarSize + 2,
+ conf::timeline::msgMargin,
0,
0);
}
@@ -533,3 +515,48 @@ TimelineItem::addSaveImageAction(ImageItem *image)
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); });
+}
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index 7e281e03..ded5ad2c 100644
--- a/src/timeline/TimelineView.cc
+++ b/src/timeline/TimelineView.cc
@@ -491,6 +491,7 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id)
if (msg.widget) {
msg.widget->setEventId(event_id);
msg.widget->markReceived();
+ eventIds_[event_id] = msg.widget;
}
pending_sent_msgs_.append(msg);
@@ -591,6 +592,9 @@ TimelineView::isPendingMessage(const QString &txnid,
void
TimelineView::removePendingMessage(const QString &txnid)
{
+ if (txnid.isEmpty())
+ return;
+
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) {
int index = std::distance(pending_sent_msgs_.begin(), it);
@@ -739,3 +743,44 @@ TimelineView::toggleScrollDownButton()
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();
+}
diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index ccbf509b..55f25dfc 100644
--- a/src/timeline/TimelineViewManager.cc
+++ b/src/timeline/TimelineViewManager.cc
@@ -44,6 +44,16 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<MatrixClient> client, QW
&MatrixClient::messageSendFailed,
this,
&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