Newer
Older
#include <QFileDialog>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QSettings>
#include "MainWindow.h"
#include "MatrixClient.h"
namespace std {
inline uint
qHash(const std::string &key, uint seed = 0)
{
return qHash(QByteArray::fromRawData(key.data(), key.length()), seed);
}
}
template<class T>
qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
{
using mtx::events::EventType;
switch (e.type) {
case EventType::RoomKeyRequest:
return qml_mtx_events::EventType::KeyRequest;
case EventType::RoomAliases:
return qml_mtx_events::EventType::Aliases;
case EventType::RoomAvatar:
return qml_mtx_events::EventType::Avatar;
case EventType::RoomCanonicalAlias:
return qml_mtx_events::EventType::CanonicalAlias;
case EventType::RoomCreate:
return qml_mtx_events::EventType::RoomCreate;
case EventType::RoomEncrypted:
return qml_mtx_events::EventType::Encrypted;
case EventType::RoomEncryption:
return qml_mtx_events::EventType::Encryption;
case EventType::RoomGuestAccess:
return qml_mtx_events::EventType::RoomGuestAccess;
case EventType::RoomHistoryVisibility:
return qml_mtx_events::EventType::RoomHistoryVisibility;
return qml_mtx_events::EventType::RoomJoinRules;
case EventType::RoomMember:
return qml_mtx_events::EventType::Member;
case EventType::RoomMessage:
return qml_mtx_events::EventType::UnknownMessage;
case EventType::RoomName:
return qml_mtx_events::EventType::Name;
case EventType::RoomPowerLevels:
return qml_mtx_events::EventType::PowerLevels;
case EventType::RoomTopic:
return qml_mtx_events::EventType::Topic;
case EventType::RoomTombstone:
return qml_mtx_events::EventType::Tombstone;
case EventType::RoomRedaction:
return qml_mtx_events::EventType::Redaction;
case EventType::RoomPinnedEvents:
return qml_mtx_events::EventType::PinnedEvents;
case EventType::Sticker:
return qml_mtx_events::EventType::Sticker;
case EventType::Tag:
return qml_mtx_events::EventType::Tag;
case EventType::Unsupported:
return qml_mtx_events::EventType::Unsupported;
default:
return qml_mtx_events::EventType::UnknownMessage;
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
}
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
{
return qml_mtx_events::EventType::AudioMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
{
return qml_mtx_events::EventType::EmoteMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
{
return qml_mtx_events::EventType::FileMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
{
return qml_mtx_events::EventType::ImageMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
{
return qml_mtx_events::EventType::NoticeMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
{
return qml_mtx_events::EventType::TextMessage;
}
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
{
return qml_mtx_events::EventType::VideoMessage;
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
{
return qml_mtx_events::EventType::Redacted;
// ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
// ::EventType::LocationMessage; }
};
toRoomEventType(const mtx::events::collections::TimelineEvents &event)
return std::visit(RoomEventType{}, event);
QString
toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
{
return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
event);
}
TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
: QAbstractListModel(parent)
, room_id_(room_id)
{
connect(
this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents);
connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) {
Nicolas Werner
committed
nhlog::ui()->error("Failed to send {}, retrying", txn_id.toStdString());
QTimer::singleShot(5000, this, [this]() { emit nextPendingMessage(); });
});
connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
pending.removeOne(txn_id);
int idx = idToIndex(txn_id);
if (idx < 0) {
// transaction already received via sync
return;
}
eventOrder[idx] = event_id;
auto ev = events.value(txn_id);
[event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
auto eventCopy = e;
eventCopy.event_id = event_id.toStdString();
return eventCopy;
},
ev);
events.remove(txn_id);
events.insert(event_id, ev);
// mark our messages as read
readEvent(event_id.toStdString());
emit dataChanged(index(idx, 0), index(idx, 0));
if (pending.size() > 0)
emit nextPendingMessage();
connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
emit ChatPage::instance()->showNotification(msg);
});
connect(
this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
this,
[this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
events.insert(QString::fromStdString(mtx::accessors::event_id(event)),
event);
auto idx = idToIndex(requestingEvent);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
});
QHash<int, QByteArray>
TimelineModel::roleNames() const
{
return {
{Section, "section"},
{Body, "body"},
{FormattedBody, "formattedBody"},
{UserId, "userId"},
{UserName, "userName"},
{Timestamp, "timestamp"},
{Height, "height"},
{Width, "width"},
{ProportionalHeight, "proportionalHeight"},
{RoomName, "roomName"},
{RoomTopic, "roomTopic"},
};
}
int
TimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return (int)this->eventOrder.size();
}
QVariantMap
TimelineModel::getDump(QString eventId) const
{
if (events.contains(eventId))
TimelineModel::data(const QString &id, int role) const
mtx::events::collections::TimelineEvents event = events.value(id);
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
event = decryptEvent(*e).event;
}
return QVariant(QString::fromStdString(acc::sender(event)));
case UserName:
return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case Timestamp:
return QVariant(origin_server_ts(event));
return QVariant(toRoomEventType(event));
case TypeString:
return QVariant(toRoomEventTypeString(event));
return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
case FormattedBody: {
const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
bool isReply = !in_reply_to_event(event).empty();
auto formattedBody_ = QString::fromStdString(formatted_body(event));
if (formattedBody_.isEmpty()) {
auto body_ = QString::fromStdString(body(event));
if (isReply) {
while (body_.startsWith("> "))
body_ = body_.right(body_.size() - body_.indexOf('\n') - 1);
if (body_.startsWith('\n'))
body_ = body_.right(body_.size() - 1);
}
formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>");
} else {
if (isReply)
formattedBody_ = formattedBody_.remove(replyFallback);
}
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
return QVariant(QString::fromStdString(url(event)));
return QVariant(QString::fromStdString(thumbnail_url(event)));
case Blurhash:
return QVariant(QString::fromStdString(blurhash(event)));
return QVariant(QString::fromStdString(filename(event)));
return QVariant(utils::humanReadableFileSize(filesize(event)));
return QVariant(QString::fromStdString(mimetype(event)));
return QVariant(qulonglong{media_height(event)});
return QVariant(qulonglong{media_width(event)});
case ProportionalHeight: {
auto w = media_width(event);
if (w == 0)
w = 1;
double prop = media_height(event) / (double)w;
return QVariant(prop > 0 ? prop : 1.);
}
if (acc::sender(event) != http::client()->user_id().to_string())
else if (pending.contains(id))
return qml_mtx_events::Sent;
else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1)
return qml_mtx_events::Read;
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event)));
case Reactions:
if (reactions.count(id))
return QVariant::fromValue((QObject *)&reactions.at(id));
else
return {};
case RoomId:
return QVariant(QString::fromStdString(room_id(event)));
return QVariant(QString::fromStdString(room_name(event)));
return QVariant(QString::fromStdString(room_topic(event)));
case Dump: {
QVariantMap m;
auto names = roleNames();
// m.insert(names[Section], data(id, static_cast<int>(Section)));
m.insert(names[Type], data(id, static_cast<int>(Type)));
m.insert(names[TypeString], data(id, static_cast<int>(TypeString)));
m.insert(names[Body], data(id, static_cast<int>(Body)));
m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody)));
m.insert(names[UserId], data(id, static_cast<int>(UserId)));
m.insert(names[UserName], data(id, static_cast<int>(UserName)));
m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp)));
m.insert(names[Url], data(id, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl)));
m.insert(names[Blurhash], data(id, static_cast<int>(Blurhash)));
m.insert(names[Filename], data(id, static_cast<int>(Filename)));
m.insert(names[Filesize], data(id, static_cast<int>(Filesize)));
m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));
m.insert(names[Height], data(id, static_cast<int>(Height)));
m.insert(names[Width], data(id, static_cast<int>(Width)));
m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight)));
m.insert(names[Id], data(id, static_cast<int>(Id)));
m.insert(names[State], data(id, static_cast<int>(State)));
m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted)));
m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo)));
m.insert(names[RoomName], data(id, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic)));
default:
return QVariant();
}
}
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
QVariant
TimelineModel::data(const QModelIndex &index, int role) const
{
using namespace mtx::accessors;
namespace acc = mtx::accessors;
if (index.row() < 0 && index.row() >= (int)eventOrder.size())
return QVariant();
QString id = eventOrder[index.row()];
mtx::events::collections::TimelineEvents event = events.value(id);
if (role == Section) {
QDateTime date = origin_server_ts(event);
date.setTime(QTime());
std::string userId = acc::sender(event);
for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
auto tempEv = events.value(eventOrder[r]);
QDateTime prevDate = origin_server_ts(tempEv);
prevDate.setTime(QTime());
if (prevDate != date)
return QString("%2 %1")
.arg(date.toMSecsSinceEpoch())
.arg(QString::fromStdString(userId));
std::string prevUserId = acc::sender(tempEv);
if (userId != prevUserId)
break;
}
return QString("%1").arg(QString::fromStdString(userId));
}
return data(id, role);
}
bool
TimelineModel::canFetchMore(const QModelIndex &) const
{
if (eventOrder.empty())
return true;
if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(
events[eventOrder.back()]))
return true;
else
return false;
}
void
TimelineModel::setPaginationInProgress(const bool paginationInProgress)
{
if (m_paginationInProgress == paginationInProgress) {
return;
}
m_paginationInProgress = paginationInProgress;
emit paginationInProgressChanged(m_paginationInProgress);
}
void
TimelineModel::fetchMore(const QModelIndex &)
{
if (m_paginationInProgress) {
nhlog::ui()->warn("Already loading older messages");
return;
}
setPaginationInProgress(true);
mtx::http::MessagesOpts opts;
opts.room_id = room_id_.toStdString();
opts.from = prev_batch_token_.toStdString();
nhlog::ui()->debug("Paginating room {}", opts.room_id);
http::client()->messages(
opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) {
if (err) {
Nicolas Werner
committed
nhlog::net()->error("failed to call /messages ({}): {} - {} - {}",
opts.room_id,
mtx::errors::to_string(err->matrix_error.errcode),
Nicolas Werner
committed
err->matrix_error.error,
err->parse_error);
setPaginationInProgress(false);
return;
}
emit oldMessagesRetrieved(std::move(res));
setPaginationInProgress(false);
TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
if (isInitialSync) {
prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
isInitialSync = false;
}
if (timeline.events.empty())
return;
std::vector<QString> ids = internalAddEvents(timeline.events);
if (!ids.empty()) {
beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
endInsertRows();
}
if (!timeline.events.empty())
updateLastMessage();
template<typename T>
auto
isMessage(const mtx::events::RoomEvent<T> &e)
-> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool>
{
return true;
}
template<typename T>
auto
isMessage(const mtx::events::Event<T> &)
{
return false;
}
template<typename T>
auto
isMessage(const mtx::events::EncryptedEvent<T> &)
{
return true;
}
void
TimelineModel::updateLastMessage()
for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) {
if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
event = decryptEvent(*e).event;
}
if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, event))
continue;
auto description = utils::getMessageDescription(
event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
emit manager_->updateRoomsLastMessage(room_id_, description);
return;
}
}
std::vector<QString>
TimelineModel::internalAddEvents(
const std::vector<mtx::events::collections::TimelineEvents> &timeline)
{
QString id = QString::fromStdString(mtx::accessors::event_id(e));
if (this->events.contains(id)) {
this->events.insert(id, e);
int idx = idToIndex(id);
emit dataChanged(index(idx, 0), index(idx, 0));
Nicolas Werner
committed
continue;
Nicolas Werner
committed
QString txid = QString::fromStdString(mtx::accessors::transaction_id(e));
if (this->pending.removeOne(txid)) {
this->events.insert(id, e);
this->events.remove(txid);
int idx = idToIndex(txid);
if (idx < 0) {
nhlog::ui()->warn("Received index out of range");
continue;
}
eventOrder[idx] = id;
emit dataChanged(index(idx, 0), index(idx, 0));
continue;
}
std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
QString redacts = QString::fromStdString(redaction->redacts);
auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts);
auto event = events.value(redacts);
if (auto reaction =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
&event)) {
QString reactedTo =
QString::fromStdString(reaction->content.relates_to.event_id);
reactions[reactedTo].removeReaction(*reaction);
int idx = idToIndex(reactedTo);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
}
[](const auto &ev)
-> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
mtx::events::RoomEvent<mtx::events::msg::Redacted>
replacement = {};
replacement.event_id = ev.event_id;
replacement.room_id = ev.room_id;
replacement.sender = ev.sender;
replacement.origin_server_ts = ev.origin_server_ts;
replacement.type = ev.type;
return replacement;
},
e);
events.insert(redacts, redactedEvent);
int row = (int)std::distance(eventOrder.begin(), redacted);
emit dataChanged(index(row, 0), index(row, 0));
}
continue; // don't insert redaction into timeline
}
if (auto reaction =
std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&e)) {
QString reactedTo =
QString::fromStdString(reaction->content.relates_to.event_id);
reactions[reactedTo].addReaction(room_id_.toStdString(), *reaction);
int idx = idToIndex(reactedTo);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
continue; // don't insert reaction into timeline
}
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
auto e_ = decryptEvent(*event).event;
auto encInfo = mtx::accessors::file(e_);
if (encInfo)
emit newEncryptedImage(encInfo.value());
}
this->events.insert(id, e);
ids.push_back(id);
auto replyTo = mtx::accessors::in_reply_to_event(e);
auto qReplyTo = QString::fromStdString(replyTo);
if (!replyTo.empty() && !events.contains(qReplyTo)) {
http::client()->get_event(
this->room_id_.toStdString(),
replyTo,
[this, id, replyTo](
const mtx::events::collections::TimelineEvents &timeline,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"Failed to retrieve event with id {}, which was "
"requested to show the replyTo for event {}",
replyTo,
id.toStdString());
return;
}
emit eventFetched(id, timeline);
void
TimelineModel::setCurrentIndex(int index)
{
auto oldIndex = idToIndex(currentId);
emit currentIndexChanged(index);
if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) &&
ChatPage::instance()->isActiveWindow()) {
readEvent(currentId.toStdString());
void
TimelineModel::readEvent(const std::string &id)
{
http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to read_event ({}, {})",
room_id_.toStdString(),
currentId.toStdString());
}
});
}
void
TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
{
std::vector<QString> ids = internalAddEvents(msgs.chunk);
Nicolas Werner
committed
if (!ids.empty()) {
beginInsertRows(QModelIndex(),
static_cast<int>(this->eventOrder.size()),
static_cast<int>(this->eventOrder.size() + ids.size() - 1));
this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
Nicolas Werner
committed
endInsertRows();
}
prev_batch_token_ = QString::fromStdString(msgs.end);
if (ids.empty() && !msgs.chunk.empty()) {
// no visible events fetched, prevent loading from stopping
fetchMore(QModelIndex());
}
QString
TimelineModel::displayName(QString id) const
{
return cache::displayName(room_id_, id).toHtmlEscaped();
QString
TimelineModel::avatarUrl(QString id) const
{
QString
TimelineModel::formatDateSeparator(QDate date) const
{
auto now = QDateTime::currentDateTime();
QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
if (now.date().year() == date.year()) {
QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
fmt = fmt.remove(rx);
}
return date.toString(fmt);
}
QString
TimelineModel::escapeEmoji(QString str) const
{
return utils::replaceEmoji(str);
}
void
TimelineModel::viewRawMessage(QString id) const
{
std::string ev = utils::serialize_event(events.value(id)).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
TimelineModel::viewDecryptedRawMessage(QString id) const
{
auto event = events.value(id);
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
event = decryptEvent(*e).event;
}
std::string ev = utils::serialize_event(event).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
void
TimelineModel::openUserProfile(QString userid) const
{
MainWindow::instance()->openUserProfile(userid, room_id_);
}
DecryptionResult
TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
{
static QCache<std::string, DecryptionResult> decryptedEvents{300};
if (auto cachedEvent = decryptedEvents.object(e.event_id))
return *cachedEvent;
MegolmSessionIndex index;
index.room_id = room_id_.toStdString();
index.session_id = e.content.session_id;
index.sender_key = e.content.sender_key;
mtx::events::RoomEvent<mtx::events::msg::Notice> dummy;
dummy.origin_server_ts = e.origin_server_ts;
dummy.event_id = e.event_id;
dummy.sender = e.sender;
dummy.content.body =
tr("-- Encrypted Event (No keys found for decryption) --",
"Placeholder, when the message was not decrypted yet or can't be decrypted.")
if (!cache::inboundMegolmSessionExists(index)) {
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
index.room_id,
index.session_id,
e.sender);
// TODO: request megolm session_id & session_key from the sender.
decryptedEvents.insert(
dummy.event_id, new DecryptionResult{dummy, false}, 1);
return {dummy, false};
}
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
"Placeholder, when the message can't be decrypted, because "
"the DB access failed when trying to lookup the session.")
.toStdString();
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
return {dummy, false};
}
std::string msg_str;
try {
auto session = cache::getInboundMegolmSession(index);
auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
msg_str = std::string((char *)res.data.data(), res.data.size());
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
index.room_id,
index.session_id,
index.sender_key,
e.what());
dummy.content.body =
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
"Placeholder, when the message can't be decrypted, because the DB access "
"failed.")
.toStdString();
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
return {dummy, false};
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
index.room_id,
index.session_id,
index.sender_key,
e.what());
dummy.content.body =
tr("-- Decryption Error (%1) --",
"Placeholder, when the message can't be decrypted. In this case, the Olm "
"decrytion returned an error, which is passed ad %1.")
.arg(e.what())
.toStdString();
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
return {dummy, false};
}
// Add missing fields for the event.
json body = json::parse(msg_str);
body["event_id"] = e.event_id;
body["sender"] = e.sender;
body["origin_server_ts"] = e.origin_server_ts;
body["unsigned"] = e.unsigned_data;
// relations are unencrypted in content...
if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
json event_array = json::array();
event_array.push_back(body);
std::vector<mtx::events::collections::TimelineEvents> temp_events;
mtx::responses::utils::parse_timeline_events(event_array, temp_events);
if (temp_events.size() == 1) {
decryptedEvents.insert(e.event_id, new DecryptionResult{temp_events[0], true}, 1);
return {temp_events[0], true};
}
dummy.content.body =
tr("-- Encrypted Event (Unknown event type) --",
"Placeholder, when the message was decrypted, but we couldn't parse it, because "
"Nheko/mtxclient don't support that event type yet.")
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
void
TimelineModel::replyAction(QString id)
{
setReply(id);
ChatPage::instance()->focusMessageInput();
}
RelatedInfo
TimelineModel::relatedInfo(QString id)
{
if (!events.contains(id))
return {};
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
event = decryptEvent(*e).event;
}
RelatedInfo related = {};
related.quoted_user = QString::fromStdString(mtx::accessors::sender(event));
related.related_event = mtx::accessors::event_id(event);
related.type = mtx::accessors::msg_type(event);
// get body, strip reply fallback, then transform the event to text, if it is a media event
// etc
related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
while (related.quoted_body.startsWith(">"))
related.quoted_body.remove(plainQuote);
if (related.quoted_body.startsWith("\n"))
related.quoted_body.remove(0, 1);
related.quoted_body = utils::getQuoteBody(related);
// get quoted body and strip reply fallback
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body.remove(QRegularExpression(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
}
void
TimelineModel::readReceiptsAction(QString id) const
{
MainWindow::instance()->openReadReceiptsDialog(id);
void
TimelineModel::redactEvent(QString id)
{
if (!id.isEmpty())
http::client()->redact_event(
room_id_.toStdString(),
id.toStdString(),
[this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit redactionFailed(
tr("Message redaction failed: %1")
.arg(QString::fromStdString(err->matrix_error.error)));
return;
}
emit eventRedacted(id);
});
}
int
TimelineModel::idToIndex(QString id) const
{
if (id.isEmpty())
return -1;
for (int i = 0; i < (int)eventOrder.size(); i++)
if (id == eventOrder[i])
return i;
return -1;
}
QString
TimelineModel::indexToId(int index) const
{
if (index < 0 || index >= (int)eventOrder.size())
return "";
return eventOrder[index];
}
void
TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
{
for (const auto &id : event_ids) {
read.insert(id);
int idx = idToIndex(id);
if (idx < 0) {
nhlog::ui()->warn("Read index out of range");
return;
}
emit dataChanged(index(idx, 0), index(idx, 0));
}
}
void
TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content)
{
const auto room_id = room_id_.toStdString();
using namespace mtx::events;
using namespace mtx::identifiers;
json doc = {{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}};
try {
// Check if we have already an outbound megolm session then we can use.
if (cache::outboundMegolmSessionExists(room_id)) {
auto data =
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
room_id,
txn_id,
data,