summary refs log tree commit diff
path: root/src/timeline/TimelineModel.cpp
diff options
context:
space:
mode:
authorJussi Kuokkanen <jussi.kuokkanen@protonmail.com>2020-08-28 23:35:40 +0300
committerJussi Kuokkanen <jussi.kuokkanen@protonmail.com>2020-08-28 23:35:40 +0300
commit5e344d2685c3efc191d53534cdcdc994c5d9463d (patch)
tree62a1ed20dc3b95a8399323297e7ab1dd93ece6e4 /src/timeline/TimelineModel.cpp
parentadd emoji completer to text input (diff)
parentMerge pull request #265 from trilene/voip (diff)
downloadnheko-5e344d2685c3efc191d53534cdcdc994c5d9463d.tar.xz
Merge branch 'master' of https://github.com/Nheko-Reborn/nheko
Diffstat (limited to 'src/timeline/TimelineModel.cpp')
-rw-r--r--src/timeline/TimelineModel.cpp921
1 files changed, 238 insertions, 683 deletions
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp

index 67e07d7b..b6c2d4bb 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -136,6 +136,11 @@ struct RoomEventType { return qml_mtx_events::EventType::CallHangUp; } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::CallCandidates> &) + { + return qml_mtx_events::EventType::CallCandidates; + } // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return // ::EventType::LocationMessage; } }; @@ -156,70 +161,10 @@ toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) : QAbstractListModel(parent) + , events(room_id.toStdString(), this) , room_id_(room_id) , manager_(manager) { - connect(this, - &TimelineModel::oldMessagesRetrieved, - this, - &TimelineModel::addBackwardsEvents, - Qt::QueuedConnection); - connect( - this, - &TimelineModel::messageFailed, - this, - [this](QString txn_id) { - nhlog::ui()->error("Failed to send {}, retrying", txn_id.toStdString()); - - QTimer::singleShot(5000, this, [this]() { emit nextPendingMessage(); }); - }, - Qt::QueuedConnection); - connect( - this, - &TimelineModel::messageSent, - this, - [this](QString txn_id, QString event_id) { - pending.removeOne(txn_id); - - auto ev = events.value(txn_id); - - if (auto reaction = - std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&ev)) { - QString reactedTo = - QString::fromStdString(reaction->content.relates_to.event_id); - auto &rModel = reactions[reactedTo]; - rModel.removeReaction(*reaction); - auto rCopy = *reaction; - rCopy.event_id = event_id.toStdString(); - rModel.addReaction(room_id_.toStdString(), rCopy); - } - - int idx = idToIndex(txn_id); - if (idx < 0) { - // transaction already received via sync - return; - } - eventOrder[idx] = event_id; - ev = std::visit( - [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(); - }, - Qt::QueuedConnection); connect( this, &TimelineModel::redactionFailed, @@ -228,27 +173,44 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj Qt::QueuedConnection); connect(this, - &TimelineModel::nextPendingMessage, - this, - &TimelineModel::processOnePendingMessage, - Qt::QueuedConnection); - connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage, Qt::QueuedConnection); + connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending); connect( + &events, + &EventStore::dataChanged, this, - &TimelineModel::eventFetched, - 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)); + [this](int from, int to) { + nhlog::ui()->debug( + "data changed {} to {}", events.size() - to - 1, events.size() - from - 1); + emit dataChanged(index(events.size() - to - 1, 0), + index(events.size() - from - 1, 0)); }, Qt::QueuedConnection); + + connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) { + int first = events.size() - to; + int last = events.size() - from; + if (from >= events.size()) { + int batch_size = to - from; + first += batch_size; + last += batch_size; + } else { + first -= 1; + last -= 1; + } + nhlog::ui()->debug("begin insert from {} to {}", first, last); + beginInsertRows(QModelIndex(), first, last); + }); + connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); }); + connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); }); + connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); }); + connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); + connect( + &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); } QHash<int, QByteArray> @@ -290,28 +252,22 @@ int TimelineModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return (int)this->eventOrder.size(); + return this->events.size(); } QVariantMap -TimelineModel::getDump(QString eventId) const +TimelineModel::getDump(QString eventId, QString relatedTo) const { - if (events.contains(eventId)) - return data(eventId, Dump).toMap(); + if (auto event = events.get(eventId.toStdString(), relatedTo.toStdString())) + return data(*event, Dump).toMap(); return {}; } QVariant -TimelineModel::data(const QString &id, int role) const +TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const { using namespace mtx::accessors; - namespace acc = mtx::accessors; - 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; - } + namespace acc = mtx::accessors; switch (role) { case UserId: @@ -397,8 +353,9 @@ TimelineModel::data(const QString &id, int role) const return QVariant(prop > 0 ? prop : 1.); } case Id: - return id; + return QVariant(QString::fromStdString(event_id(event))); case State: { + auto id = QString::fromStdString(event_id(event)); auto containsOthers = [](const auto &vec) { for (const auto &e : vec) if (e.second != http::client()->user_id().to_string()) @@ -409,7 +366,7 @@ TimelineModel::data(const QString &id, int role) const // only show read receipts for messages not from us if (acc::sender(event) != http::client()->user_id().to_string()) return qml_mtx_events::Empty; - else if (pending.contains(id)) + else if (!id.isEmpty() && id[0] == "m") return qml_mtx_events::Sent; else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_))) return qml_mtx_events::Read; @@ -417,19 +374,22 @@ TimelineModel::data(const QString &id, int role) const return qml_mtx_events::Received; } case IsEncrypted: { - return std::holds_alternative< - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]); + auto id = event_id(event); + auto encrypted_event = events.get(id, id, false); + return encrypted_event && + std::holds_alternative< + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + *encrypted_event); } case IsRoomEncrypted: { return cache::isRoomEncrypted(room_id_.toStdString()); } 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 Reactions: { + auto id = event_id(event); + return QVariant::fromValue(events.reactions(id)); + } case RoomId: return QVariant(room_id_); case RoomName: @@ -443,31 +403,32 @@ TimelineModel::data(const QString &id, int role) const 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[IsOnlyEmoji], data(id, static_cast<int>(IsOnlyEmoji))); - 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[IsRoomEncrypted], data(id, static_cast<int>(IsRoomEncrypted))); - 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))); - m.insert(names[CallType], data(id, static_cast<int>(CallType))); + m.insert(names[Type], data(event, static_cast<int>(Type))); + m.insert(names[TypeString], data(event, static_cast<int>(TypeString))); + m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji))); + m.insert(names[Body], data(event, static_cast<int>(Body))); + m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody))); + m.insert(names[UserId], data(event, static_cast<int>(UserId))); + m.insert(names[UserName], data(event, static_cast<int>(UserName))); + m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp))); + m.insert(names[Url], data(event, static_cast<int>(Url))); + m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl))); + m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash))); + m.insert(names[Filename], data(event, static_cast<int>(Filename))); + m.insert(names[Filesize], data(event, static_cast<int>(Filesize))); + m.insert(names[MimeType], data(event, static_cast<int>(MimeType))); + m.insert(names[Height], data(event, static_cast<int>(Height))); + m.insert(names[Width], data(event, static_cast<int>(Width))); + m.insert(names[ProportionalHeight], + data(event, static_cast<int>(ProportionalHeight))); + m.insert(names[Id], data(event, static_cast<int>(Id))); + m.insert(names[State], data(event, static_cast<int>(State))); + m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted))); + m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted))); + m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo))); + m.insert(names[RoomName], data(event, static_cast<int>(RoomName))); + m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic))); + m.insert(names[CallType], data(event, static_cast<int>(CallType))); return QVariant(m); } @@ -481,29 +442,33 @@ 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()) + if (index.row() < 0 && index.row() >= rowCount()) return QVariant(); - QString id = eventOrder[index.row()]; + auto event = events.get(rowCount() - index.row() - 1); - mtx::events::collections::TimelineEvents event = events.value(id); + if (!event) + return ""; if (role == Section) { - QDateTime date = origin_server_ts(event); + QDateTime date = origin_server_ts(*event); date.setTime(QTime()); - std::string userId = acc::sender(event); + std::string userId = acc::sender(*event); + + for (int r = rowCount() - index.row(); r < events.size(); r++) { + auto tempEv = events.get(r); + if (!tempEv) + break; - for (size_t r = index.row() + 1; r < eventOrder.size(); r++) { - auto tempEv = events.value(eventOrder[r]); - QDateTime prevDate = origin_server_ts(tempEv); + 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); + std::string prevUserId = acc::sender(*tempEv); if (userId != prevUserId) break; } @@ -511,16 +476,17 @@ TimelineModel::data(const QModelIndex &index, int role) const return QString("%1").arg(QString::fromStdString(userId)); } - return data(id, role); + return data(*event, role); } bool TimelineModel::canFetchMore(const QModelIndex &) const { - if (eventOrder.empty()) + if (!events.size()) return true; - if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>( - events[eventOrder.back()])) + if (auto first = events.get(0); + first && + !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first)) return true; else @@ -547,50 +513,44 @@ TimelineModel::fetchMore(const QModelIndex &) } 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) { - nhlog::net()->error("failed to call /messages ({}): {} - {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error, - err->parse_error); - setPaginationInProgress(false); - return; - } - - emit oldMessagesRetrieved(std::move(res)); - setPaginationInProgress(false); - }); + events.fetchMore(); } void 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, true); + events.handleSync(timeline); - if (!ids.empty()) { - beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); - endInsertRows(); - } + using namespace mtx::events; + for (auto e : timeline.events) { + if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) { + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = encryptedEvent->content.session_id; + index.sender_key = encryptedEvent->content.sender_key; + + auto result = olm::decryptEvent(index, *encryptedEvent); + if (result.event) + e = result.event.value(); + } - if (!timeline.events.empty()) - updateLastMessage(); + if (std::holds_alternative<RoomEvent<msg::CallCandidates>>(e) || + std::holds_alternative<RoomEvent<msg::CallInvite>>(e) || + std::holds_alternative<RoomEvent<msg::CallAnswer>>(e) || + std::holds_alternative<RoomEvent<msg::CallHangUp>>(e)) + std::visit( + [this](auto &event) { + event.room_id = room_id_.toStdString(); + if (event.sender != http::client()->user_id().to_string()) + emit newCallEvent(event); + }, + e); + } + updateLastMessage(); } template<typename T> @@ -649,21 +609,17 @@ isYourJoin(const mtx::events::Event<T> &) void TimelineModel::updateLastMessage() { - for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) { - auto event = events.value(*it); - if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( - &event)) { - if (decryptDescription) { - event = decryptEvent(*e).event; - } - } + for (auto it = events.size() - 1; it >= 0; --it) { + auto event = events.get(it, decryptDescription); + if (!event) + continue; - if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, event)) { - auto time = mtx::accessors::origin_server_ts(event); + if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { + auto time = mtx::accessors::origin_server_ts(*event); uint64_t ts = time.toMSecsSinceEpoch(); emit manager_->updateRoomsLastMessage( room_id_, - DescInfo{QString::fromStdString(mtx::accessors::event_id(event)), + DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), QString::fromStdString(http::client()->user_id().to_string()), tr("You joined this room."), utils::descriptiveTime(time), @@ -671,191 +627,16 @@ TimelineModel::updateLastMessage() time}); return; } - if (!std::visit([](const auto &e) -> bool { return isMessage(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_); + *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, - bool emitCallEvents) -{ - std::vector<QString> ids; - for (auto e : 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)); - continue; - } - - 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; - } - - if (auto redaction = - 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)); - } - - if (redacted != eventOrder.end()) { - auto redactedEvent = std::visit( - [](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); - events.insert(id, e); - - // remove local echo - if (!txid.isEmpty()) { - auto rCopy = *reaction; - rCopy.event_id = txid.toStdString(); - reactions[reactedTo].removeReaction(rCopy); - } - - 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 - } - - if (auto event = - 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()); - - if (std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(e_)) { - // don't display CallCandidate events to user - events.insert(id, e); - if (emitCallEvents) - emit newCallEvent(e_); - continue; - } - - if (emitCallEvents) { - if (auto callInvite = std::get_if< - mtx::events::RoomEvent<mtx::events::msg::CallInvite>>(&e_)) { - callInvite->room_id = room_id_.toStdString(); - emit newCallEvent(e_); - } else if (std::holds_alternative<mtx::events::RoomEvent< - mtx::events::msg::CallCandidates>>(e_) || - std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>( - e_) || - std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>( - e_)) { - emit newCallEvent(e_); - } - } - } - - if (std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(e)) { - // don't display CallCandidate events to user - events.insert(id, e); - if (emitCallEvents) - emit newCallEvent(e); - continue; - } - - if (emitCallEvents) { - if (auto callInvite = - std::get_if<mtx::events::RoomEvent<mtx::events::msg::CallInvite>>( - &e)) { - callInvite->room_id = room_id_.toStdString(); - emit newCallEvent(e); - } else if (std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>(e) || - std::holds_alternative< - mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>(e)) { - emit newCallEvent(e); - } - } - - 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); - }); - } - } - return ids; -} - void TimelineModel::setCurrentIndex(int index) { @@ -863,7 +644,7 @@ TimelineModel::setCurrentIndex(int index) currentId = indexToId(index); emit currentIndexChanged(index); - if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) && + if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m") && ChatPage::instance()->isActiveWindow()) { readEvent(currentId.toStdString()); } @@ -881,27 +662,6 @@ TimelineModel::readEvent(const std::string &id) }); } -void -TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - std::vector<QString> ids = internalAddEvents(msgs.chunk, false); - - 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()); - 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 { @@ -938,7 +698,10 @@ TimelineModel::escapeEmoji(QString str) const void TimelineModel::viewRawMessage(QString id) const { - std::string ev = mtx::accessors::serialize_event(events.value(id)).dump(4); + auto e = events.get(id.toStdString(), "", false); + if (!e) + return; + std::string ev = mtx::accessors::serialize_event(*e).dump(4); auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); Q_UNUSED(dialog); } @@ -946,13 +709,11 @@ TimelineModel::viewRawMessage(QString id) const void 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; - } + auto e = events.get(id.toStdString(), ""); + if (!e) + return; - std::string ev = mtx::accessors::serialize_event(event).dump(4); + std::string ev = mtx::accessors::serialize_event(*e).dump(4); auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); Q_UNUSED(dialog); } @@ -963,114 +724,6 @@ 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.") - .toStdString(); - - try { - 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.") - .toStdString(); - decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1); - return {dummy, false}; -} - void TimelineModel::replyAction(QString id) { @@ -1081,23 +734,18 @@ TimelineModel::replyAction(QString id) RelatedInfo TimelineModel::relatedInfo(QString id) { - if (!events.contains(id)) + auto event = events.get(id.toStdString(), ""); + if (!event) return {}; - auto event = events.value(id); - 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); + 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)); + related.quoted_body = QString::fromStdString(mtx::accessors::body(*event)); QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption); while (related.quoted_body.startsWith(">")) related.quoted_body.remove(plainQuote); @@ -1106,7 +754,7 @@ TimelineModel::relatedInfo(QString id) 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 = mtx::accessors::formattedBodyWithFallback(*event); related.quoted_formatted_body.remove(QRegularExpression( "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); related.room = room_id_; @@ -1144,18 +792,19 @@ 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; + + auto idx = events.idToIndex(id.toStdString()); + if (idx) + return events.size() - *idx - 1; + else + return -1; } QString TimelineModel::indexToId(int index) const { - if (index < 0 || index >= (int)eventOrder.size()) - return ""; - return eventOrder[index]; + auto id = events.indexToId(events.size() - index - 1); + return id ? QString::fromStdString(*id) : ""; } // Note: this will only be called for our messages @@ -1189,28 +838,16 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, try { // Check if we have already an outbound megolm session then we can use. if (cache::outboundMegolmSessionExists(room_id)) { - auto data = + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + event.event_id = txn_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast<int>(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); + emit this->addPendingMessageToStore(event); return; } @@ -1239,40 +876,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, const auto members = cache::roomMembers(room_id); nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - auto keeper = - std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc); + auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() { + try { + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc); + event.event_id = txn_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - http::client() - ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast<int>(err->status_code); - nhlog::net()->warn( - "[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed( - QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - emit messageFailed(QString::fromStdString(txn_id)); - } - }); + emit this->addPendingMessageToStore(event); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save megolm outbound session: {}", + e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } + }); mtx::requests::QueryKeys req; for (const auto &member : members) @@ -1286,8 +908,8 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, static_cast<int>(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - emit messageFailed(QString::fromStdString(txn_id)); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); return; } @@ -1387,11 +1009,13 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit messageFailed(QString::fromStdString(txn_id)); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); - emit messageFailed(QString::fromStdString(txn_id)); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); } } @@ -1483,13 +1107,12 @@ TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, struct SendMessageVisitor { - SendMessageVisitor(const QString &txn_id, TimelineModel *model) - : txn_id_qstr_(txn_id) - , model_(model) + explicit SendMessageVisitor(TimelineModel *model) + : model_(model) {} template<typename T, mtx::events::EventType Event> - void sendRoomEvent(const mtx::events::RoomEvent<T> &msg) + void sendRoomEvent(mtx::events::RoomEvent<T> msg) { if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { auto encInfo = mtx::accessors::file(msg); @@ -1497,36 +1120,13 @@ struct SendMessageVisitor emit model_->newEncryptedImage(encInfo.value()); model_->sendEncryptedMessageEvent( - txn_id_qstr_.toStdString(), nlohmann::json(msg.content), Event); + msg.event_id, nlohmann::json(msg.content), Event); } else { - sendUnencryptedRoomEvent<T, Event>(msg); + msg.type = Event; + emit model_->addPendingMessageToStore(msg); } } - template<typename T, mtx::events::EventType Event> - void sendUnencryptedRoomEvent(const mtx::events::RoomEvent<T> &msg) - { - QString txn_id_qstr = txn_id_qstr_; - TimelineModel *model = model_; - http::client()->send_room_message<T, Event>( - model->room_id_.toStdString(), - txn_id_qstr.toStdString(), - msg.content, - [txn_id_qstr, model](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast<int>(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id_qstr.toStdString(), - err->matrix_error.error, - status_code); - emit model->messageFailed(txn_id_qstr); - } - emit model->messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); - } - // Do-nothing operator for all unhandled events template<typename T> void operator()(const mtx::events::Event<T> &) @@ -1535,7 +1135,7 @@ struct SendMessageVisitor // Operator for m.room.message events that contain a msgtype in their content template<typename T, std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> - void operator()(const mtx::events::RoomEvent<T> &msg) + void operator()(mtx::events::RoomEvent<T> msg) { sendRoomEvent<T, mtx::events::EventType::RoomMessage>(msg); } @@ -1545,10 +1145,10 @@ struct SendMessageVisitor // reactions need to have the relation outside of ciphertext, or synapse / the homeserver // cannot handle it correctly. See the MSC for more details: // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption - void operator()(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &msg) + void operator()(mtx::events::RoomEvent<mtx::events::msg::Reaction> msg) { - sendUnencryptedRoomEvent<mtx::events::msg::Reaction, - mtx::events::EventType::Reaction>(msg); + msg.type = mtx::events::EventType::Reaction; + emit model_->addPendingMessageToStore(msg); } void operator()(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &event) @@ -1575,65 +1175,38 @@ struct SendMessageVisitor event); } - QString txn_id_qstr_; TimelineModel *model_; }; void -TimelineModel::processOnePendingMessage() -{ - if (pending.isEmpty()) - return; - - QString txn_id_qstr = pending.first(); - - auto event = events.value(txn_id_qstr); - std::visit(SendMessageVisitor{txn_id_qstr, this}, event); -} - -void TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) { std::visit( [](auto &msg) { - msg.event_id = http::client()->generate_txn_id(); + msg.type = mtx::events::EventType::RoomMessage; + msg.event_id = "m" + http::client()->generate_txn_id(); msg.sender = http::client()->user_id().to_string(); msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); }, event); - internalAddEvents({event}, false); - - QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event)); - pending.push_back(txn_id_qstr); - if (!std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&event) && - !std::get_if<mtx::events::RoomEvent<mtx::events::msg::CallCandidates>>(&event)) { - beginInsertRows(QModelIndex(), 0, 0); - this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr); - endInsertRows(); - } - updateLastMessage(); - - emit nextPendingMessage(); + std::visit(SendMessageVisitor{this}, event); } bool TimelineModel::saveMedia(QString eventId) const { - mtx::events::collections::TimelineEvents event = events.value(eventId); - - if (auto e = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { - event = decryptEvent(*e).event; - } + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return false; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(event); + auto encryptionInfo = mtx::accessors::file(*event); - qml_mtx_events::EventType eventType = toRoomEventType(event); + qml_mtx_events::EventType eventType = toRoomEventType(*event); QString dialogTitle; if (eventType == qml_mtx_events::EventType::ImageMessage) { @@ -1698,18 +1271,15 @@ TimelineModel::saveMedia(QString eventId) const void TimelineModel::cacheMedia(QString eventId) { - mtx::events::collections::TimelineEvents event = events.value(eventId); - - if (auto e = - std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { - event = decryptEvent(*e).event; - } + mtx::events::collections::TimelineEvents *event = events.get(eventId.toStdString(), ""); + if (!event) + return; - QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); - QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); - QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + QString mxcUrl = QString::fromStdString(mtx::accessors::url(*event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(*event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(*event)); - auto encryptionInfo = mtx::accessors::file(event); + auto encryptionInfo = mtx::accessors::file(*event); // If the message is a link to a non mxcUrl, don't download it if (!mxcUrl.startsWith("mxc://")) { @@ -1830,11 +1400,11 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg) QString TimelineModel::formatJoinRuleEvent(QString id) { - if (!events.contains(id)) + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) return ""; - auto event = - std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(&events[id]); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(e); if (!event) return ""; @@ -1855,11 +1425,11 @@ TimelineModel::formatJoinRuleEvent(QString id) QString TimelineModel::formatGuestAccessEvent(QString id) { - if (!events.contains(id)) + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) return ""; - auto event = - std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(&events[id]); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(e); if (!event) return ""; @@ -1879,11 +1449,11 @@ TimelineModel::formatGuestAccessEvent(QString id) QString TimelineModel::formatHistoryVisibilityEvent(QString id) { - if (!events.contains(id)) + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) return ""; - auto event = - std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(&events[id]); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(e); if (!event) return ""; @@ -1913,11 +1483,11 @@ TimelineModel::formatHistoryVisibilityEvent(QString id) QString TimelineModel::formatPowerLevelEvent(QString id) { - if (!events.contains(id)) + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) return ""; - auto event = - std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(&events[id]); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); if (!event) return ""; @@ -1931,37 +1501,22 @@ TimelineModel::formatPowerLevelEvent(QString id) QString TimelineModel::formatMemberEvent(QString id) { - if (!events.contains(id)) + mtx::events::collections::TimelineEvents *e = events.get(id.toStdString(), ""); + if (!e) return ""; - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&events[id]); + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); if (!event) return ""; mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; - QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state); - if (!prevEventId.isEmpty()) { - if (!events.contains(prevEventId)) { - http::client()->get_event( - this->room_id_.toStdString(), - event->unsigned_data.replaces_state, - [this, id, prevEventId]( - 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 membership for event {}", - prevEventId.toStdString(), - id.toStdString()); - return; - } - emit eventFetched(id, timeline); - }); - } else { + if (!event->unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = + events.get(event->unsigned_data.replaces_state, event->event_id); + if (tempPrevEvent) { prevEvent = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>( - &events[prevEventId]); + tempPrevEvent); } }