diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h index 95a5765d..dabd609c 100644 --- a/include/AvatarProvider.h +++ b/include/AvatarProvider.h @@ -17,45 +17,30 @@ #pragma once +#include #include #include -#include #include class MatrixClient; class TimelineItem; -//! Saved cache data per user. -struct AvatarData -{ - //! The avatar image of the user. - QImage img; - //! The url that was used to download the avatar. - QUrl url; -}; - class AvatarProvider : public QObject { Q_OBJECT public: - static void init(QSharedPointer client); + static void init(QSharedPointer client) { client_ = client; } //! The callback is called with the downloaded avatar for the given user //! or the avatar is downloaded first and then saved for re-use. - static void resolve(const QString &userId, + static void resolve(const QString &room_id, + const QString &userId, QObject *receiver, std::function callback); - //! Used to initialize the mapping user -> avatar url. - static void setAvatarUrl(const QString &userId, const QUrl &url); //! Remove all saved data. static void clear() { avatars_.clear(); }; private: - //! Update the cache with the downloaded avatar. - static void updateAvatar(const QString &uid, const QImage &img); - static QSharedPointer client_; - - using UserID = QString; - static std::map avatars_; + static QHash avatars_; }; diff --git a/include/Cache.h b/include/Cache.h index 93668b8c..b3bc085b 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -17,12 +17,23 @@ #pragma once +#include #include #include #include #include #include "RoomState.h" +#include "Utils.h" + +struct SearchResult +{ + QString user_id; + QString display_name; +}; + +Q_DECLARE_METATYPE(SearchResult) +Q_DECLARE_METATYPE(QVector) //! Used to uniquely identify a list of read receipts. struct ReadReceiptKey @@ -44,6 +55,60 @@ from_json(const json &j, ReadReceiptKey &key) key.room_id = j.at("room_id").get(); } +//! UI info associated with a room. +struct RoomInfo +{ + //! The calculated name of the room. + std::string name; + //! The topic of the room. + std::string topic; + //! The calculated avatar url of the room. + std::string avatar_url; + //! Whether or not the room is an invite. + bool is_invite = false; +}; + +inline void +to_json(json &j, const RoomInfo &info) +{ + j["name"] = info.name; + j["topic"] = info.topic; + j["avatar_url"] = info.avatar_url; + j["is_invite"] = info.is_invite; +} + +inline void +from_json(const json &j, RoomInfo &info) +{ + info.name = j.at("name"); + info.topic = j.at("topic"); + info.avatar_url = j.at("avatar_url"); + info.is_invite = j.at("is_invite"); +} + +//! Basic information per member; +struct MemberInfo +{ + std::string name; + std::string avatar_url; +}; + +inline void +to_json(json &j, const MemberInfo &info) +{ + j["name"] = info.name; + j["avatar_url"] = info.avatar_url; +} + +inline void +from_json(const json &j, MemberInfo &info) +{ + info.name = j.at("name"); + info.avatar_url = j.at("avatar_url"); +} + +Q_DECLARE_METATYPE(RoomInfo) + class Cache : public QObject { Q_OBJECT @@ -51,22 +116,50 @@ class Cache : public QObject public: Cache(const QString &userId, QObject *parent = nullptr); - void setState(const QString &nextBatchToken, - const std::map> &states); + static QHash DisplayNames; + static QHash AvatarUrls; + + static std::string displayName(const std::string &room_id, const std::string &user_id); + static QString displayName(const QString &room_id, const QString &user_id); + static QString avatarUrl(const QString &room_id, const QString &user_id); + + static void removeDisplayName(const QString &room_id, const QString &user_id); + static void removeAvatarUrl(const QString &room_id, const QString &user_id); + + static void insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name); + static void insertAvatarUrl(const QString &room_id, + const QString &user_id, + const QString &avatar_url); + + //! Load saved data for the display names & avatars. + void populateMembers(); + std::vector joinedRooms(); + + QMap roomInfo(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id); + + void saveState(const mtx::responses::Sync &res); bool isInitialized() const; QString nextBatchToken() const; - void states(); - - using Invites = std::map; - Invites invites(); - void setInvites(const Invites &invites); void deleteData(); - void unmount() { isMounted_ = false; }; - void removeRoom(const QString &roomid); - void removeInvite(const QString &roomid); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; void setup(); bool isFormatValid(); @@ -88,24 +181,206 @@ public: QByteArray image(const QString &url) const; void saveImage(const QString &url, const QByteArray &data); -signals: - void statesLoaded(std::map states); + std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); + std::map getRoomInfo(const std::vector &rooms); + std::map roomUpdates(const mtx::responses::Sync &sync) + { + return getRoomInfo(roomsWithStateUpdates(sync)); + } + + QVector getAutocompleteMatches(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); private: + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template + void saveStateEvents(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const std::vector &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, membersdb, room_id, e); + } + + template + void saveStateEvent(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (mpark::holds_alternative>(event)) { + const auto e = mpark::get>(event); + + switch (e.content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = e.content.display_name.empty() + ? e.state_key + : e.content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e.content.avatar_url}; + + lmdb::dbi_put(txn, + membersdb, + lmdb::val(e.state_key), + lmdb::val(json(tmp).dump())); + + insertDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e.state_key), + QString::fromStdString(display_name)); + + insertAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e.state_key), + QString::fromStdString(e.content.avatar_url)); + + break; + } + default: { + lmdb::dbi_del( + txn, membersdb, lmdb::val(e.state_key), lmdb::val("")); + + removeDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e.state_key)); + removeAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e.state_key)); + + break; + } + } + + return; + } + + if (!isStateEvent(event)) + return; + + mpark::visit( + [&txn, &statesdb](auto e) { + lmdb::dbi_put( + txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); + }, + event); + } + + template + bool isStateEvent(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + template + bool containsStateUpdates(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + void saveInvites(lmdb::txn &txn, + const std::map &rooms); + + //! Remove any saved invites that are not found in the input. + void removeStaleInvites(lmdb::txn &txn, const std::map &curr); + + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map &rooms) + { + for (const auto &room : rooms) + removeRoom(txn, room.first); + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const QString &token); - void insertRoomState(lmdb::txn &txn, - const QString &roomid, - const QSharedPointer &state); lmdb::env env_; - lmdb::dbi stateDb_; - lmdb::dbi roomDb_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; lmdb::dbi invitesDb_; - lmdb::dbi imagesDb_; + lmdb::dbi mediaDb_; lmdb::dbi readReceiptsDb_; - bool isMounted_; - - QString userId_; + QString localUserId_; QString cacheDirectory_; }; diff --git a/include/ChatPage.h b/include/ChatPage.h index 25cd8615..a6789aea 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -24,16 +24,16 @@ #include #include +#include "Cache.h" #include "CommunitiesList.h" #include "Community.h" + #include -class Cache; class MatrixClient; class OverlayModal; class QuickSwitcher; class RoomList; -class RoomSettings; class RoomState; class SideBarActions; class Splitter; @@ -52,6 +52,9 @@ constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; +Q_DECLARE_METATYPE(mtx::responses::Rooms); +Q_DECLARE_METATYPE(std::vector); + class ChatPage : public QWidget { Q_OBJECT @@ -88,6 +91,14 @@ signals: void showLoginPage(const QString &msg); void showUserSettingsPage(); void showOverlayProgressBar(); + void startConsesusTimer(); + + void initializeRoomList(QMap); + void initializeViews(const mtx::responses::Rooms &rooms); + void initializeEmptyViews(const std::vector &rooms); + void syncUI(const mtx::responses::Rooms &rooms); + void continueSync(const QString &next_batch); + void syncRoomlist(const std::map &updates); private slots: void showUnreadMessageNotification(int count); @@ -98,9 +109,9 @@ private slots: void syncCompleted(const mtx::responses::Sync &response); void changeTopRoomInfo(const QString &room_id); void logout(); - void addRoom(const QString &room_id); void removeRoom(const QString &room_id); - void removeInvite(const QString &room_id); + //! Handles initial sync failures. + void retryInitialSync(int status_code = -1); private: static ChatPage *instance_; @@ -110,28 +121,11 @@ private: using Membership = mtx::events::StateEvent; using Memberships = std::map; - using JoinedRooms = std::map; - using LeftRooms = std::map; - using InvitedRooms = std::map; - + using LeftRooms = std::map; void removeLeftRooms(const LeftRooms &rooms); - void updateJoinedRooms(const JoinedRooms &rooms); - void trackInvites(const InvitedRooms &rooms) - { - for (const auto &invite : rooms) - roomInvites_[QString::fromStdString(invite.first)] = true; - } - - std::map> generateMembershipDifference( - const JoinedRooms &rooms, - const RoomStates &states) const; void updateTypingUsers(const QString &roomid, const std::vector &user_ids); - using MemberEvent = mtx::events::StateEvent; - void updateUserDisplayName(const MemberEvent &event); - void updateUserAvatarUrl(const MemberEvent &event); - void loadStateFromCache(); void deleteConfigs(); void resetUI(); @@ -141,10 +135,6 @@ private: template Memberships getMemberships(const std::vector &events) const; - template - void updateUserMetadata(const std::vector &collection); - - void retryInitialSync(int status_code = -1); //! Update the room with the new notification count. void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count); @@ -186,8 +176,6 @@ private: UserInfoWidget *user_info_widget_; RoomStates roomStates_; - std::map> roomSettings_; - std::map roomInvites_; std::map> communities_; @@ -211,22 +199,6 @@ private: QSharedPointer cache_; }; -template -void -ChatPage::updateUserMetadata(const std::vector &collection) -{ - using Member = mtx::events::StateEvent; - - for (const auto &event : collection) { - if (mpark::holds_alternative(event)) { - auto member = mpark::get(event); - - updateUserAvatarUrl(member); - updateUserDisplayName(member); - } - } -} - template std::map> ChatPage::getMemberships(const std::vector &collection) const diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 62ac2088..d16031d3 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -32,6 +32,8 @@ signals: void avatarDownloaded(const QImage &img); }; +Q_DECLARE_METATYPE(mtx::responses::Sync) + /* * MatrixClient provides the high level API to communicate with * a Matrix homeserver. All the responses are returned through signals. diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h index 35214c30..d06a759e 100644 --- a/include/RoomInfoListItem.h +++ b/include/RoomInfoListItem.h @@ -24,11 +24,9 @@ #include -#include "RoomState.h" - class Menu; class RippleOverlay; -class RoomSettings; +struct RoomInfo; struct DescInfo { @@ -70,24 +68,13 @@ class RoomInfoListItem : public QWidget Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) public: - RoomInfoListItem(QSharedPointer settings, - QSharedPointer state, - QString room_id, - QWidget *parent = 0); - - RoomInfoListItem(QString room_id, mtx::responses::InvitedRoom room, QWidget *parent = 0); + RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0); void updateUnreadMessageCount(int count); void clearUnreadMessageCount() { updateUnreadMessageCount(0); }; - void setState(QSharedPointer state) - { - state_ = state; - update(); - } QString roomId() { return roomId_; } bool isPressed() const { return isPressed_; } - QSharedPointer state() const { return state_; } int unreadMessageCount() const { return unreadMsgCount_; } void setAvatar(const QImage &avatar_image); @@ -133,6 +120,15 @@ public: void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; } void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; } + void setRoomName(const QString &name) { roomName_ = name; } + void setRoomType(bool isInvite) + { + if (isInvite) + roomType_ = RoomType::Invited; + else + roomType_ = RoomType::Joined; + } + signals: void clicked(const QString &room_id); void leaveRoom(const QString &room_id); @@ -150,15 +146,7 @@ protected: private: void init(QWidget *parent); - QString roomName() - { - if (roomType_ == RoomType::Joined) - return state_->getName(); - - return roomName_; - } - - QString notificationText(); + QString roomName() { return roomName_; } RippleOverlay *ripple_overlay_; @@ -170,9 +158,6 @@ private: RoomType roomType_ = RoomType::Joined; - // State information for the joined rooms. - QSharedPointer state_; - // State information for the invited rooms. mtx::responses::InvitedRoom invitedRoom_; @@ -184,11 +169,8 @@ private: QPixmap roomAvatar_; Menu *menu_; - QAction *toggleNotifications_; QAction *leaveRoom_; - QSharedPointer roomSettings_; - bool isPressed_ = false; int unreadMsgCount_ = 0; diff --git a/include/RoomList.h b/include/RoomList.h index bcac8094..f9cdc210 100644 --- a/include/RoomList.h +++ b/include/RoomList.h @@ -23,14 +23,13 @@ #include #include +#include "Cache.h" #include class LeaveRoomDialog; class MatrixClient; -class Cache; class OverlayModal; class RoomInfoListItem; -class RoomSettings; class RoomState; class Sync; class UserSettings; @@ -46,22 +45,18 @@ public: QWidget *parent = 0); void setCache(QSharedPointer cache) { cache_ = cache; } - void setInitialRooms(const std::map> &settings, - const std::map> &states); - void sync(const std::map> &states, - const std::map> &settings); - void syncInvites(const std::map &rooms); + void initialize(const QMap &info); + void sync(const std::map &info); - void clear(); + void clear() { rooms_.clear(); }; void updateAvatar(const QString &room_id, const QString &url); - void addRoom(const QSharedPointer &settings, - const QSharedPointer &state, - const QString &room_id); - void addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room); + void addRoom(const QString &room_id, const RoomInfo &info); + void addInvitedRoom(const QString &room_id, const RoomInfo &info); void removeRoom(const QString &room_id, bool reset); void setFilterRooms(bool filterRooms); void setRoomFilter(std::vector room_ids); + void updateRoom(const QString &room_id, const RoomInfo &info); signals: void roomChanged(const QString &room_id); diff --git a/include/SuggestionsPopup.hpp b/include/SuggestionsPopup.hpp index dcbe52fa..64a86d7a 100644 --- a/include/SuggestionsPopup.hpp +++ b/include/SuggestionsPopup.hpp @@ -6,15 +6,7 @@ #include class Avatar; - -struct SearchResult -{ - QString user_id; - QString display_name; -}; - -Q_DECLARE_METATYPE(SearchResult) -Q_DECLARE_METATYPE(QVector) +struct SearchResult; class PopupItem : public QWidget { diff --git a/include/TextInputWidget.h b/include/TextInputWidget.h index 7a52ea77..1f122504 100644 --- a/include/TextInputWidget.h +++ b/include/TextInputWidget.h @@ -36,7 +36,7 @@ #include "emoji/PickButton.h" -class RoomState; +class Cache; namespace dialogs { class PreviewUploadOverlay; @@ -131,12 +131,12 @@ public: QColor borderColor() const { return borderColor_; } void setBorderColor(QColor &color) { borderColor_ = color; } + void setCache(QSharedPointer cache) { cache_ = cache; } public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void setRoomState(QSharedPointer state) { currState_ = state; } private slots: void addSelectedEmoji(const QString &emoji); @@ -172,8 +172,7 @@ private: FlatButton *sendMessageBtn_; emoji::PickButton *emojiBtn_; - //! State of the current room. - QSharedPointer currState_; + QSharedPointer cache_; QColor borderColor_; }; diff --git a/include/Utils.h b/include/Utils.h index cbecb4ac..5586a479 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -16,7 +16,7 @@ descriptiveTime(const QDateTime &then); //! Generate a message description from the event to be displayed //! in the RoomList. DescInfo -getMessageDescription(const TimelineEvent &event, const QString &localUser); +getMessageDescription(const TimelineEvent &event, const QString &localUser, const QString &room_id); //! Get the first character of a string, taking into account that //! surrogate pairs might be in use. diff --git a/include/dialogs/ReadReceipts.h b/include/dialogs/ReadReceipts.h index 8f860b46..bd4e4fc5 100644 --- a/include/dialogs/ReadReceipts.h +++ b/include/dialogs/ReadReceipts.h @@ -16,7 +16,10 @@ class ReceiptItem : public QWidget Q_OBJECT public: - ReceiptItem(QWidget *parent, const QString &user_id, uint64_t timestamp); + ReceiptItem(QWidget *parent, + const QString &user_id, + uint64_t timestamp, + const QString &room_id); private: QString dateFormat(const QDateTime &then) const; diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h index b7a5623f..952fb661 100644 --- a/include/timeline/TimelineItem.h +++ b/include/timeline/TimelineItem.h @@ -26,9 +26,9 @@ #include #include "AvatarProvider.h" +#include "Cache.h" #include "ChatPage.h" #include "RoomInfoListItem.h" -#include "TimelineViewManager.h" #include "Utils.h" class ImageItem; @@ -43,12 +43,15 @@ class TimelineItem : public QWidget public: TimelineItem(const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent = 0); TimelineItem(const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent = 0); TimelineItem(const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent = 0); // For local messages. @@ -57,28 +60,49 @@ public: const QString &userid, QString body, bool withSender, + const QString &room_id, QWidget *parent = 0); // m.image - TimelineItem(ImageItem *item, const QString &userid, bool withSender, QWidget *parent = 0); - TimelineItem(FileItem *item, const QString &userid, bool withSender, QWidget *parent = 0); - TimelineItem(AudioItem *item, const QString &userid, bool withSender, QWidget *parent = 0); - TimelineItem(VideoItem *item, const QString &userid, bool withSender, QWidget *parent = 0); + TimelineItem(ImageItem *item, + const QString &userid, + bool withSender, + const QString &room_id, + QWidget *parent = 0); + TimelineItem(FileItem *item, + const QString &userid, + bool withSender, + const QString &room_id, + QWidget *parent = 0); + TimelineItem(AudioItem *item, + const QString &userid, + bool withSender, + const QString &room_id, + QWidget *parent = 0); + TimelineItem(VideoItem *item, + const QString &userid, + bool withSender, + const QString &room_id, + QWidget *parent = 0); TimelineItem(ImageItem *img, const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent); TimelineItem(FileItem *file, const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent); TimelineItem(AudioItem *audio, const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent); TimelineItem(VideoItem *video, const mtx::events::RoomEvent &e, bool with_sender, + const QString &room_id, QWidget *parent); void setUserAvatar(const QImage &pixmap); @@ -86,7 +110,7 @@ public: QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } void markReceived(); - void setRoomId(const QString &room_id) { room_id_ = room_id; } + void setRoomId(QString room_id) { room_id_ = room_id; } void sendReadReceipt() const { if (!event_id_.isEmpty()) @@ -159,7 +183,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &msgDescription, bool withSender) { - auto displayName = TimelineViewManager::displayName(userid); + auto displayName = Cache::displayName(room_id_, userid); auto timestamp = QDateTime::currentDateTime(); descriptionMsg_ = {"You", @@ -183,7 +207,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, 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 { setupSimpleLayout(); @@ -208,7 +232,7 @@ TimelineItem::setupWidgetLayout(Widget *widget, const auto sender = QString::fromStdString(event.sender); 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, @@ -232,7 +256,7 @@ TimelineItem::setupWidgetLayout(Widget *widget, 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 { setupSimpleLayout(); diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index b38d6a7d..6f70dd1c 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -259,8 +259,7 @@ TimelineView::addUserMessage(const QString &url, auto widget = new Widget(client_, url, trimmed, size, this); TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, scroll_widget_); - view_item->setRoomId(room_id_); + new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); addTimelineItem(view_item); @@ -280,8 +279,7 @@ template TimelineItem * TimelineView::createTimelineItem(const Event &event, bool withSender) { - TimelineItem *item = new TimelineItem(event, withSender, scroll_widget_); - item->setRoomId(room_id_); + TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); return item; } @@ -290,8 +288,7 @@ TimelineItem * TimelineView::createTimelineItem(const Event &event, bool withSender) { auto eventWidget = new Widget(client_, event); - auto item = new TimelineItem(eventWidget, event, withSender, scroll_widget_); - item->setRoomId(room_id_); + auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); return item; } diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h index c7bbd71a..4c994098 100644 --- a/include/timeline/TimelineViewManager.h +++ b/include/timeline/TimelineViewManager.h @@ -39,7 +39,7 @@ public: // Initialize with timeline events. void initialize(const mtx::responses::Rooms &rooms); // Empty initialization. - void initialize(const std::vector &rooms); + void initialize(const std::vector &rooms); void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); void addRoom(const QString &room_id); @@ -51,9 +51,6 @@ public: bool hasLoaded() const; static QString chooseRandomColor(); - static QString displayName(const QString &userid); - - static std::map DISPLAY_NAMES; signals: void clearRoomMessageCount(QString roomid); 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 AvatarProvider::client_; - -std::map AvatarProvider::avatars_; +QHash AvatarProvider::avatars_; void -AvatarProvider::init(QSharedPointer client) -{ - client_ = client; -} - -void -AvatarProvider::updateAvatar(const QString &uid, const QImage &img) -{ - auto avatarData = &avatars_[uid]; - avatarData->img = img; -} - -void -AvatarProvider::resolve(const QString &userId, +AvatarProvider::resolve(const QString &room_id, + const QString &user_id, QObject *receiver, std::function 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 #include #include +#include #include #include @@ -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>; using Receipts = std::map>; 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(); } 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> &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(); - } + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr); + txn.commit(); } void -Cache::insertRoomState(lmdb::txn &txn, - const QString &roomid, - const QSharedPointer &state) +Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) { - 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; - } - } - } + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); } void -Cache::removeRoom(const QString &roomid) +Cache::removeRoom(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); - + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); txn.commit(); } void -Cache::removeInvite(const QString &room_id) +Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) { - 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); - - txn.commit(); -} - -void -Cache::states() -{ - std::map 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> 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 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 -Cache::invites() -{ - std::map 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 &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 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 &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 &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>(e)) { + auto msg = mpark::get>(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 +Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + std::vector 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 +Cache::getRoomInfo(const std::vector &rooms) +{ + std::map 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 +Cache::roomInfo() +{ + QMap 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 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 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 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 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 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 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 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 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 +Cache::joinedRooms() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + + std::string id, data; + std::vector 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 +Cache::getAutocompleteMatches(const std::string &room_id, + const std::string &query, + std::uint8_t max_items) +{ + std::multimap> 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 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 Cache::DisplayNames; +QHash 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 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 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 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 &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>(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType>(); } 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(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); + // Process ephemeral data per room. + for (const auto &room : response.rooms.join) { + auto room_id = QString::fromStdString(room.first); - const auto nextBatchToken = QString::fromStdString(response.next_batch); + updateTypingUsers(room_id, room.second.ephemeral.typing); + updateRoomNotificationCount(room_id, + room.second.unread_notifications.notification_count); + } - auto stateDiff = generateMembershipDifference(response.rooms.join, roomStates_); - QtConcurrent::run(cache_.data(), &Cache::setState, nextBatchToken, stateDiff); - QtConcurrent::run(cache_.data(), &Cache::setInvites, response.rooms.invite); + 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; + } - room_list_->sync(roomStates_, roomSettings_); - room_list_->syncInvites(response.rooms.invite); - trackInvites(response.rooms.invite); - - view_manager_->sync(response.rooms); - - client_->setNextBatchToken(nextBatchToken); - client_->sync(); - - 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; + qDebug() << "initial sync completed"; - for (auto it = joined.cbegin(); it != joined.cend(); ++it) { - auto roomState = QSharedPointer(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(); - - const auto room_id = QString::fromStdString(it->first); - - roomStates_.emplace(room_id, roomState); - roomSettings_.emplace(room_id, - QSharedPointer(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 &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() << "restoring state from cache"; - qDebug() << "Restored nextBatchToken" << cache_->nextBatchToken(); - client_->setNextBatchToken(cache_->nextBatchToken()); + QtConcurrent::run([this]() { + try { + cache_->populateMembers(); - qRegisterMetaType>(); + 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; + } - QtConcurrent::run(cache_.data(), &Cache::states); + // Start receiving events. + emit continueSync(cache_->nextBatchToken()); - connect( - cache_.data(), &Cache::statesLoaded, this, [this](std::map rooms) { - qDebug() << "Cache data loaded"; - - std::vector roomKeys; - - for (auto const &room : rooms) { - auto roomState = QSharedPointer(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(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); - - // Show the content if consensus can't be achieved. - showContentTimer_->start(SHOW_CONTENT_TIMEOUT); - - // Start receiving events. - client_->sync(); - }); + // Check periodically if the timelines have been loaded. + emit startConsesusTimer(); + }); } void @@ -734,69 +707,30 @@ ChatPage::showQuickSwitcher() std::map 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(new RoomState); - - roomStates_.emplace(room_id, room_state); - roomSettings_.emplace(room_id, - QSharedPointer(new RoomSettings(room_id))); - - room_list_->addRoom(roomSettings_[room_id], roomStates_[room_id], room_id); - room_list_->highlightSelectedRoom(room_id); - - changeTopRoomInfo(room_id); + try { + 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) { + const auto err = QString::fromStdString(e.what()); + emit showNotification(QString("Failed to load room list: %1").arg(err)); } } void ChatPage::removeRoom(const QString &room_id) { - roomStates_.erase(room_id); - roomSettings_.erase(room_id); - try { cache_->removeRoom(room_id); - cache_->removeInvite(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 -ChatPage::removeInvite(const QString &room_id) -{ - try { - cache_->removeInvite(room_id); - } 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 &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 &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 &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 &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(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(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> -ChatPage::generateMembershipDifference( - const std::map &rooms, - const std::map> &states) const -{ - std::map> 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(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) { 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 #include #include +#include #include #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(); + 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 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(); - } 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 +#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 settings, - QSharedPointer 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 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 &settings, - const QSharedPointer &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 &settings, rooms_.emplace(room_id, QSharedPointer(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> &settings, - const std::map> &states) +RoomList::initialize(const QMap &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> } void -RoomList::sync(const std::map> &states, - const std::map> &settings) +RoomList::sync(const std::map &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 &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 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(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 #include @@ -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(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 +#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> 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 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 @@ -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; using Emote = mtx::events::RoomEvent; @@ -36,7 +38,7 @@ utils::getMessageDescription(const TimelineEvent &event, const QString &localUse const auto msg = mpark::get