summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-04-21 16:34:50 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-04-21 16:35:03 +0300
commit2f00fc51bf27708a9c0ac1ce186043059f93923e (patch)
tree2e65159dd08fd9576b18f7f1570b41b56029ccda /src
parentPopup improvements (diff)
downloadnheko-2f00fc51bf27708a9c0ac1ce186043059f93923e.tar.xz
Cache refactoring
Diffstat (limited to 'src')
-rw-r--r--src/AvatarProvider.cc49
-rw-r--r--src/Cache.cc893
-rw-r--r--src/ChatPage.cc442
-rw-r--r--src/MatrixClient.cc24
-rw-r--r--src/RoomInfoListItem.cc63
-rw-r--r--src/RoomList.cc77
-rw-r--r--src/SuggestionsPopup.cpp15
-rw-r--r--src/TextInputWidget.cc49
-rw-r--r--src/Utils.cc20
-rw-r--r--src/dialogs/ReadReceipts.cc22
-rw-r--r--src/timeline/TimelineItem.cc52
-rw-r--r--src/timeline/TimelineView.cc5
-rw-r--r--src/timeline/TimelineViewManager.cc23
13 files changed, 976 insertions, 758 deletions
diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc

index 5089a128..9f861fdb 100644 --- a/src/AvatarProvider.cc +++ b/src/AvatarProvider.cc
@@ -16,41 +16,33 @@ */ #include "AvatarProvider.h" +#include "Cache.h" #include "MatrixClient.h" QSharedPointer<MatrixClient> AvatarProvider::client_; - -std::map<QString, AvatarData> AvatarProvider::avatars_; - -void -AvatarProvider::init(QSharedPointer<MatrixClient> client) -{ - client_ = client; -} - -void -AvatarProvider::updateAvatar(const QString &uid, const QImage &img) -{ - auto avatarData = &avatars_[uid]; - avatarData->img = img; -} +QHash<QString, QImage> AvatarProvider::avatars_; void -AvatarProvider::resolve(const QString &userId, +AvatarProvider::resolve(const QString &room_id, + const QString &user_id, QObject *receiver, std::function<void(QImage)> callback) { - if (avatars_.find(userId) == avatars_.end()) + const auto key = QString("%1 %2").arg(room_id).arg(user_id); + + if (!Cache::AvatarUrls.contains(key)) return; - auto img = avatars_[userId].img; + if (avatars_.contains(key)) { + auto img = avatars_[key]; - if (!img.isNull()) { - callback(img); - return; + if (!img.isNull()) { + callback(img); + return; + } } - auto proxy = client_->fetchUserAvatar(avatars_[userId].url); + auto proxy = client_->fetchUserAvatar(Cache::avatarUrl(room_id, user_id)); if (proxy.isNull()) return; @@ -58,18 +50,9 @@ AvatarProvider::resolve(const QString &userId, connect(proxy.data(), &DownloadMediaProxy::avatarDownloaded, receiver, - [userId, proxy, callback](const QImage &img) { + [user_id, proxy, callback, key](const QImage &img) { proxy->deleteLater(); - updateAvatar(userId, img); + avatars_.insert(key, img); callback(img); }); } - -void -AvatarProvider::setAvatarUrl(const QString &userId, const QUrl &url) -{ - AvatarData data; - data.url = url; - - avatars_.emplace(userId, data); -} diff --git a/src/Cache.cc b/src/Cache.cc
index 3c8a1bb3..eca13635 100644 --- a/src/Cache.cc +++ b/src/Cache.cc
@@ -20,6 +20,7 @@ #include <QByteArray> #include <QDebug> #include <QFile> +#include <QHash> #include <QStandardPaths> #include <variant.hpp> @@ -27,24 +28,39 @@ #include "Cache.h" #include "RoomState.h" +//! Should be changed when a breaking change occurs in the cache format. +//! This will reset client's data. static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.01.14"); static const lmdb::val NEXT_BATCH_KEY("next_batch"); static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); +//! Cache databases and their format. +//! +//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). +//! Format: room_id -> RoomInfo +static constexpr const char *ROOMS_DB = "rooms"; +static constexpr const char *INVITES_DB = "rooms"; +//! Keeps already downloaded media for reuse. +//! Format: matrix_url -> binary data. +static constexpr const char *MEDIA_DB = "media"; +//! Information that must be kept between sync requests. +static constexpr const char *SYNC_STATE_DB = "sync_state"; +//! Read receipts per room/event. +static constexpr const char *READ_RECEIPTS_DB = "read_receipts"; + using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} , env_{nullptr} - , stateDb_{0} - , roomDb_{0} + , syncStateDb_{0} + , roomsDb_{0} , invitesDb_{0} - , imagesDb_{0} + , mediaDb_{0} , readReceiptsDb_{0} - , isMounted_{false} - , userId_{userId} + , localUserId_{userId} {} void @@ -54,11 +70,11 @@ Cache::setup() auto statePath = QString("%1/%2/state") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(userId_.toUtf8().toHex())); + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); cacheDirectory_ = QString("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(userId_.toUtf8().toHex())); + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); bool isInitial = !QFile::exists(statePath); @@ -97,30 +113,26 @@ Cache::setup() } auto txn = lmdb::txn::begin(env_); - stateDb_ = lmdb::dbi::open(txn, "state", MDB_CREATE); - roomDb_ = lmdb::dbi::open(txn, "rooms", MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, "invites", MDB_CREATE); - imagesDb_ = lmdb::dbi::open(txn, "images", MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, "read_receipts", MDB_CREATE); - + syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); + roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); + invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); + mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); txn.commit(); - isMounted_ = true; + qRegisterMetaType<RoomInfo>(); } void Cache::saveImage(const QString &url, const QByteArray &image) { - if (!isMounted_) - return; - auto key = url.toUtf8(); try { auto txn = lmdb::txn::begin(env_); lmdb::dbi_put(txn, - imagesDb_, + mediaDb_, lmdb::val(key.data(), key.size()), lmdb::val(image.data(), image.size())); @@ -140,7 +152,7 @@ Cache::image(const QString &url) const lmdb::val image; - bool res = lmdb::dbi_get(txn, imagesDb_, lmdb::val(key.data(), key.size()), image); + bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image); txn.commit(); @@ -156,163 +168,37 @@ Cache::image(const QString &url) const } void -Cache::setState(const QString &nextBatchToken, - const std::map<QString, QSharedPointer<RoomState>> &states) +Cache::removeInvite(const std::string &room_id) { - if (!isMounted_) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - setNextBatchToken(txn, nextBatchToken); - - for (auto const &state : states) - insertRoomState(txn, state.first, state.second); - - txn.commit(); - } catch (const lmdb::error &e) { - qCritical() << "The cache couldn't be updated: " << e.what(); - - unmount(); - deleteData(); - } -} - -void -Cache::insertRoomState(lmdb::txn &txn, - const QString &roomid, - const QSharedPointer<RoomState> &state) -{ - auto stateEvents = state->serialize(); - auto id = roomid.toUtf8(); - - lmdb::dbi_put(txn, roomDb_, lmdb::val(id.data(), id.size()), lmdb::val(stateEvents)); - - for (const auto &membership : state->memberships) { - lmdb::dbi membersDb = - lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); - - // The user_id this membership event relates to, is used - // as the index on the membership database. - auto key = membership.second.state_key; - - // Serialize membership event. - nlohmann::json data = membership.second; - std::string memberEvent = data.dump(); - - switch (membership.second.content.membership) { - // We add or update (e.g invite -> join) a new user to the membership - // list. - case mtx::events::state::Membership::Invite: - case mtx::events::state::Membership::Join: { - lmdb::dbi_put(txn, membersDb, lmdb::val(key), lmdb::val(memberEvent)); - break; - } - // We remove the user from the membership list. - case mtx::events::state::Membership::Leave: - case mtx::events::state::Membership::Ban: { - lmdb::dbi_del(txn, membersDb, lmdb::val(key), lmdb::val(memberEvent)); - break; - } - case mtx::events::state::Membership::Knock: { - qWarning() - << "Skipping knock membership" << roomid << QString::fromStdString(key); - break; - } - } - } + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr); + txn.commit(); } void -Cache::removeRoom(const QString &roomid) +Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) { - if (!isMounted_) - return; - - auto txn = lmdb::txn::begin(env_, nullptr, 0); - - lmdb::dbi_del(txn, roomDb_, lmdb::val(roomid.toUtf8(), roomid.toUtf8().size()), nullptr); - - txn.commit(); + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); } void -Cache::removeInvite(const QString &room_id) +Cache::removeRoom(const std::string &roomid) { - if (!isMounted_) - return; - auto txn = lmdb::txn::begin(env_, nullptr, 0); - - lmdb::dbi_del( - txn, invitesDb_, lmdb::val(room_id.toUtf8(), room_id.toUtf8().size()), nullptr); - + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); txn.commit(); } void -Cache::states() +Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) { - std::map<QString, RoomState> states; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, roomDb_); - - std::string room; - std::string stateData; - - // Retrieve all the room names. - while (cursor.get(room, stateData, MDB_NEXT)) { - auto roomid = QString::fromUtf8(room.data(), room.size()); - auto json = nlohmann::json::parse(stateData); - - RoomState state; - state.parse(json); - - auto memberDb = lmdb::dbi::open(txn, roomid.toStdString().c_str(), MDB_CREATE); - std::map<std::string, mtx::events::StateEvent<mtx::events::state::Member>> members; - - auto memberCursor = lmdb::cursor::open(txn, memberDb); - - std::string memberId; - std::string memberContent; - - while (memberCursor.get(memberId, memberContent, MDB_NEXT)) { - auto userid = QString::fromStdString(memberId); - - try { - auto data = nlohmann::json::parse(memberContent); - mtx::events::StateEvent<mtx::events::state::Member> member = data; - members.emplace(memberId, member); - } catch (std::exception &e) { - qWarning() << "Fault while parsing member event" << e.what() - << QString::fromStdString(memberContent); - continue; - } - } - - qDebug() << members.size() << "members for" << roomid; - - state.memberships = members; - states.emplace(roomid, std::move(state)); - } - - qDebug() << "Retrieved" << states.size() << "rooms"; - - cursor.close(); - - txn.commit(); - - emit statesLoaded(states); + lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size())); } void Cache::setNextBatchToken(lmdb::txn &txn, const QString &token) { - auto value = token.toUtf8(); - - lmdb::dbi_put(txn, stateDb_, NEXT_BATCH_KEY, lmdb::val(value.data(), value.size())); + setNextBatchToken(txn, token.toStdString()); } bool @@ -321,7 +207,7 @@ Cache::isInitialized() const auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val token; - bool res = lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); txn.commit(); @@ -334,7 +220,7 @@ Cache::nextBatchToken() const auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val token; - lmdb::dbi_get(txn, stateDb_, NEXT_BATCH_KEY, token); + lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); txn.commit(); @@ -356,7 +242,7 @@ Cache::isFormatValid() auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val current_version; - bool res = lmdb::dbi_get(txn, stateDb_, CACHE_FORMAT_VERSION_KEY, current_version); + bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version); txn.commit(); @@ -381,69 +267,13 @@ Cache::setCurrentFormat() lmdb::dbi_put( txn, - stateDb_, + syncStateDb_, CACHE_FORMAT_VERSION_KEY, lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size())); txn.commit(); } -std::map<std::string, mtx::responses::InvitedRoom> -Cache::invites() -{ - std::map<std::string, mtx::responses::InvitedRoom> invites; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, invitesDb_); - - std::string room_id; - std::string payload; - - mtx::responses::InvitedRoom state; - - while (cursor.get(room_id, payload, MDB_NEXT)) { - state = nlohmann::json::parse(payload); - invites.emplace(room_id, state); - } - - if (invites.size() > 0) - qDebug() << "Retrieved" << invites.size() << "invites"; - - cursor.close(); - - txn.commit(); - - return invites; -} - -void -Cache::setInvites(const std::map<std::string, mtx::responses::InvitedRoom> &invites) -{ - if (!isMounted_) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - for (auto it = invites.cbegin(); it != invites.cend(); ++it) { - nlohmann::json j; - - for (const auto &e : it->second.invite_state) { - mpark::visit( - [&j](auto msg) { j["invite_state"]["events"].push_back(msg); }, - e); - } - - lmdb::dbi_put(txn, invitesDb_, lmdb::val(it->first), lmdb::val(j.dump())); - } - - txn.commit(); - } catch (const lmdb::error &e) { - qCritical() << "setInvitedRooms: " << e.what(); - unmount(); - } -} - CachedReceipts Cache::readReceipts(const QString &event_id, const QString &room_id) { @@ -533,3 +363,634 @@ Cache::updateReadReceipt(const std::string &room_id, const Receipts &receipts) } } } + +void +Cache::saveState(const mtx::responses::Sync &res) +{ + auto txn = lmdb::txn::begin(env_); + + setNextBatchToken(txn, res.next_batch); + + // Save joined rooms + for (const auto &room : res.rooms.join) { + auto statesdb = getStatesDb(txn, room.first); + auto membersdb = getMembersDb(txn, room.first); + + saveStateEvents(txn, statesdb, membersdb, room.first, room.second.state.events); + saveStateEvents(txn, statesdb, membersdb, room.first, room.second.timeline.events); + + RoomInfo updatedInfo; + updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = + getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first)) + .toStdString(); + + lmdb::dbi_put( + txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + } + + saveInvites(txn, res.rooms.invite); + + std::map<std::string, bool> invites; + for (const auto &invite : res.rooms.invite) + invites.emplace(std::move(invite.first), true); + + // removeStaleInvites(txn, invites); + + removeLeftRooms(txn, res.rooms.leave); + + txn.commit(); +} + +void +Cache::removeStaleInvites(lmdb::txn &txn, const std::map<std::string, bool> &curr) +{ + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + + std::string room_id, room_data; + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + if (curr.find(room_id) == curr.end()) + lmdb::cursor_del(invitesCursor); + } + + invitesCursor.close(); +} + +void +Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::InvitedRoom> &rooms) +{ + for (const auto &room : rooms) { + auto statesdb = getInviteStatesDb(txn, room.first); + auto membersdb = getInviteMembersDb(txn, room.first); + + saveInvite(txn, statesdb, membersdb, room.second); + + RoomInfo updatedInfo; + updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = + getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.is_invite = true; + + lmdb::dbi_put( + txn, invitesDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + } +} + +void +Cache::saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto &e : room.invite_state) { + if (mpark::holds_alternative<StrippedEvent<Member>>(e)) { + auto msg = mpark::get<StrippedEvent<Member>>(e); + + auto display_name = msg.content.display_name.empty() + ? msg.state_key + : msg.content.display_name; + + MemberInfo tmp{display_name, msg.content.avatar_url}; + + lmdb::dbi_put( + txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); + } else { + mpark::visit( + [&txn, &statesdb](auto msg) { + bool res = lmdb::dbi_put(txn, + statesdb, + lmdb::val(to_string(msg.type)), + lmdb::val(json(msg).dump())); + + if (!res) + std::cout << "couldn't save data" << json(msg).dump() + << '\n'; + }, + e); + } + } +} + +std::vector<std::string> +Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + std::vector<std::string> rooms; + for (const auto &room : res.rooms.join) { + bool hasUpdates = false; + for (const auto &s : room.second.state.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + for (const auto &s : room.second.timeline.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + if (hasUpdates) + rooms.emplace_back(room.first); + } + + for (const auto &room : res.rooms.invite) { + for (const auto &s : room.second.invite_state) { + if (containsStateUpdates(s)) { + rooms.emplace_back(room.first); + break; + } + } + } + + return rooms; +} + +std::map<QString, RoomInfo> +Cache::getRoomInfo(const std::vector<std::string> &rooms) +{ + std::map<QString, RoomInfo> room_info; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + for (const auto &room : rooms) { + lmdb::val data; + + // Check if the room is joined. + if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) { + try { + room_info.emplace( + QString::fromStdString(room), + json::parse(std::string(data.data(), data.size()))); + } catch (const json::exception &e) { + qWarning() + << "failed to parse room info:" << QString::fromStdString(room) + << QString::fromStdString(std::string(data.data(), data.size())); + } + } else { + // Check if the room is an invite. + if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) { + try { + room_info.emplace( + QString::fromStdString(room), + json::parse(std::string(data.data(), data.size()))); + } catch (const json::exception &e) { + qWarning() << "failed to parse room info for invite:" + << QString::fromStdString(room) + << QString::fromStdString( + std::string(data.data(), data.size())); + } + } + } + } + + txn.commit(); + + return room_info; +} + +QMap<QString, RoomInfo> +Cache::roomInfo() +{ + QMap<QString, RoomInfo> result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + + std::string room_id; + std::string room_data; + + // Gather info about the joined rooms. + while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + + // Gather info about the invites. + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + + invitesCursor.close(); + roomsCursor.close(); + + txn.commit(); + + return result; +} + +QString +Cache::getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StateEvent<Avatar> msg = + json::parse(std::string(event.data(), event.size())); + + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + // We don't use an avatar for group chats. + if (membersdb.size(txn) > 2) + return QString(); + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id; + std::string member_data; + + // Resolve avatar for 1-1 chats. + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo m = json::parse(member_data); + + cursor.close(); + return QString::fromStdString(m.avatar_url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + // Default case when there is only one member. + return avatarUrl(room_id, localUserId_); +} + +QString +Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StateEvent<Name> msg = json::parse(std::string(event.data(), event.size())); + + if (!msg.content.name.empty()) + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event); + + if (res) { + try { + StateEvent<CanonicalAlias> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.alias.empty()) + return QString::fromStdString(msg.content.alias); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + const int total = membersdb.size(txn); + + std::size_t ii = 0; + std::string user_id; + std::string member_data; + std::map<std::string, MemberInfo> members; + + while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { + try { + members.emplace(user_id, json::parse(member_data)); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + + ii++; + } + + cursor.close(); + + if (total == 1 && !members.empty()) + return QString::fromStdString(members.begin()->second.name); + + auto first_member = [&members, this]() { + for (const auto &m : members) { + if (m.first != localUserId_.toStdString()) + return QString::fromStdString(m.second.name); + } + + return localUserId_; + }(); + + if (total == 2) + return first_member; + else if (total > 2) + return QString("%1 and %2 others").arg(first_member).arg(total); + + return "Empty Room"; +} + +QString +Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StateEvent<Topic> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.topic.empty()) + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + return QString(); +} + +QString +Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StrippedEvent<state::Name> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.name); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + return QString("Empty Room"); +} + +QString +Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StrippedEvent<state::Avatar> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.avatar_url); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + cursor.close(); + + return QString(); +} + +QString +Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = + lmdb::dbi_get(txn, db, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StrippedEvent<Topic> msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + qWarning() << QString::fromStdString(e.what()); + } + } + + return QString(); +} + +std::vector<std::string> +Cache::joinedRooms() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + + std::string id, data; + std::vector<std::string> room_ids; + + // Gather the room ids for the joined rooms. + while (roomsCursor.get(id, data, MDB_NEXT)) + room_ids.emplace_back(id); + + roomsCursor.close(); + txn.commit(); + + return room_ids; +} + +void +Cache::populateMembers() +{ + auto rooms = joinedRooms(); + qDebug() << "loading" << rooms.size() << "rooms"; + + auto txn = lmdb::txn::begin(env_); + + for (const auto &room : rooms) { + const auto roomid = QString::fromStdString(room); + + auto membersdb = getMembersDb(txn, room); + auto cursor = lmdb::cursor::open(txn, membersdb); + + std::string user_id, info; + while (cursor.get(user_id, info, MDB_NEXT)) { + MemberInfo m = json::parse(info); + + const auto userid = QString::fromStdString(user_id); + + insertDisplayName(roomid, userid, QString::fromStdString(m.name)); + insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url)); + } + + cursor.close(); + } + + txn.commit(); +} + +QVector<SearchResult> +Cache::getAutocompleteMatches(const std::string &room_id, + const std::string &query, + std::uint8_t max_items) +{ + std::multimap<int, std::pair<std::string, std::string>> items; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, getMembersDb(txn, room_id)); + + std::string user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + const auto display_name = displayName(room_id, user_id); + const int score = utils::levenshtein_distance(query, display_name); + + items.emplace(score, std::make_pair(user_id, display_name)); + } + + auto end = items.begin(); + + if (items.size() >= max_items) + std::advance(end, max_items); + else if (items.size() > 0) + std::advance(end, items.size()); + + QVector<SearchResult> results; + for (auto it = items.begin(); it != end; it++) { + const auto user = it->second; + results.push_back(SearchResult{QString::fromStdString(user.first), + QString::fromStdString(user.second)}); + } + + return results; +} + +QHash<QString, QString> Cache::DisplayNames; +QHash<QString, QString> Cache::AvatarUrls; + +QString +Cache::displayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt]; + + return user_id; +} + +std::string +Cache::displayName(const std::string &room_id, const std::string &user_id) +{ + auto fmt = QString::fromStdString(room_id + " " + user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt].toStdString(); + + return user_id; +} + +QString +Cache::avatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (AvatarUrls.contains(fmt)) + return AvatarUrls[fmt]; + + return QString(); +} + +void +Cache::insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.insert(fmt, display_name); +} + +void +Cache::removeDisplayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.remove(fmt); +} + +void +Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.insert(fmt, avatar_url); +} + +void +Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.remove(fmt); +} diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 8b8b4438..aa21a98e 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc
@@ -28,7 +28,6 @@ #include "OverlayModal.h" #include "QuickSwitcher.h" #include "RoomList.h" -#include "RoomSettings.h" #include "RoomState.h" #include "SideBarActions.h" #include "Splitter.h" @@ -158,20 +157,21 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, typingDisplay_->setUsers(users); }); connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); - connect(room_list_, &RoomList::roomChanged, text_input_, [this](const QString &room_id) { - if (roomStates_.find(room_id) != roomStates_.end()) - text_input_->setRoomState(roomStates_[room_id]); - else - qWarning() << "no state found for room_id" << room_id; - }); - connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit); connect( room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); - connect(room_list_, &RoomList::acceptInvite, client_.data(), &MatrixClient::joinRoom); - connect(room_list_, &RoomList::declineInvite, client_.data(), &MatrixClient::leaveRoom); + connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { + view_manager_->addRoom(room_id); + client_->joinRoom(room_id); + room_list_->removeRoom(room_id, currentRoom() == room_id); + }); + + connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { + client_->leaveRoom(room_id); + room_list_->removeRoom(room_id, currentRoom() == room_id); + }); connect(text_input_, &TextInputWidget::startedTyping, this, [this]() { if (!userSettings_->isTypingNotificationsEnabled()) @@ -329,15 +329,22 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, connect(client_.data(), &MatrixClient::joinedRoom, this, [this](const QString &room_id) { emit showNotification("You joined the room."); - removeInvite(room_id); + + // We remove any invites with the same room_id. + try { + cache_->removeInvite(room_id.toStdString()); + } catch (const lmdb::error &e) { + emit showNotification(QString("Failed to remove invite: %1") + .arg(QString::fromStdString(e.what()))); + } }); + connect(client_.data(), &MatrixClient::leftRoom, this, &ChatPage::removeRoom); connect(client_.data(), &MatrixClient::invitedUser, this, [this](QString, QString user) { emit showNotification(QString("Invited user %1").arg(user)); }); connect(client_.data(), &MatrixClient::roomCreated, this, [this](QString room_id) { 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)); }); @@ -394,7 +401,38 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, AvatarProvider::init(client); + connect(this, &ChatPage::continueSync, this, [this](const QString &next_batch) { + syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT); + client_->setNextBatchToken(next_batch); + client_->sync(); + }); + + connect(this, &ChatPage::startConsesusTimer, this, [this]() { + consensusTimer_->start(CONSENSUS_TIMEOUT); + showContentTimer_->start(SHOW_CONTENT_TIMEOUT); + }); + connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize); + connect(this, + &ChatPage::initializeViews, + view_manager_, + [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + connect( + this, + &ChatPage::initializeEmptyViews, + this, + [this](const std::vector<std::string> &rooms) { view_manager_->initialize(rooms); }); + connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { + view_manager_->initialize(rooms); + removeLeftRooms(rooms.leave); + }); + connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); + instance_ = this; + + qRegisterMetaType<std::map<QString, RoomInfo>>(); + qRegisterMetaType<QMap<QString, RoomInfo>>(); + qRegisterMetaType<mtx::responses::Rooms>(); + qRegisterMetaType<std::vector<std::string>>(); } void @@ -412,8 +450,6 @@ ChatPage::resetUI() { roomAvatars_.clear(); room_list_->clear(); - roomSettings_.clear(); - roomStates_.clear(); top_bar_->reset(); user_info_widget_->reset(); view_manager_->clearAll(); @@ -451,6 +487,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) cache_ = QSharedPointer<Cache>(new Cache(userid)); room_list_->setCache(cache_); + text_input_->setCache(cache_); try { cache_->setup(); @@ -467,7 +504,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } } catch (const lmdb::error &e) { qCritical() << "Cache failure" << e.what(); - cache_->unmount(); cache_->deleteData(); qInfo() << "Falling back to initial sync ..."; } @@ -482,25 +518,28 @@ ChatPage::syncCompleted(const mtx::responses::Sync &response) { syncTimeoutTimer_->stop(); - updateJoinedRooms(response.rooms.join); - removeLeftRooms(response.rooms.leave); - - const auto nextBatchToken = QString::fromStdString(response.next_batch); - - auto stateDiff = generateMembershipDifference(response.rooms.join, roomStates_); - QtConcurrent::run(cache_.data(), &Cache::setState, nextBatchToken, stateDiff); - QtConcurrent::run(cache_.data(), &Cache::setInvites, response.rooms.invite); + // Process ephemeral data per room. + for (const auto &room : response.rooms.join) { + auto room_id = QString::fromStdString(room.first); - room_list_->sync(roomStates_, roomSettings_); - room_list_->syncInvites(response.rooms.invite); - trackInvites(response.rooms.invite); - - view_manager_->sync(response.rooms); + updateTypingUsers(room_id, room.second.ephemeral.typing); + updateRoomNotificationCount(room_id, + room.second.unread_notifications.notification_count); + } - client_->setNextBatchToken(nextBatchToken); - client_->sync(); + QtConcurrent::run([this, res = std::move(response)]() { + try { + cache_->saveState(res); + emit syncRoomlist(cache_->roomUpdates(res)); + } catch (const lmdb::error &e) { + std::cout << "save cache error:" << e.what() << '\n'; + // TODO: retry sync. + return; + } - syncTimeoutTimer_->start(SYNC_RETRY_TIMEOUT); + emit syncUI(std::move(res.rooms)); + emit continueSync(cache_->nextBatchToken()); + }); } void @@ -508,57 +547,22 @@ ChatPage::initialSyncCompleted(const mtx::responses::Sync &response) { initialSyncTimer_->stop(); - auto joined = response.rooms.join; - - for (auto it = joined.cbegin(); it != joined.cend(); ++it) { - auto roomState = QSharedPointer<RoomState>(new RoomState); - - // Build the current state from the timeline and state events. - roomState->updateFromEvents(it->second.state.events); - roomState->updateFromEvents(it->second.timeline.events); - - // Remove redundant memberships. - roomState->removeLeaveMemberships(); - - // Resolve room name and avatar. e.g in case of one-to-one chats. - roomState->resolveName(); - roomState->resolveAvatar(); + qDebug() << "initial sync completed"; - const auto room_id = QString::fromStdString(it->first); - - roomStates_.emplace(room_id, roomState); - roomSettings_.emplace(room_id, - QSharedPointer<RoomSettings>(new RoomSettings(room_id))); - - for (const auto &membership : roomState->memberships) { - updateUserDisplayName(membership.second); - updateUserAvatarUrl(membership.second); + QtConcurrent::run([this, res = std::move(response)]() { + try { + cache_->saveState(res); + emit initializeRoomList(cache_->roomInfo()); + } catch (const lmdb::error &e) { + qWarning() << "cache error:" << QString::fromStdString(e.what()); + emit retryInitialSync(); + return; } - QApplication::processEvents(); - } - - QtConcurrent::run(cache_.data(), - &Cache::setState, - QString::fromStdString(response.next_batch), - roomStates_); - QtConcurrent::run(cache_.data(), &Cache::setInvites, response.rooms.invite); - - // Create timelines - view_manager_->initialize(response.rooms); - - // Initialize room list. - room_list_->setInitialRooms(roomSettings_, roomStates_); - room_list_->syncInvites(response.rooms.invite); - trackInvites(response.rooms.invite); - - client_->setNextBatchToken(QString::fromStdString(response.next_batch)); - client_->sync(); - - // Add messages - view_manager_->sync(response.rooms); - - emit contentLoaded(); + emit initializeViews(std::move(res.rooms)); + emit continueSync(cache_->nextBatchToken()); + emit contentLoaded(); + }); } void @@ -613,19 +617,26 @@ ChatPage::updateOwnCommunitiesInfo(const QList<QString> &own_communities) void ChatPage::changeTopRoomInfo(const QString &room_id) { - if (roomStates_.find(room_id) == roomStates_.end()) - return; + try { + auto room_info = cache_->getRoomInfo({room_id.toStdString()}); - auto state = roomStates_[room_id]; + if (room_info.find(room_id) == room_info.end()) + return; - top_bar_->updateRoomName(state->getName()); - top_bar_->updateRoomTopic(state->getTopic()); - top_bar_->setRoomSettings(roomSettings_[room_id]); + const auto name = QString::fromStdString(room_info[room_id].name); + const auto avatar_url = QString::fromStdString(room_info[room_id].avatar_url); - if (roomAvatars_.find(room_id) != roomAvatars_.end()) - top_bar_->updateRoomAvatar(roomAvatars_[room_id].toImage()); - else - top_bar_->updateRoomAvatarFromName(state->getName()); + top_bar_->updateRoomName(name); + top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); + + if (roomAvatars_.find(room_id) != roomAvatars_.end()) + top_bar_->updateRoomAvatar(roomAvatars_[room_id].toImage()); + else + top_bar_->updateRoomAvatarFromName(name); + } catch (const lmdb::error &e) { + qWarning() << "failed to change top bar room info" + << QString::fromStdString(e.what()); + } current_room_ = room_id; } @@ -645,64 +656,26 @@ ChatPage::showUnreadMessageNotification(int count) void ChatPage::loadStateFromCache() { - qDebug() << "Restoring state from cache"; - - qDebug() << "Restored nextBatchToken" << cache_->nextBatchToken(); - client_->setNextBatchToken(cache_->nextBatchToken()); + qDebug() << "restoring state from cache"; - qRegisterMetaType<std::map<QString, RoomState>>(); + QtConcurrent::run([this]() { + try { + cache_->populateMembers(); - QtConcurrent::run(cache_.data(), &Cache::states); - - connect( - cache_.data(), &Cache::statesLoaded, this, [this](std::map<QString, RoomState> rooms) { - qDebug() << "Cache data loaded"; - - std::vector<QString> roomKeys; - - for (auto const &room : rooms) { - auto roomState = QSharedPointer<RoomState>(new RoomState(room.second)); - - // Clean up and prepare state for use. - roomState->removeLeaveMemberships(); - roomState->resolveName(); - roomState->resolveAvatar(); - - // Save the current room state. - roomStates_.emplace(room.first, roomState); - - // Create or restore the settings for this room. - roomSettings_.emplace( - room.first, QSharedPointer<RoomSettings>(new RoomSettings(room.first))); - - // Resolve user avatars. - for (auto const &membership : roomState->memberships) { - updateUserDisplayName(membership.second); - updateUserAvatarUrl(membership.second); - } - - roomKeys.emplace_back(room.first); - } - - // Initializing empty timelines. - view_manager_->initialize(roomKeys); - - // Initialize room list from the restored state and settings. - room_list_->setInitialRooms(roomSettings_, roomStates_); - - const auto invites = cache_->invites(); - room_list_->syncInvites(invites); - trackInvites(invites); - - // Check periodically if the timelines have been loaded. - consensusTimer_->start(CONSENSUS_TIMEOUT); + emit initializeRoomList(cache_->roomInfo()); + emit initializeEmptyViews(cache_->joinedRooms()); + } catch (const lmdb::error &e) { + std::cout << "load cache error:" << e.what() << '\n'; + // TODO Clear cache and restart. + return; + } - // Show the content if consensus can't be achieved. - showContentTimer_->start(SHOW_CONTENT_TIMEOUT); + // Start receiving events. + emit continueSync(cache_->nextBatchToken()); - // Start receiving events. - client_->sync(); - }); + // Check periodically if the timelines have been loaded. + emit startConsesusTimer(); + }); } void @@ -734,69 +707,30 @@ ChatPage::showQuickSwitcher() std::map<QString, QString> rooms; - for (auto const &state : roomStates_) { - QString deambiguator = - QString::fromStdString(state.second->canonical_alias.content.alias); - if (deambiguator == "") - deambiguator = state.first; - rooms.emplace(state.second->getName() + " (" + deambiguator + ")", state.first); - } - - quickSwitcher_->setRoomList(rooms); - quickSwitcherModal_->show(); -} - -void -ChatPage::addRoom(const QString &room_id) -{ - if (roomStates_.find(room_id) == roomStates_.end()) { - auto room_state = QSharedPointer<RoomState>(new RoomState); - - roomStates_.emplace(room_id, room_state); - roomSettings_.emplace(room_id, - QSharedPointer<RoomSettings>(new RoomSettings(room_id))); - - room_list_->addRoom(roomSettings_[room_id], roomStates_[room_id], room_id); - room_list_->highlightSelectedRoom(room_id); - - changeTopRoomInfo(room_id); - } -} - -void -ChatPage::removeRoom(const QString &room_id) -{ - roomStates_.erase(room_id); - roomSettings_.erase(room_id); - try { - cache_->removeRoom(room_id); - cache_->removeInvite(room_id); + auto info = cache_->roomInfo(); + for (auto it = info.begin(); it != info.end(); ++it) + rooms.emplace(QString::fromStdString(it.value().name), it.key()); + quickSwitcher_->setRoomList(rooms); + quickSwitcherModal_->show(); } catch (const lmdb::error &e) { - qCritical() << "The cache couldn't be updated: " << e.what(); - // TODO: Notify the user. - cache_->unmount(); - cache_->deleteData(); + const auto err = QString::fromStdString(e.what()); + emit showNotification(QString("Failed to load room list: %1").arg(err)); } - - room_list_->removeRoom(room_id, room_id == current_room_); - roomInvites_.erase(room_id); } void -ChatPage::removeInvite(const QString &room_id) +ChatPage::removeRoom(const QString &room_id) { try { - cache_->removeInvite(room_id); + cache_->removeRoom(room_id); + cache_->removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { qCritical() << "The cache couldn't be updated: " << e.what(); // TODO: Notify the user. - cache_->unmount(); - cache_->deleteData(); } room_list_->removeRoom(room_id, room_id == current_room_); - roomInvites_.erase(room_id); } void @@ -821,7 +755,7 @@ ChatPage::updateTypingUsers(const QString &roomid, const std::vector<std::string if (user == user_id) continue; - users.append(TimelineViewManager::displayName(user)); + users.append(Cache::displayName(roomid, user)); } users.sort(); @@ -834,135 +768,15 @@ ChatPage::updateTypingUsers(const QString &roomid, const std::vector<std::string } void -ChatPage::updateUserAvatarUrl(const mtx::events::StateEvent<mtx::events::state::Member> &membership) -{ - auto uid = QString::fromStdString(membership.state_key); - auto url = QString::fromStdString(membership.content.avatar_url); - - if (!url.isEmpty()) - AvatarProvider::setAvatarUrl(uid, url); -} - -void -ChatPage::updateUserDisplayName( - const mtx::events::StateEvent<mtx::events::state::Member> &membership) -{ - auto displayName = QString::fromStdString(membership.content.display_name); - auto stateKey = QString::fromStdString(membership.state_key); - - if (!displayName.isEmpty()) - TimelineViewManager::DISPLAY_NAMES.emplace(stateKey, displayName); -} - -void ChatPage::removeLeftRooms(const std::map<std::string, mtx::responses::LeftRoom> &rooms) { for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { const auto room_id = QString::fromStdString(it->first); - - if (roomStates_.find(room_id) != roomStates_.end()) - removeRoom(room_id); - - if (roomInvites_.find(room_id) != roomInvites_.end()) - removeInvite(room_id); + room_list_->removeRoom(room_id, room_id == current_room_); } } void -ChatPage::updateJoinedRooms(const std::map<std::string, mtx::responses::JoinedRoom> &rooms) -{ - for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { - const auto roomid = QString::fromStdString(it->first); - - if (roomInvites_.find(roomid) != roomInvites_.end()) - removeInvite(roomid); - - updateTypingUsers(roomid, it->second.ephemeral.typing); - updateRoomNotificationCount(roomid, - it->second.unread_notifications.notification_count); - - if (it->second.ephemeral.receipts.size() > 0) - QtConcurrent::run(cache_.data(), - &Cache::updateReadReceipt, - it->first, - it->second.ephemeral.receipts); - - const auto newStateEvents = it->second.state; - const auto newTimelineEvents = it->second.timeline; - - // Merge the new updates for rooms that we are tracking. - if (roomStates_.find(roomid) != roomStates_.end()) { - auto oldState = roomStates_[roomid]; - oldState->updateFromEvents(newStateEvents.events); - oldState->updateFromEvents(newTimelineEvents.events); - oldState->resolveName(); - oldState->resolveAvatar(); - } else { - // Build the current state from the timeline and state events. - auto roomState = QSharedPointer<RoomState>(new RoomState); - roomState->updateFromEvents(newStateEvents.events); - roomState->updateFromEvents(newTimelineEvents.events); - - // Resolve room name and avatar. e.g in case of one-to-one chats. - roomState->resolveName(); - roomState->resolveAvatar(); - - roomStates_.emplace(roomid, roomState); - - roomSettings_.emplace( - roomid, QSharedPointer<RoomSettings>(new RoomSettings(roomid))); - - view_manager_->addRoom(it->second, roomid); - } - - updateUserMetadata(newStateEvents.events); - updateUserMetadata(newTimelineEvents.events); - - if (roomid == current_room_) - changeTopRoomInfo(roomid); - - QApplication::processEvents(); - } -} - -std::map<QString, QSharedPointer<RoomState>> -ChatPage::generateMembershipDifference( - const std::map<std::string, mtx::responses::JoinedRoom> &rooms, - const std::map<QString, QSharedPointer<RoomState>> &states) const -{ - std::map<QString, QSharedPointer<RoomState>> stateDiff; - - for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { - const auto room_id = QString::fromStdString(it->first); - - if (states.find(room_id) == states.end()) - continue; - - auto all_memberships = getMemberships(it->second.state.events); - auto timelineMemberships = getMemberships(it->second.timeline.events); - - // We have to process first the state events and then the timeline. - for (auto mm = timelineMemberships.cbegin(); mm != timelineMemberships.cend(); ++mm) - all_memberships.emplace(mm->first, mm->second); - - auto local = QSharedPointer<RoomState>(new RoomState); - local->aliases = states.at(room_id)->aliases; - local->avatar = states.at(room_id)->avatar; - local->canonical_alias = states.at(room_id)->canonical_alias; - local->history_visibility = states.at(room_id)->history_visibility; - local->join_rules = states.at(room_id)->join_rules; - local->name = states.at(room_id)->name; - local->power_levels = states.at(room_id)->power_levels; - local->topic = states.at(room_id)->topic; - local->memberships = all_memberships; - - stateDiff.emplace(room_id, local); - } - - return stateDiff; -} - -void ChatPage::showReadReceipts(const QString &event_id) { if (receiptsDialog_.isNull()) { diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index e7d04ebb..17a34d96 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc
@@ -28,6 +28,7 @@ #include <QProcessEnvironment> #include <QSettings> #include <QUrlQuery> +#include <QtConcurrent> #include <mtx/errors.hpp> #include "Deserializable.h" @@ -438,16 +439,15 @@ MatrixClient::initialSync() noexcept return; } - auto data = reply->readAll(); - - try { - mtx::responses::Sync response = nlohmann::json::parse(data); - emit initialSyncCompleted(response); - } catch (std::exception &e) { - qWarning() << "Initial sync error:" << e.what(); - emit initialSyncFailed(); - return; - } + qRegisterMetaType<mtx::responses::Sync>(); + QtConcurrent::run([data = reply->readAll(), this]() { + try { + emit initialSyncCompleted(nlohmann::json::parse(std::move(data))); + } catch (std::exception &e) { + qWarning() << "Initial sync error:" << e.what(); + emit initialSyncFailed(); + } + }); }); } @@ -730,10 +730,8 @@ MatrixClient::fetchUserAvatar(const QUrl &avatarUrl) { QList<QString> url_parts = avatarUrl.toString().split("mxc://"); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for user avatar:" << avatarUrl.toString(); + if (url_parts.size() != 2) return QSharedPointer<DownloadMediaProxy>(); - } QUrlQuery query; query.addQueryItem("width", "128"); diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
index 0c1a89db..981908e2 100644 --- a/src/RoomInfoListItem.cc +++ b/src/RoomInfoListItem.cc
@@ -22,12 +22,12 @@ #include <variant.hpp> +#include "Cache.h" #include "Config.h" #include "Menu.h" #include "Ripple.h" #include "RippleOverlay.h" #include "RoomInfoListItem.h" -#include "RoomSettings.h" #include "Theme.h" #include "Utils.h" @@ -73,15 +73,20 @@ RoomInfoListItem::init(QWidget *parent) headingFont_ = font_; headingFont_.setPixelSize(conf::roomlist::fonts::heading); headingFont_.setWeight(60); + + menu_ = new Menu(this); + leaveRoom_ = new QAction(tr("Leave room"), this); + connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); + menu_->addAction(leaveRoom_); } -RoomInfoListItem::RoomInfoListItem(QString room_id, - mtx::responses::InvitedRoom room, - QWidget *parent) +RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent) : QWidget(parent) - , roomType_{RoomType::Invited} - , invitedRoom_{std::move(room)} - , roomId_{std::move(room_id)} + , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} + , roomId_(std::move(room_id)) + , roomName_{QString::fromStdString(std::move(info.name))} + , isPressed_(false) + , unreadMsgCount_(0) { init(parent); @@ -91,47 +96,8 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, // // State events in invited rooms don't contain timestamp info, // so we can't use them for sorting. - auto now = QDateTime::currentDateTime(); - lastMsgInfo_ = {"-", "-", "-", "-", now.addYears(10)}; - - roomAvatar_ = QString::fromStdString(invitedRoom_.avatar()); - roomName_ = QString::fromStdString(invitedRoom_.name()); -} - -RoomInfoListItem::RoomInfoListItem(QSharedPointer<RoomSettings> settings, - QSharedPointer<RoomState> state, - QString room_id, - QWidget *parent) - : QWidget(parent) - , state_(state) - , roomId_(room_id) - , roomSettings_{settings} - , isPressed_(false) - , unreadMsgCount_(0) -{ - init(parent); - - menu_ = new Menu(this); - - toggleNotifications_ = new QAction(notificationText(), this); - connect(toggleNotifications_, &QAction::triggered, this, [this]() { - roomSettings_->toggleNotifications(); - }); - - leaveRoom_ = new QAction(tr("Leave room"), this); - connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); - - menu_->addAction(toggleNotifications_); - menu_->addAction(leaveRoom_); -} - -QString -RoomInfoListItem::notificationText() -{ - if (roomSettings_.isNull() || roomSettings_->isNotificationsEnabled()) - return QString(tr("Disable notifications")); - - return tr("Enable notifications"); + if (roomType_ == RoomType::Invited) + lastMsgInfo_ = {"-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; } void @@ -352,7 +318,6 @@ RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event) if (roomType_ == RoomType::Invited) return; - toggleNotifications_->setText(notificationText()); menu_->popup(event->globalPos()); } diff --git a/src/RoomList.cc b/src/RoomList.cc
index 866d28ae..7d17585c 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc
@@ -26,7 +26,6 @@ #include "OverlayModal.h" #include "RoomInfoListItem.h" #include "RoomList.h" -#include "RoomSettings.h" #include "RoomState.h" #include "UserSettingsPage.h" @@ -74,17 +73,11 @@ RoomList::RoomList(QSharedPointer<MatrixClient> client, } void -RoomList::clear() +RoomList::addRoom(const QString &room_id, const RoomInfo &info) { - rooms_.clear(); -} + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); + room_item->setRoomName(QString::fromStdString(std::move(info.name))); -void -RoomList::addRoom(const QSharedPointer<RoomSettings> &settings, - const QSharedPointer<RoomState> &state, - const QString &room_id) -{ - auto room_item = new RoomInfoListItem(settings, state, room_id, scrollArea_); connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) { MainWindow::instance()->openLeaveRoomDialog(room_id); @@ -92,8 +85,8 @@ RoomList::addRoom(const QSharedPointer<RoomSettings> &settings, rooms_.emplace(room_id, QSharedPointer<RoomInfoListItem>(room_item)); - if (!state->getAvatar().toString().isEmpty()) - updateAvatar(room_id, state->getAvatar().toString()); + if (!info.avatar_url.empty()) + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); int pos = contentsLayout_->count() - 1; contentsLayout_->insertWidget(pos, room_item); @@ -164,20 +157,19 @@ RoomList::calculateUnreadMessageCount() } void -RoomList::setInitialRooms(const std::map<QString, QSharedPointer<RoomSettings>> &settings, - const std::map<QString, QSharedPointer<RoomState>> &states) +RoomList::initialize(const QMap<QString, RoomInfo> &info) { + qDebug() << "initialize room list"; + rooms_.clear(); - if (settings.size() != states.size()) { - qWarning() << "Initializing room list"; - qWarning() << "Different number of room states and room settings"; - return; + for (auto it = info.begin(); it != info.end(); it++) { + if (it.value().is_invite) + addInvitedRoom(it.key(), it.value()); + else + addRoom(it.key(), it.value()); } - for (auto const &state : states) - addRoom(settings.at(state.first), state.second, state.first); - if (rooms_.empty()) return; @@ -190,21 +182,11 @@ RoomList::setInitialRooms(const std::map<QString, QSharedPointer<RoomSettings>> } void -RoomList::sync(const std::map<QString, QSharedPointer<RoomState>> &states, - const std::map<QString, QSharedPointer<RoomSettings>> &settings) +RoomList::sync(const std::map<QString, RoomInfo> &info) { - for (auto const &state : states) { - if (!roomExists(state.first)) - addRoom(settings.at(state.first), state.second, state.first); - - auto room = rooms_[state.first]; - auto new_avatar = state.second->getAvatar(); - - updateAvatar(state.first, new_avatar.toString()); - - room->setState(state.second); - } + for (const auto &room : info) + updateRoom(room.first, room.second); } void @@ -368,14 +350,24 @@ RoomList::paintEvent(QPaintEvent *) } void -RoomList::syncInvites(const std::map<std::string, mtx::responses::InvitedRoom> &rooms) +RoomList::updateRoom(const QString &room_id, const RoomInfo &info) { - for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { - const auto room_id = QString::fromStdString(it->first); + qDebug() << "updateRoom" << QString::fromStdString(info.name) << room_id; - if (!roomExists(room_id)) - addInvitedRoom(room_id, it->second); + if (!roomExists(room_id)) { + if (info.is_invite) + addInvitedRoom(room_id, info); + else + addRoom(room_id, info); + + return; } + + auto room = rooms_[room_id]; + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); + room->setRoomName(QString::fromStdString(info.name)); + room->setRoomType(info.is_invite); + room->update(); } void @@ -386,15 +378,16 @@ RoomList::setRoomFilter(std::vector<QString> room_ids) } void -RoomList::addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room) +RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) { - auto room_item = new RoomInfoListItem(room_id, room, scrollArea_); + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); + connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite); connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite); rooms_.emplace(room_id, QSharedPointer<RoomInfoListItem>(room_item)); - updateAvatar(room_id, QString::fromStdString(room.avatar())); + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); int pos = contentsLayout_->count() - 1; contentsLayout_->insertWidget(pos, room_item); diff --git a/src/SuggestionsPopup.cpp b/src/SuggestionsPopup.cpp
index 51229806..e7011fcb 100644 --- a/src/SuggestionsPopup.cpp +++ b/src/SuggestionsPopup.cpp
@@ -1,10 +1,11 @@ #include "Avatar.h" #include "AvatarProvider.h" +#include "Cache.h" +#include "ChatPage.h" #include "Config.h" #include "DropShadow.h" #include "SuggestionsPopup.hpp" #include "Utils.h" -#include "timeline/TimelineViewManager.h" #include <QDebug> #include <QPaintEvent> @@ -30,7 +31,7 @@ PopupItem::PopupItem(QWidget *parent, const QString &user_id) QFont font; font.setPixelSize(conf::popup::font); - auto displayName = TimelineViewManager::displayName(user_id); + auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), user_id); avatar_->setSize(conf::popup::avatar); avatar_->setLetter(utils::firstChar(displayName)); @@ -45,8 +46,10 @@ PopupItem::PopupItem(QWidget *parent, const QString &user_id) topLayout_->addWidget(avatar_); topLayout_->addWidget(userName_, 1); - AvatarProvider::resolve( - user_id, this, [this](const QImage &img) { avatar_->setImage(img); }); + AvatarProvider::resolve(ChatPage::instance()->currentRoom(), + user_id, + this, + [this](const QImage &img) { avatar_->setImage(img); }); } void @@ -65,7 +68,7 @@ void PopupItem::mousePressEvent(QMouseEvent *event) { if (event->buttons() != Qt::RightButton) - emit clicked(TimelineViewManager::displayName(user_id_)); + emit clicked(Cache::displayName(ChatPage::instance()->currentRoom(), user_id_)); QWidget::mousePressEvent(event); } @@ -164,7 +167,7 @@ SuggestionsPopup::selectHoveredSuggestion() return; const auto &widget = qobject_cast<PopupItem *>(item->widget()); - emit itemSelected(TimelineViewManager::displayName(widget->user())); + emit itemSelected(Cache::displayName(ChatPage::instance()->currentRoom(), widget->user())); resetSelection(); } diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index 4c6c8704..1535f563 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc
@@ -31,8 +31,9 @@ #include <variant.hpp> +#include "Cache.h" +#include "ChatPage.h" #include "Config.h" -#include "RoomState.h" #include "TextInputWidget.h" #include "Utils.h" @@ -40,7 +41,6 @@ static constexpr size_t INPUT_HISTORY_SIZE = 127; static constexpr int MAX_TEXTINPUT_HEIGHT = 120; static constexpr int InputHeight = 26; static constexpr int ButtonHeight = 24; -static constexpr int MaxPopupItems = 5; FilteredTextEdit::FilteredTextEdit(QWidget *parent) : QTextEdit{parent} @@ -454,49 +454,16 @@ TextInputWidget::TextInputWidget(QWidget *parent) input_->setFixedHeight(textInputHeight); }); connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty() || currState_.isNull()) + if (q.isEmpty() || cache_.isNull()) return; QtConcurrent::run([this, q = q.toLower().toStdString()]() { - std::multimap<int, std::pair<std::string, std::string>> items; - - auto get_name = [](auto membership) { - auto name = membership.second.content.display_name; - auto key = membership.first; - - // Remove the leading '@' character. - if (name.empty()) { - key.erase(0, 1); - name = key; - } - - return std::make_pair(key, name); - }; - - for (const auto &m : currState_->memberships) { - const auto user = get_name(m); - const int score = utils::levenshtein_distance(q, user.second); - - items.emplace(score, user); + try { + emit input_->resultsRetrieved(cache_->getAutocompleteMatches( + ChatPage::instance()->currentRoom().toStdString(), q)); + } catch (const lmdb::error &e) { + std::cout << e.what() << '\n'; } - - QVector<SearchResult> results; - auto end = items.begin(); - - if (items.size() >= MaxPopupItems) - std::advance(end, MaxPopupItems); - else if (items.size() > 0) - std::advance(end, items.size()); - - for (auto it = items.begin(); it != end; it++) { - const auto user = it->second; - - results.push_back( - SearchResult{QString::fromStdString(user.first), - QString::fromStdString(user.second)}); - } - - emit input_->resultsRetrieved(results); }); }); diff --git a/src/Utils.cc b/src/Utils.cc
index 169be75e..1c053d38 100644 --- a/src/Utils.cc +++ b/src/Utils.cc
@@ -1,5 +1,5 @@ +#include "Cache.h" #include "Utils.h" -#include "timeline/TimelineViewManager.h" #include <variant.hpp> @@ -22,7 +22,9 @@ utils::descriptiveTime(const QDateTime &then) } DescInfo -utils::getMessageDescription(const TimelineEvent &event, const QString &localUser) +utils::getMessageDescription(const TimelineEvent &event, + const QString &localUser, + const QString &room_id) { using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; @@ -36,7 +38,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<Audio>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{sender == localUser ? "You" : username, @@ -48,7 +50,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse auto msg = mpark::get<Emote>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); const auto body = QString::fromStdString(msg.content.body).trimmed(); @@ -61,7 +63,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<File>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{sender == localUser ? "You" : username, @@ -73,7 +75,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<Image>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{sender == localUser ? "You" : username, @@ -85,7 +87,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<Notice>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{ @@ -94,7 +96,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<Text>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); const auto body = QString::fromStdString(msg.content.body).trimmed(); @@ -107,7 +109,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get<Video>(event); QString sender = QString::fromStdString(msg.sender); - const auto username = TimelineViewManager::displayName(sender); + const auto username = Cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{sender == localUser ? "You" : username, diff --git a/src/dialogs/ReadReceipts.cc b/src/dialogs/ReadReceipts.cc
index 63ce68e6..a2e1faf2 100644 --- a/src/dialogs/ReadReceipts.cc +++ b/src/dialogs/ReadReceipts.cc
@@ -6,17 +6,21 @@ #include <QTimer> #include <QVBoxLayout> +#include "ChatPage.h" #include "Config.h" #include "Utils.h" #include "Avatar.h" #include "AvatarProvider.h" +#include "Cache.h" #include "dialogs/ReadReceipts.h" -#include "timeline/TimelineViewManager.h" using namespace dialogs; -ReceiptItem::ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp) +ReceiptItem::ReceiptItem(QWidget *parent, + const QString &user_id, + uint64_t timestamp, + const QString &room_id) : QWidget(parent) { topLayout_ = new QHBoxLayout(this); @@ -29,7 +33,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, const QString &user_id, uint64_t times QFont font; font.setPixelSize(conf::receipts::font); - auto displayName = TimelineViewManager::displayName(user_id); + auto displayName = Cache::displayName(room_id, user_id); avatar_ = new Avatar(this); avatar_->setSize(40); @@ -51,8 +55,10 @@ ReceiptItem::ReceiptItem(QWidget *parent, const QString &user_id, uint64_t times topLayout_->addWidget(avatar_); topLayout_->addLayout(textLayout_, 1); - AvatarProvider::resolve( - user_id, this, [this](const QImage &img) { avatar_->setImage(img); }); + AvatarProvider::resolve(ChatPage::instance()->currentRoom(), + user_id, + this, + [this](const QImage &img) { avatar_->setImage(img); }); } QString @@ -104,8 +110,10 @@ ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<u userList_->clear(); for (const auto &receipt : receipts) { - auto user = - new ReceiptItem(this, QString::fromStdString(receipt.second), receipt.first); + auto user = new ReceiptItem(this, + QString::fromStdString(receipt.second), + receipt.first, + ChatPage::instance()->currentRoom()); auto item = new QListWidgetItem(userList_); item->setSizeHint(user->minimumSizeHint()); diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cc
index 0296c6cd..cd1dac67 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cc
@@ -99,12 +99,14 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, const QString &userid, QString body, bool withSender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { init(); - auto displayName = TimelineViewManager::displayName(userid); + auto displayName = Cache::displayName(room_id_, userid); auto timestamp = QDateTime::currentDateTime(); if (ty == mtx::events::MessageType::Emote) { @@ -127,7 +129,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, messageLayout_->addLayout(headerLayout_, 1); AvatarProvider::resolve( - userid, this, [this](const QImage &img) { setUserAvatar(img); }); + room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -143,8 +145,10 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, TimelineItem::TimelineItem(ImageItem *image, const QString &userid, bool withSender, + const QString &room_id, QWidget *parent) : QWidget{parent} + , room_id_{room_id} { init(); @@ -153,8 +157,13 @@ TimelineItem::TimelineItem(ImageItem *image, addSaveImageAction(image); } -TimelineItem::TimelineItem(FileItem *file, const QString &userid, bool withSender, QWidget *parent) +TimelineItem::TimelineItem(FileItem *file, + const QString &userid, + bool withSender, + const QString &room_id, + QWidget *parent) : QWidget{parent} + , room_id_{room_id} { init(); @@ -164,8 +173,10 @@ TimelineItem::TimelineItem(FileItem *file, const QString &userid, bool withSende TimelineItem::TimelineItem(AudioItem *audio, const QString &userid, bool withSender, + const QString &room_id, QWidget *parent) : QWidget{parent} + , room_id_{room_id} { init(); @@ -175,8 +186,10 @@ TimelineItem::TimelineItem(AudioItem *audio, TimelineItem::TimelineItem(VideoItem *video, const QString &userid, bool withSender, + const QString &room_id, QWidget *parent) : QWidget{parent} + , room_id_{room_id} { init(); @@ -186,8 +199,10 @@ TimelineItem::TimelineItem(VideoItem *video, TimelineItem::TimelineItem(ImageItem *image, const mtx::events::RoomEvent<mtx::events::msg::Image> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>( image, event, " sent an image", with_sender); @@ -198,8 +213,10 @@ TimelineItem::TimelineItem(ImageItem *image, TimelineItem::TimelineItem(FileItem *file, const mtx::events::RoomEvent<mtx::events::msg::File> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>( file, event, " sent a file", with_sender); @@ -208,8 +225,10 @@ TimelineItem::TimelineItem(FileItem *file, TimelineItem::TimelineItem(AudioItem *audio, const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>( audio, event, " sent an audio clip", with_sender); @@ -218,8 +237,10 @@ TimelineItem::TimelineItem(AudioItem *audio, TimelineItem::TimelineItem(VideoItem *video, const mtx::events::RoomEvent<mtx::events::msg::Video> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>( video, event, " sent a video clip", with_sender); @@ -230,8 +251,10 @@ TimelineItem::TimelineItem(VideoItem *video, */ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { init(); @@ -240,7 +263,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - descriptionMsg_ = {TimelineViewManager::displayName(sender), + descriptionMsg_ = {Cache::displayName(room_id_, sender), sender, " sent a notification", utils::descriptiveTime(timestamp), @@ -253,7 +276,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice body = "<i>" + body + "</i>"; if (with_sender) { - auto displayName = TimelineViewManager::displayName(sender); + auto displayName = Cache::displayName(room_id_, sender); generateBody(displayName, body); setupAvatarLayout(displayName); @@ -261,7 +284,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice messageLayout_->addLayout(headerLayout_, 1); AvatarProvider::resolve( - sender, this, [this](const QImage &img) { setUserAvatar(img); }); + room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -279,8 +302,10 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice */ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { init(); @@ -289,7 +314,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> auto body = QString::fromStdString(event.content.body).trimmed(); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = TimelineViewManager::displayName(sender); + auto displayName = Cache::displayName(room_id_, sender); auto emoteMsg = QString("* %1 %2").arg(displayName).arg(body); descriptionMsg_ = {"", sender, emoteMsg, utils::descriptiveTime(timestamp), timestamp}; @@ -306,7 +331,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> messageLayout_->addLayout(headerLayout_, 1); AvatarProvider::resolve( - sender, this, [this](const QImage &img) { setUserAvatar(img); }); + room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(emoteMsg); setupSimpleLayout(); @@ -324,8 +349,10 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> */ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event, bool with_sender, + const QString &room_id, QWidget *parent) : QWidget(parent) + , room_id_{room_id} { init(); @@ -334,7 +361,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> auto body = QString::fromStdString(event.content.body).trimmed(); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = TimelineViewManager::displayName(sender); + auto displayName = Cache::displayName(room_id_, sender); QSettings settings; descriptionMsg_ = {sender == settings.value("auth/user_id") ? "You" : displayName, @@ -356,7 +383,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> messageLayout_->addLayout(headerLayout_, 1); AvatarProvider::resolve( - sender, this, [this](const QImage &img) { setUserAvatar(img); }); + room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); } else { generateBody(body); setupSimpleLayout(); @@ -532,7 +559,7 @@ TimelineItem::addAvatar() // TODO: should be replaced with the proper event struct. auto userid = descriptionMsg_.userid; - auto displayName = TimelineViewManager::displayName(userid); + auto displayName = Cache::displayName(room_id_, userid); QFontMetrics fm(usernameFont_); userName_ = new QLabel(this); @@ -566,5 +593,6 @@ TimelineItem::addAvatar() messageLayout_->addWidget(checkmark_); messageLayout_->addWidget(timestamp_); - AvatarProvider::resolve(userid, this, [this](const QImage &img) { setUserAvatar(img); }); + AvatarProvider::resolve( + room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); } diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc
index d18c1cff..f7194265 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc
@@ -477,8 +477,7 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) auto with_sender = lastSender_ != local_user_; TimelineItem *view_item = - new TimelineItem(ty, local_user_, body, with_sender, scroll_widget_); - view_item->setRoomId(room_id_); + new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); addTimelineItem(view_item); @@ -538,7 +537,7 @@ TimelineView::notifyForLastEvent() void TimelineView::notifyForLastEvent(const TimelineEvent &event) { - auto descInfo = utils::getMessageDescription(event, local_user_); + auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); if (!descInfo.timestamp.isEmpty()) emit updateLastTimelineMessage(room_id_, descInfo); diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cc
index 55f25dfc..28f44770 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cc
@@ -172,18 +172,23 @@ TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { addRoom(it->second, QString::fromStdString(it->first)); } + + sync(rooms); } void -TimelineViewManager::initialize(const std::vector<QString> &rooms) +TimelineViewManager::initialize(const std::vector<std::string> &rooms) { for (const auto &roomid : rooms) - addRoom(roomid); + addRoom(QString::fromStdString(roomid)); } void TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) { + if (timelineViewExists(room_id)) + return; + // Create a history view with the room events. TimelineView *view = new TimelineView(room.timeline, client_, room_id); views_.emplace(room_id, QSharedPointer<TimelineView>(view)); @@ -200,6 +205,9 @@ TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QStri void TimelineViewManager::addRoom(const QString &room_id) { + if (timelineViewExists(room_id)) + return; + // Create a history view without any events. TimelineView *view = new TimelineView(client_, room_id); views_.emplace(room_id, QSharedPointer<TimelineView>(view)); @@ -247,8 +255,6 @@ TimelineViewManager::setHistoryView(const QString &room_id) view->scrollDown(); } -std::map<QString, QString> TimelineViewManager::DISPLAY_NAMES; - QString TimelineViewManager::chooseRandomColor() { @@ -307,15 +313,6 @@ TimelineViewManager::chooseRandomColor() return color.name(); } -QString -TimelineViewManager::displayName(const QString &userid) -{ - if (DISPLAY_NAMES.find(userid) != DISPLAY_NAMES.end()) - return DISPLAY_NAMES.at(userid); - - return userid; -} - bool TimelineViewManager::hasLoaded() const {