diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 17fe7360..9630ae3a 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -66,6 +66,12 @@ Item { text: qsTr("redacted") } } + DelegateChoice { + roleValue: MtxEvent.Redaction + Pill { + text: qsTr("redacted") + } + } DelegateChoice { roleValue: MtxEvent.Encryption Pill { diff --git a/src/Cache.cpp b/src/Cache.cpp index 173b2c70..233ef2b4 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1241,8 +1241,25 @@ Cache::getTimelineMentions() return notifs; } +std::string +Cache::previousBatchToken(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr); + auto orderDb = getEventOrderDb(txn, room_id); + + auto cursor = lmdb::cursor::open(txn, orderDb); + lmdb::val indexVal, val; + if (!cursor.get(indexVal, val, MDB_FIRST)) { + return ""; + } + + auto j = json::parse(std::string_view(val.data(), val.size())); + + return j.value("prev_batch", ""); +} + Cache::Messages -Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, int64_t index, bool forward) +Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t index, bool forward) { // TODO(nico): Limit the messages returned by this maybe? auto orderDb = getOrderToMessageDb(txn, room_id); @@ -1253,16 +1270,16 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, int64_t i lmdb::val indexVal, event_id; auto cursor = lmdb::cursor::open(txn, orderDb); - if (index == std::numeric_limits::max()) { + if (index == std::numeric_limits::max()) { if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) { - index = *indexVal.data(); + index = *indexVal.data(); } else { messages.end_of_cache = true; return messages; } } else { if (cursor.get(indexVal, event_id, MDB_SET)) { - index = *indexVal.data(); + index = *indexVal.data(); } else { messages.end_of_cache = true; return messages; @@ -1296,7 +1313,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, int64_t i cursor.close(); // std::reverse(timeline.events.begin(), timeline.events.end()); - messages.next_index = *indexVal.data(); + messages.next_index = *indexVal.data(); messages.end_of_cache = !ret; return messages; @@ -1402,16 +1419,16 @@ Cache::getTimelineRange(const std::string &room_id) } TimelineRange range{}; - range.last = *indexVal.data(); + range.last = *indexVal.data(); if (!cursor.get(indexVal, val, MDB_FIRST)) { return {}; } - range.first = *indexVal.data(); + range.first = *indexVal.data(); return range; } -std::optional +std::optional Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) { auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); @@ -1424,11 +1441,11 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id) return {}; } - return *val.data(); + return *val.data(); } std::optional -Cache::getTimelineEventId(const std::string &room_id, int64_t index) +Cache::getTimelineEventId(const std::string &room_id, uint64_t index) { auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); auto orderDb = getOrderToMessageDb(txn, room_id); @@ -2074,6 +2091,9 @@ Cache::saveTimelineMessages(lmdb::txn &txn, const std::string &room_id, const mtx::responses::Timeline &res) { + if (res.events.empty()) + return; + auto eventsDb = getEventsDb(txn, room_id); auto relationsDb = getRelationsDb(txn, room_id); @@ -2090,16 +2110,16 @@ Cache::saveTimelineMessages(lmdb::txn &txn, using namespace mtx::events::state; lmdb::val indexVal, val; - int64_t index = 0; - auto cursor = lmdb::cursor::open(txn, orderDb); + uint64_t index = std::numeric_limits::max() / 2; + auto cursor = lmdb::cursor::open(txn, orderDb); if (cursor.get(indexVal, val, MDB_LAST)) { index = *indexVal.data(); } - int64_t msgIndex = 0; - auto msgCursor = lmdb::cursor::open(txn, order2msgDb); + uint64_t msgIndex = std::numeric_limits::max() / 2; + auto msgCursor = lmdb::cursor::open(txn, order2msgDb); if (msgCursor.get(indexVal, val, MDB_LAST)) { - msgIndex = *indexVal.data(); + msgIndex = *indexVal.data(); } bool first = true; @@ -2111,39 +2131,19 @@ Cache::saveTimelineMessages(lmdb::txn &txn, continue; lmdb::val ev{}; - bool success = - lmdb::dbi_get(txn, eventsDb, lmdb::val(redaction->redacts), ev); - if (!success) - continue; + lmdb::dbi_put( + txn, eventsDb, lmdb::val(redaction->redacts), lmdb::val(event.dump())); + lmdb::dbi_put( + txn, eventsDb, lmdb::val(redaction->event_id), lmdb::val(event.dump())); - mtx::events::collections::TimelineEvent te; - - try { - mtx::events::collections::from_json( - json::parse(std::string_view(ev.data(), ev.size())), te); - } catch (std::exception &e) { - nhlog::db()->error("Failed to parse message from cache {}", - e.what()); - continue; + lmdb::val oldIndex{}; + if (lmdb::dbi_get( + txn, msg2orderDb, lmdb::val(redaction->redacts), oldIndex)) { + lmdb::dbi_put( + txn, order2msgDb, oldIndex, lmdb::val(redaction->event_id)); + lmdb::dbi_put( + txn, msg2orderDb, lmdb::val(redaction->event_id), oldIndex); } - - auto redactedEvent = std::visit( - [](const auto &ev) -> mtx::events::RoomEvent { - mtx::events::RoomEvent 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; - }, - te.data); - - lmdb::dbi_put(txn, - eventsDb, - lmdb::val(redaction->redacts), - lmdb::val(json(redactedEvent).dump())); } else { std::string event_id_val = event["event_id"].get(); lmdb::val event_id = event_id_val; @@ -2193,6 +2193,83 @@ Cache::saveTimelineMessages(lmdb::txn &txn, } } +uint64_t +Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res) +{ + auto txn = lmdb::txn::begin(env_); + auto eventsDb = getEventsDb(txn, room_id); + auto relationsDb = getRelationsDb(txn, room_id); + + auto orderDb = getEventOrderDb(txn, room_id); + auto msg2orderDb = getMessageToOrderDb(txn, room_id); + auto order2msgDb = getOrderToMessageDb(txn, room_id); + + lmdb::val indexVal, val; + uint64_t index = std::numeric_limits::max() / 2; + auto cursor = lmdb::cursor::open(txn, orderDb); + if (cursor.get(indexVal, val, MDB_FIRST)) { + index = *indexVal.data(); + } + + uint64_t msgIndex = std::numeric_limits::max() / 2; + auto msgCursor = lmdb::cursor::open(txn, order2msgDb); + if (msgCursor.get(indexVal, val, MDB_FIRST)) { + msgIndex = *indexVal.data(); + } + + if (res.chunk.empty()) + return index; + + std::string event_id_val; + for (const auto &e : res.chunk) { + auto event = mtx::accessors::serialize_event(e); + event_id_val = event["event_id"].get(); + lmdb::val event_id = event_id_val; + lmdb::dbi_put(txn, eventsDb, event_id, lmdb::val(event.dump())); + + --index; + + json orderEntry = json::object(); + orderEntry["event_id"] = event_id_val; + + nhlog::db()->debug("saving '{}'", orderEntry.dump()); + + lmdb::dbi_put( + txn, orderDb, lmdb::val(&index, sizeof(index)), lmdb::val(orderEntry.dump())); + + // TODO(Nico): Allow blacklisting more event types in UI + if (event["type"] != "m.reaction" && event["type"] != "m.dummy") { + --msgIndex; + lmdb::dbi_put( + txn, order2msgDb, lmdb::val(&msgIndex, sizeof(msgIndex)), event_id); + + lmdb::dbi_put( + txn, msg2orderDb, event_id, lmdb::val(&msgIndex, sizeof(msgIndex))); + } + + if (event.contains("content") && event["content"].contains("m.relates_to")) { + auto temp = event["content"]["m.relates_to"]; + std::string relates_to = temp.contains("m.in_reply_to") + ? temp["m.in_reply_to"]["event_id"] + : temp["event_id"]; + + if (!relates_to.empty()) + lmdb::dbi_put(txn, relationsDb, lmdb::val(relates_to), event_id); + } + } + + json orderEntry = json::object(); + orderEntry["event_id"] = event_id_val; + orderEntry["prev_batch"] = res.end; + lmdb::cursor_put( + cursor.handle(), lmdb::val(&index, sizeof(index)), lmdb::val(orderEntry.dump())); + nhlog::db()->debug("saving '{}'", orderEntry.dump()); + + txn.commit(); + + return msgIndex; +} + mtx::responses::Notifications Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id) { @@ -2337,14 +2414,14 @@ Cache::deleteOldMessages() auto eventsDb = getEventsDb(txn, room_id); auto cursor = lmdb::cursor::open(txn, orderDb); - int64_t first, last; + uint64_t first, last; if (cursor.get(indexVal, val, MDB_LAST)) { - last = *indexVal.data(); + last = *indexVal.data(); } else { continue; } if (cursor.get(indexVal, val, MDB_FIRST)) { - first = *indexVal.data(); + first = *indexVal.data(); } else { continue; } diff --git a/src/Cache_p.h b/src/Cache_p.h index 6b4b260e..1d6d62dd 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -179,8 +179,8 @@ public: }; Messages getTimelineMessages(lmdb::txn &txn, const std::string &room_id, - int64_t index = std::numeric_limits::max(), - bool forward = false); + uint64_t index = std::numeric_limits::max(), + bool forward = false); std::optional getEvent( const std::string &room_id, @@ -190,12 +190,15 @@ public: const mtx::events::collections::TimelineEvent &event); struct TimelineRange { - int64_t first, last; + uint64_t first, last; }; std::optional getTimelineRange(const std::string &room_id); - std::optional getTimelineIndex(const std::string &room_id, - std::string_view event_id); - std::optional getTimelineEventId(const std::string &room_id, int64_t index); + std::optional getTimelineIndex(const std::string &room_id, + std::string_view event_id); + std::optional getTimelineEventId(const std::string &room_id, uint64_t index); + + std::string previousBatchToken(const std::string &room_id); + uint64_t saveOldMessages(const std::string &room_id, const mtx::responses::Messages &res); //! Remove old unused data. void deleteOldMessages(); diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 3ef28c86..666912ee 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -795,43 +795,39 @@ ChatPage::loadStateFromCache() nhlog::db()->info("restoring state from cache"); - QtConcurrent::run([this]() { - try { - cache::restoreSessions(); - olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); + try { + cache::restoreSessions(); + olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - cache::populateMembers(); + cache::populateMembers(); - emit initializeEmptyViews(cache::roomMessages()); - emit initializeRoomList(cache::roomInfo()); - emit initializeMentions(cache::getTimelineMentions()); - emit syncTags(cache::roomInfo().toStdMap()); + emit initializeEmptyViews(cache::roomMessages()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); + emit syncTags(cache::roomInfo().toStdMap()); - cache::calculateRoomReadStatus(); + cache::calculateRoomReadStatus(); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); - emit dropToLoginPageCb( - tr("Failed to restore OLM account. Please login again.")); - return; - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to restore cache: {}", e.what()); - emit dropToLoginPageCb( - tr("Failed to restore save data. Please login again.")); - return; - } catch (const json::exception &e) { - nhlog::db()->critical("failed to parse cache data: {}", e.what()); - return; - } + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore OLM account. Please login again.")); + return; + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to restore cache: {}", e.what()); + emit dropToLoginPageCb(tr("Failed to restore save data. Please login again.")); + return; + } catch (const json::exception &e) { + nhlog::db()->critical("failed to parse cache data: {}", e.what()); + return; + } - nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - getProfileInfo(); + getProfileInfo(); - // Start receiving events. - emit trySyncCb(); - }); + // Start receiving events. + emit trySyncCb(); } void diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 719743fb..7f21e1ed 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -34,12 +34,31 @@ EventStore::EventStore(std::string room_id, QObject *) cache::client()->storeEvent(room_id_, id, {timeline}); if (!relatedTo.empty()) { - auto idx = idToIndex(id); + auto idx = idToIndex(relatedTo); if (idx) emit dataChanged(*idx, *idx); } }, Qt::QueuedConnection); + + connect( + this, + &EventStore::oldMessagesRetrieved, + this, + [this](const mtx::responses::Messages &res) { + // + uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); + if (newFirst == first) + fetchMore(); + else { + emit beginInsertRows(toExternalIdx(newFirst), + toExternalIdx(this->first - 1)); + this->first = newFirst; + emit endInsertRows(); + emit fetchedMore(); + } + }, + Qt::QueuedConnection); } void @@ -49,8 +68,16 @@ EventStore::handleSync(const mtx::responses::Timeline &events) nhlog::db()->warn("{} called from a different thread!", __func__); auto range = cache::client()->getTimelineRange(room_id_); + if (!range) + return; - if (range && range->last > this->last) { + if (events.limited) { + emit beginResetModel(); + this->last = range->last; + this->first = range->first; + emit endResetModel(); + + } else if (range->last > this->last) { emit beginInsertRows(toExternalIdx(this->last + 1), toExternalIdx(range->last)); this->last = range->last; emit endInsertRows(); @@ -290,3 +317,27 @@ EventStore::event(std::string_view id, std::string_view related_to, bool decrypt return event_ptr; } + +void +EventStore::fetchMore() +{ + mtx::http::MessagesOpts opts; + opts.room_id = room_id_; + opts.from = cache::client()->previousBatchToken(room_id_); + + nhlog::ui()->debug("Paginating room {}, token {}", opts.room_id, opts.from); + + 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); + return; + } + + emit oldMessagesRetrieved(std::move(res)); + }); +} diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h index 83c8f7a4..f2997245 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h @@ -8,6 +8,7 @@ #include #include +#include #include class EventStore : public QObject @@ -20,7 +21,7 @@ public: struct Index { std::string room; - int64_t idx; + uint64_t idx; friend uint qHash(const Index &i, uint seed = 0) noexcept { @@ -66,12 +67,12 @@ public: int size() const { - return last != std::numeric_limits::max() + return last != std::numeric_limits::max() ? static_cast(last - first) + 1 : 0; } - int toExternalIdx(int64_t idx) const { return static_cast(idx - first); } - int64_t toInternalIdx(int idx) const { return first + idx; } + int toExternalIdx(uint64_t idx) const { return static_cast(idx - first); } + uint64_t toInternalIdx(int idx) const { return first + idx; } std::optional idToIndex(std::string_view id) const; std::optional indexToId(int idx) const; @@ -79,11 +80,15 @@ public: signals: void beginInsertRows(int from, int to); void endInsertRows(); + void beginResetModel(); + void endResetModel(); void dataChanged(int from, int to); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); void eventFetched(std::string id, std::string relatedTo, mtx::events::collections::TimelineEvents timeline); + void oldMessagesRetrieved(const mtx::responses::Messages &); + void fetchedMore(); private: mtx::events::collections::TimelineEvents *decryptEvent( @@ -92,8 +97,8 @@ private: std::string room_id_; - int64_t first = std::numeric_limits::max(), - last = std::numeric_limits::max(); + uint64_t first = std::numeric_limits::max(), + last = std::numeric_limits::max(); static QCache decryptedEvents_; static QCache events_; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 6df92d7a..60264e86 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -229,20 +229,33 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj &EventStore::dataChanged, this, [this](int from, int to) { - emit dataChanged(index(events.size() - to, 0), index(events.size() - from, 0)); + 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) { - nhlog::ui()->info("begin insert from {} to {}", - events.size() - to + (to - from), - events.size() - from + (to - from)); - beginInsertRows(QModelIndex(), - events.size() - to + (to - from), - events.size() - from + (to - from)); + 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 @@ -512,8 +525,9 @@ TimelineModel::canFetchMore(const QModelIndex &) const { if (!events.size()) return true; - if (!std::holds_alternative>( - *events.event(0))) + if (auto first = events.event(0); + first && + !std::holds_alternative>(*first)) return true; else @@ -540,27 +554,8 @@ 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