summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-06-13 01:44:25 +0000
committerGitHub <noreply@github.com>2021-06-13 01:44:25 +0000
commit5b4566d3f959e13b34d34a4156a0646b26c7fd08 (patch)
tree11d1b42e1d9f92977c48670605bb8a2e26ddd43c /src
parentTranslated using Weblate (Esperanto) (diff)
parentFix button spacing (diff)
downloadnheko-5b4566d3f959e13b34d34a4156a0646b26c7fd08.tar.xz
Merge pull request #605 from Nheko-Reborn/qml-roomlist
Qml roomlist and stuff
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp90
-rw-r--r--src/Cache.h2
-rw-r--r--src/CacheStructs.h13
-rw-r--r--src/Cache_p.h6
-rw-r--r--src/ChatPage.cpp337
-rw-r--r--src/ChatPage.h60
-rw-r--r--src/CommunitiesList.cpp345
-rw-r--r--src/CommunitiesList.h66
-rw-r--r--src/CommunitiesListItem.cpp201
-rw-r--r--src/CommunitiesListItem.h108
-rw-r--r--src/CompletionProxyModel.cpp6
-rw-r--r--src/MainWindow.cpp36
-rw-r--r--src/MainWindow.h5
-rw-r--r--src/Olm.cpp9
-rw-r--r--src/RoomInfoListItem.cpp522
-rw-r--r--src/RoomInfoListItem.h210
-rw-r--r--src/RoomList.cpp540
-rw-r--r--src/RoomList.h101
-rw-r--r--src/SideBarActions.cpp120
-rw-r--r--src/SideBarActions.h54
-rw-r--r--src/Splitter.cpp166
-rw-r--r--src/Splitter.h49
-rw-r--r--src/UserInfoWidget.cpp219
-rw-r--r--src/UserInfoWidget.h68
-rw-r--r--src/UserSettingsPage.cpp68
-rw-r--r--src/UserSettingsPage.h12
-rw-r--r--src/popups/PopupItem.cpp89
-rw-r--r--src/popups/PopupItem.h66
-rw-r--r--src/popups/SuggestionsPopup.cpp164
-rw-r--r--src/popups/SuggestionsPopup.h53
-rw-r--r--src/popups/UserMentions.cpp178
-rw-r--r--src/popups/UserMentions.h49
-rw-r--r--src/timeline/CommunitiesModel.cpp188
-rw-r--r--src/timeline/CommunitiesModel.h64
-rw-r--r--src/timeline/EventStore.cpp14
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/InputBar.cpp36
-rw-r--r--src/timeline/InputBar.h1
-rw-r--r--src/timeline/RoomlistModel.cpp590
-rw-r--r--src/timeline/RoomlistModel.h164
-rw-r--r--src/timeline/TimelineModel.cpp45
-rw-r--r--src/timeline/TimelineModel.h15
-rw-r--r--src/timeline/TimelineViewManager.cpp326
-rw-r--r--src/timeline/TimelineViewManager.h68
-rw-r--r--src/ui/NhekoDropArea.cpp2
-rw-r--r--src/ui/NhekoGlobalObject.cpp142
-rw-r--r--src/ui/NhekoGlobalObject.h57
-rw-r--r--src/ui/Theme.cpp125
-rw-r--r--src/ui/Theme.h42
-rw-r--r--src/ui/ThemeManager.cpp39
-rw-r--r--src/ui/ThemeManager.h5
-rw-r--r--src/ui/UserProfile.cpp36
-rw-r--r--src/ui/UserProfile.h3
53 files changed, 1729 insertions, 4247 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 24b2bc24..0d75ac51 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -253,6 +253,8 @@ Cache::setup() outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); txn.commit(); + + databaseReady_ = true; } void @@ -788,6 +790,7 @@ Cache::nextBatchToken() void Cache::deleteData() { + this->databaseReady_ = false; // TODO: We need to remove the env_ while not accepting new requests. lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, roomsDb_); @@ -2042,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) return fallbackDesc; } -std::map<QString, bool> +QHash<QString, RoomInfo> Cache::invites() { - std::map<QString, bool> result; + QHash<QString, RoomInfo> result; auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); auto cursor = lmdb::cursor::open(txn, invitesDb_); - std::string_view room_id, unused; + std::string_view room_id, room_data; - while (cursor.get(room_id, unused, MDB_NEXT)) - result.emplace(QString::fromStdString(std::string(room_id)), true); + while (cursor.get(room_id, room_data, MDB_NEXT)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + room_id, + std::string(room_data), + e.what()); + } + } cursor.close(); - txn.commit(); + + return result; +} + +std::optional<RoomInfo> +Cache::invite(std::string_view roomid) +{ + std::optional<RoomInfo> result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::string_view room_data; + + if (invitesDb_.get(txn, roomid, room_data)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); + result = std::move(tmp); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + roomid, + std::string(room_data), + e.what()); + } + } return result; } @@ -2426,7 +2465,7 @@ Cache::joinedRooms() std::optional<MemberInfo> Cache::getMember(const std::string &room_id, const std::string &user_id) { - if (user_id.empty()) + if (user_id.empty() || !env_.handle()) return std::nullopt; try { @@ -2440,7 +2479,8 @@ Cache::getMember(const std::string &room_id, const std::string &user_id) return m; } } catch (std::exception &e) { - nhlog::db()->warn("Failed to read member ({}): {}", user_id, e.what()); + nhlog::db()->warn( + "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); } return std::nullopt; } @@ -3412,6 +3452,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query if (!updateToWrite.master_keys.keys.empty() && update.master_keys.keys != updateToWrite.master_keys.keys) { + nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", + user, + updateToWrite.master_keys.keys.size(), + update.master_keys.keys.size()); updateToWrite.master_key_changed = true; } @@ -3466,6 +3510,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query } } } + for (auto &[user_id, update] : updates) { (void)update; if (user_id == local_user) { @@ -3473,9 +3518,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query (void)status; emit verificationStatusChanged(user); } - } else { - emit verificationStatusChanged(user_id); } + emit verificationStatusChanged(user_id); } } @@ -3549,10 +3593,23 @@ Cache::query_keys(const std::string &user_id, last_changed = cache_->last_changed; req.token = last_changed; + // use context object so that we can disconnect again + QObject *context{new QObject(this)}; + QObject::connect(this, + &Cache::verificationStatusChanged, + context, + [cb, user_id, context_ = context](std::string updated_user) mutable { + if (user_id == updated_user) { + context_->deleteLater(); + auto keys = cache::userKeys(user_id); + cb(keys.value_or(UserKeyCache{}), {}); + } + }); + http::client()->query_keys( req, - [cb, user_id, last_changed](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {},{}", mtx::errors::to_string(err->matrix_error.errcode), @@ -3561,10 +3618,7 @@ Cache::query_keys(const std::string &user_id, return; } - cache::updateUserKeys(last_changed, res); - - auto keys = cache::userKeys(user_id); - cb(keys.value_or(UserKeyCache{}), err); + emit userKeysUpdate(last_changed, res); }); } @@ -3999,6 +4053,8 @@ avatarUrl(const QString &room_id, const QString &user_id) mtx::presence::PresenceState presenceState(const std::string &user_id) { + if (!instance_) + return {}; return instance_->presenceState(user_id); } std::string @@ -4049,7 +4105,7 @@ roomInfo(bool withInvites) { return instance_->roomInfo(withInvites); } -std::map<QString, bool> +QHash<QString, RoomInfo> invites() { return instance_->invites(); diff --git a/src/Cache.h b/src/Cache.h
index 427dbafc..74ec9695 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -62,7 +62,7 @@ joinedRooms(); QMap<QString, RoomInfo> roomInfo(bool withInvites = true); -std::map<QString, bool> +QHash<QString, RoomInfo> invites(); //! Calculate & return the name of the room. diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index c449f013..f7d6f0e2 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h
@@ -50,6 +50,19 @@ struct DescInfo QDateTime datetime; }; +inline bool +operator==(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} +inline bool +operator!=(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} + //! UI info associated with a room. struct RoomInfo { diff --git a/src/Cache_p.h b/src/Cache_p.h
index 356c6e42..f2911622 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -70,7 +70,8 @@ public: QMap<QString, RoomInfo> roomInfo(bool withInvites = true); std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); - std::map<QString, bool> invites(); + QHash<QString, RoomInfo> invites(); + std::optional<RoomInfo> invite(std::string_view roomid); //! Calculate & return the name of the room. QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); @@ -100,6 +101,7 @@ public: void saveState(const mtx::responses::Sync &res); bool isInitialized(); + bool isDatabaseReady() { return databaseReady_ && isInitialized(); } std::string nextBatchToken(); @@ -620,6 +622,8 @@ private: QString cacheDirectory_; VerificationStorage verification_storage; + + bool databaseReady_ = false; }; namespace cache { diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 1a1c1044..0f16f205 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -23,10 +23,6 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" -#include "RoomList.h" -#include "SideBarActions.h" -#include "Splitter.h" -#include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" #include "ui/OverlayModal.h" @@ -36,7 +32,6 @@ #include "notifications/Manager.h" #include "dialogs/ReadReceipts.h" -#include "popups/UserMentions.h" #include "timeline/TimelineViewManager.h" #include "blurhash.hpp" @@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) topLayout_->setSpacing(0); topLayout_->setMargin(0); - communitiesList_ = new CommunitiesList(this); - topLayout_->addWidget(communitiesList_); - - splitter = new Splitter(this); - splitter->setHandleWidth(0); - - topLayout_->addWidget(splitter); - - // SideBar - sideBar_ = new QFrame(this); - sideBar_->setObjectName("sideBar"); - sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal); - sideBarLayout_ = new QVBoxLayout(sideBar_); - sideBarLayout_->setSpacing(0); - sideBarLayout_->setMargin(0); - - sideBarTopWidget_ = new QWidget(sideBar_); - sidebarActions_ = new SideBarActions(this); - connect( - sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); - connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); - connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); - - user_info_widget_ = new UserInfoWidget(sideBar_); - connect(user_info_widget_, &UserInfoWidget::openGlobalUserProfile, this, [this]() { - UserProfile *userProfile = new UserProfile("", utils::localUser(), view_manager_); - emit view_manager_->openProfile(userProfile); - }); - - user_mentions_popup_ = new popups::UserMentions(); - room_list_ = new RoomList(userSettings, sideBar_); - connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); - - sideBarLayout_->addWidget(user_info_widget_); - sideBarLayout_->addWidget(room_list_); - sideBarLayout_->addWidget(sidebarActions_); - - sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_); - sideBarTopWidgetLayout_->setSpacing(0); - sideBarTopWidgetLayout_->setMargin(0); - - // Content - content_ = new QFrame(this); - content_->setObjectName("mainContent"); - contentLayout_ = new QVBoxLayout(content_); - contentLayout_->setSpacing(0); - contentLayout_->setMargin(0); - view_manager_ = new TimelineViewManager(callManager_, this); - contentLayout_->addWidget(view_manager_->getWidget()); - - // Splitter - splitter->addWidget(sideBar_); - splitter->addWidget(content_); - splitter->restoreSizes(parent->width()); + topLayout_->addWidget(view_manager_->getWidget()); connect(this, &ChatPage::downloadedSecrets, @@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) trySync(); }); - connect( - new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { - if (isVisible()) - room_list_->nextRoom(); - }); - connect( - new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() { - if (isVisible()) - room_list_->previousRoom(); - }); - connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { if (http::client()->access_token().empty()) { @@ -185,10 +116,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect( - view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { - const auto room_id = current_room_.toStdString(); + const auto room_id = currentRoom().toStdString(); for (int ii = 0; ii < users.size(); ++ii) { QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { @@ -211,36 +140,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } }); - connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) { - this->current_room_ = room_id; - }); - connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); - connect( - room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); - - connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { - joinRoom(room_id); - room_list_->removeRoom(room_id, currentRoom() == room_id); - }); - - connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { - leaveRoom(room_id); - room_list_->removeRoom(room_id, currentRoom() == room_id); - }); - - connect(view_manager_, - &TimelineViewManager::updateRoomsLastMessage, - room_list_, - &RoomList::updateRoomDescription); - - connect(room_list_, - SIGNAL(totalUnreadMessageCountUpdated(int)), - this, - SIGNAL(unreadMessages(int))); - - connect( - this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); - connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); @@ -255,60 +154,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } }); - connect(communitiesList_, - &CommunitiesList::communityChanged, - this, - [this](const QString &groupId) { - current_community_ = groupId; - - if (groupId == "world") { - auto hidden = communitiesList_->hiddenTagsAndCommunities(); - std::set<QString> roomsToHide = communitiesList_->roomList(groupId); - for (const auto &hiddenTag : hidden) { - auto temp = communitiesList_->roomList(hiddenTag); - roomsToHide.insert(temp.begin(), temp.end()); - } - - room_list_->removeFilter(roomsToHide); - } else { - auto hidden = communitiesList_->hiddenTagsAndCommunities(); - hidden.erase(current_community_); - - auto roomsToShow = communitiesList_->roomList(groupId); - for (const auto &hiddenTag : hidden) { - for (const auto &r : communitiesList_->roomList(hiddenTag)) - roomsToShow.erase(r); - } - - room_list_->applyFilter(roomsToShow); - } - }); - connect(&notificationsManager, &NotificationsManager::notificationClicked, this, [this](const QString &roomid, const QString &eventid) { Q_UNUSED(eventid) - room_list_->highlightSelectedRoom(roomid); + view_manager_->rooms()->setCurrentRoom(roomid); activateWindow(); }); connect(&notificationsManager, &NotificationsManager::sendNotificationReply, this, [this](const QString &roomid, const QString &eventid, const QString &body) { + view_manager_->rooms()->setCurrentRoom(roomid); view_manager_->queueReply(roomid, eventid, body); - room_list_->highlightSelectedRoom(roomid); activateWindow(); }); - setGroupViewState(userSettings_->groupView()); - - connect(userSettings_.data(), - &UserSettings::groupViewStateChanged, - this, - &ChatPage::setGroupViewState); + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + // ensure the qml context is shutdown before we destroy all other singletons + // Otherwise Qml will try to access the room list or settings, after they have been + // destroyed + topLayout_->removeWidget(view_manager_->getWidget()); + delete view_manager_->getWidget(); + }); - connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize); connect( this, &ChatPage::initializeViews, @@ -318,31 +188,14 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::initializeEmptyViews, view_manager_, - &TimelineViewManager::initWithMessages); - connect(this, - &ChatPage::initializeMentions, - user_mentions_popup_, - &popups::UserMentions::initializeMentions); + &TimelineViewManager::initializeRoomlist); connect( this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged); connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { - try { - room_list_->cleanupInvites(cache::invites()); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to retrieve invites: {}", e.what()); - } - view_manager_->sync(rooms); - removeLeftRooms(rooms.leave); bool hasNotifications = false; for (const auto &room : rooms.join) { - auto room_id = QString::fromStdString(room.first); - updateRoomNotificationCount( - room_id, - room.second.unread_notifications.notification_count, - room.second.unread_notifications.highlight_count); - if (room.second.unread_notifications.notification_count > 0) hasNotifications = true; } @@ -365,16 +218,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) emit notificationsRetrieved(std::move(res)); }); }); - connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); - connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags); - - // Callbacks to update the user info (top left corner of the page). - connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar); - connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) { - auto userid = utils::localUser(); - user_info_widget_->setUserId(userid); - user_info_widget_->setDisplayName(name); - }); connect( this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection); @@ -427,8 +270,6 @@ ChatPage::dropToLoginPage(const QString &msg) void ChatPage::resetUI() { - room_list_->clear(); - user_info_widget_->reset(); view_manager_->clearAll(); emit unreadMessages(0); @@ -481,9 +322,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) view_manager_, &TimelineViewManager::updateReadReceipts); - connect( - cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus); - connect(cache::client(), &Cache::removeNotification, &notificationsManager, @@ -559,10 +397,8 @@ ChatPage::loadStateFromCache() try { olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - emit initializeEmptyViews(cache::client()->roomIds()); - emit initializeRoomList(cache::roomInfo()); + emit initializeEmptyViews(); emit initializeMentions(cache::getTimelineMentions()); - emit syncTags(cache::roomInfo().toStdMap()); cache::calculateRoomReadStatus(); @@ -600,38 +436,6 @@ ChatPage::removeRoom(const QString &room_id) nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. } - - room_list_->removeRoom(room_id, room_id == current_room_); -} - -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); - room_list_->removeRoom(room_id, room_id == current_room_); - } -} - -void -ChatPage::setGroupViewState(bool isEnabled) -{ - if (!isEnabled) { - communitiesList_->communityChanged("world"); - communitiesList_->hide(); - - return; - } - - communitiesList_->show(); -} - -void -ChatPage::updateRoomNotificationCount(const QString &room_id, - uint16_t notification_count, - uint16_t highlight_count) -{ - room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count); } void @@ -680,18 +484,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res) } void -ChatPage::showNotificationsDialog(const QPoint &widgetPos) -{ - auto notifDialog = user_mentions_popup_; - - notifDialog->setGeometry( - widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); - - notifDialog->raise(); - notifDialog->showPopup(); -} - -void ChatPage::tryInitialSync() { nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); @@ -789,11 +581,9 @@ ChatPage::startInitialSync() olm::handle_to_device_messages(res.to_device.events); emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::roomInfo()); emit initializeMentions(cache::getTimelineMentions()); cache::calculateRoomReadStatus(); - emit syncTags(cache::roomInfo().toStdMap()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save state after initial sync: {}", e.what()); @@ -830,12 +620,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); - emit syncRoomlist(updates); - emit syncUI(res.rooms); - emit syncTags(cache::getRoomInfo(cache::client()->roomsWithTagUpdates(res))); - // if we process a lot of syncs (1 every 200ms), this means we clean the // db every 100s static int syncCounter = 0; @@ -939,7 +725,7 @@ ChatPage::joinRoomVia(const std::string &room_id, emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); } - room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); + view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id)); }); } @@ -987,19 +773,18 @@ ChatPage::leaveRoom(const QString &room_id) void ChatPage::changeRoom(const QString &room_id) { - view_manager_->setHistoryView(room_id); - room_list_->highlightSelectedRoom(room_id); + view_manager_->rooms()->setCurrentRoom(room_id); } void ChatPage::inviteUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm invite"), tr("Do you really want to invite %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1021,12 +806,12 @@ ChatPage::inviteUser(QString userid, QString reason) void ChatPage::kickUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm kick"), tr("Do you really want to kick %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1048,12 +833,12 @@ ChatPage::kickUser(QString userid, QString reason) void ChatPage::banUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm ban"), tr("Do you really want to ban %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1075,12 +860,12 @@ ChatPage::banUser(QString userid, QString reason) void ChatPage::unbanUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm unban"), tr("Do you really want to unban %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1182,57 +967,6 @@ ChatPage::getProfileInfo() emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); - - http::client()->joined_groups( - [this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->critical("failed to retrieve joined groups: {} {}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit updateGroupsInfo({}); - return; - } - - emit updateGroupsInfo(res); - }); -} - -bool -ChatPage::isRoomActive(const QString &room_id) -{ - return isActiveWindow() && content_->isVisible() && currentRoom() == room_id; -} - -void -ChatPage::hideSideBars() -{ - // Don't hide side bar, if we are currently only showing the side bar! - if (view_manager_->getWidget()->isVisible()) { - communitiesList_->hide(); - sideBar_->hide(); - } - view_manager_->enableBackButton(); -} - -void -ChatPage::showSideBars() -{ - if (userSettings_->groupView()) - communitiesList_->show(); - - sideBar_->show(); - view_manager_->disableBackButton(); - content_->show(); -} - -uint64_t -ChatPage::timelineWidth() -{ - int sidebarWidth = sideBar_->minimumSize().width(); - sidebarWidth += communitiesList_->minimumSize().width(); - nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth); - - return size().width() - sidebarWidth; } void @@ -1318,7 +1052,8 @@ ChatPage::startChat(QString userid) if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != room_members.end()) { - room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); + view_manager_->rooms()->setCurrentRoom( + QString::fromStdString(room_id)); return; } } @@ -1408,7 +1143,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri) if (sigil1 == "u") { if (action.isEmpty()) { - view_manager_->activeTimeline()->openUserProfile(mxid1); + if (auto t = view_manager_->rooms()->currentRoom()) + t->openUserProfile(mxid1); } else if (action == "chat") { this->startChat(mxid1); } @@ -1418,7 +1154,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri) for (auto roomid : joined_rooms) { if (roomid == targetRoomId) { - room_list_->highlightSelectedRoom(mxid1); + view_manager_->rooms()->setCurrentRoom(mxid1); if (!mxid2.isEmpty()) view_manager_->showEvent(mxid1, mxid2); return; @@ -1436,7 +1172,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri) auto aliases = cache::client()->getRoomAliases(roomid); if (aliases) { if (aliases->alias == targetRoomAlias) { - room_list_->highlightSelectedRoom( + view_manager_->rooms()->setCurrentRoom( QString::fromStdString(roomid)); if (!mxid2.isEmpty()) view_manager_->showEvent( @@ -1458,8 +1194,17 @@ ChatPage::handleMatrixUri(const QUrl &uri) handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); } -void -ChatPage::highlightRoom(const QString &room_id) +bool +ChatPage::isRoomActive(const QString &room_id) { - room_list_->highlightSelectedRoom(room_id); + return isActiveWindow() && currentRoom() == room_id; +} + +QString +ChatPage::currentRoom() const +{ + if (view_manager_->rooms()->currentRoom()) + return view_manager_->rooms()->currentRoom()->roomId(); + else + return ""; } diff --git a/src/ChatPage.h b/src/ChatPage.h
index a7489455..751e7074 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h
@@ -27,15 +27,10 @@ #include "CacheCryptoStructs.h" #include "CacheStructs.h" -#include "CommunitiesList.h" #include "notifications/Manager.h" class OverlayModal; -class RoomList; -class SideBarActions; -class Splitter; class TimelineViewManager; -class UserInfoWidget; class UserSettings; class NotificationsManager; class TimelineModel; @@ -53,11 +48,6 @@ struct Notifications; struct Sync; struct Timeline; struct Rooms; -struct LeftRoom; -} - -namespace popups { -class UserMentions; } using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>; @@ -71,7 +61,6 @@ public: // Initialize all the components of the UI. void bootstrap(QString userid, QString homeserver, QString token); - QString currentRoom() const { return current_room_; } static ChatPage *instance() { return instance_; } @@ -80,14 +69,6 @@ public: TimelineViewManager *timelineManager() { return view_manager_; } void deleteConfigs(); - CommunitiesList *communitiesList() { return communitiesList_; } - - //! Calculate the width of the message timeline. - uint64_t timelineWidth(); - //! Hide the room & group list (if it was visible). - void hideSideBars(); - //! Show the room/group list (if it was visible). - void showSideBars(); void initiateLogout(); QString status() const; @@ -95,6 +76,9 @@ public: mtx::presence::PresenceState currentPresence() const; + // TODO(Nico): Get rid of this! + QString currentRoom() const; + public slots: void handleMatrixUri(const QByteArray &uri); void handleMatrixUri(const QUrl &uri); @@ -102,7 +86,6 @@ public slots: void startChat(QString userid); void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); - void highlightRoom(const QString &room_id); void joinRoom(const QString &room); void joinRoomVia(const std::string &room_id, const std::vector<std::string> &via, @@ -145,13 +128,10 @@ signals: void leftRoom(const QString &room_id); void newRoom(const QString &room_id); - void initializeRoomList(QMap<QString, RoomInfo>); void initializeViews(const mtx::responses::Rooms &rooms); - void initializeEmptyViews(const std::vector<QString> &roomIds); + void initializeEmptyViews(); void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); void syncUI(const mtx::responses::Rooms &rooms); - void syncRoomlist(const std::map<QString, RoomInfo> &updates); - void syncTags(const std::map<QString, RoomInfo> &updates); void dropToLoginPageCb(const QString &msg); void notifyMessage(const QString &roomid, @@ -161,7 +141,6 @@ signals: const QString &message, const QImage &icon); - void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); void themeChanged(); void decryptSidebarChanged(); @@ -213,56 +192,25 @@ private: using Membership = mtx::events::StateEvent<mtx::events::state::Member>; using Memberships = std::map<std::string, Membership>; - using LeftRooms = std::map<std::string, mtx::responses::LeftRoom>; - void removeLeftRooms(const LeftRooms &rooms); - void loadStateFromCache(); void resetUI(); - //! Decides whether or not to hide the group's sidebar. - void setGroupViewState(bool isEnabled); template<class Collection> Memberships getMemberships(const std::vector<Collection> &events) const; - //! Update the room with the new notification count. - void updateRoomNotificationCount(const QString &room_id, - uint16_t notification_count, - uint16_t highlight_count); //! Send desktop notification for the received messages. void sendNotifications(const mtx::responses::Notifications &); - void showNotificationsDialog(const QPoint &point); - template<typename T> void connectCallMessage(); QHBoxLayout *topLayout_; - Splitter *splitter; - - QWidget *sideBar_; - QVBoxLayout *sideBarLayout_; - QWidget *sideBarTopWidget_; - QVBoxLayout *sideBarTopWidgetLayout_; - - QFrame *content_; - QVBoxLayout *contentLayout_; - - CommunitiesList *communitiesList_; - RoomList *room_list_; TimelineViewManager *view_manager_; - SideBarActions *sidebarActions_; QTimer connectivityTimer_; std::atomic_bool isConnected_; - QString current_room_; - QString current_community_; - - UserInfoWidget *user_info_widget_; - - popups::UserMentions *user_mentions_popup_; - // Global user settings. QSharedPointer<UserSettings> userSettings_; diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp deleted file mode 100644
index 7cc5d10e..00000000 --- a/src/CommunitiesList.cpp +++ /dev/null
@@ -1,345 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "CommunitiesList.h" -#include "Cache.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "MxcImageProvider.h" -#include "Splitter.h" -#include "UserSettingsPage.h" - -#include <mtx/responses/groups.hpp> -#include <nlohmann/json.hpp> - -#include <QLabel> - -CommunitiesList::CommunitiesList(QWidget *parent) - : QWidget(parent) -{ - QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(1); - setSizePolicy(sizePolicy); - - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{}); - setFixedWidth(sideBarSizes.groups); - - scrollArea_ = new QScrollArea(this); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - - contentsLayout_ = new QVBoxLayout(); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - - addGlobalItem(); - contentsLayout_->addStretch(1); - - scrollArea_->setLayout(contentsLayout_); - topLayout_->addWidget(scrollArea_); - - connect( - this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar); -} - -void -CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response) -{ - // remove all non-tag communities - auto it = communities_.begin(); - while (it != communities_.end()) { - if (it->second->is_tag()) { - ++it; - } else { - it = communities_.erase(it); - } - } - - addGlobalItem(); - - for (const auto &group : response.groups) - addCommunity(group); - - communities_["world"]->setPressedState(true); - selectedCommunity_ = "world"; - emit communityChanged("world"); - sortEntries(); -} - -void -CommunitiesList::syncTags(const std::map<QString, RoomInfo> &info) -{ - for (const auto &room : info) - setTagsForRoom(room.first, room.second.tags); - emit communityChanged(selectedCommunity_); - sortEntries(); -} - -void -CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector<std::string> &tags) -{ - // create missing tag if any - for (const auto &tag : tags) { - // filter out tags we should ignore according to the spec - // https://matrix.org/docs/spec/client_server/r0.4.0.html#id154 - // nheko currently does not make use of internal tags - // so we ignore any tag containig a `.` (which would indicate a tag - // in the form `tld.domain.*`) except for `m.*` and `u.*`. - if (tag.find(".") != ::std::string::npos && tag.compare(0, 2, "m.") && - tag.compare(0, 2, "u.")) - continue; - QString name = QString("tag:") + QString::fromStdString(tag); - if (!communityExists(name)) { - addCommunity(std::string("tag:") + tag); - } - } - // update membership of the room for all tags - auto it = communities_.begin(); - while (it != communities_.end()) { - // Skip if the community is not a tag - if (!it->second->is_tag()) { - ++it; - continue; - } - // insert or remove the room from the tag as appropriate - std::string current_tag = - it->first.right(static_cast<int>(it->first.size() - strlen("tag:"))) - .toStdString(); - if (std::find(tags.begin(), tags.end(), current_tag) != tags.end()) { - // the room has this tag - it->second->addRoom(room_id); - } else { - // the room does not have this tag - it->second->delRoom(room_id); - } - // Check if the tag is now empty, if yes delete it - if (it->second->rooms().empty()) { - it = communities_.erase(it); - } else { - ++it; - } - } -} - -void -CommunitiesList::addCommunity(const std::string &group_id) -{ - auto hiddenTags = UserSettings::instance()->hiddenTags(); - - const auto id = QString::fromStdString(group_id); - - CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_); - - if (hiddenTags.contains(id)) - list_item->setDisabled(true); - - communities_.emplace(id, QSharedPointer<CommunitiesListItem>(list_item)); - contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); - - connect(list_item, - &CommunitiesListItem::clicked, - this, - &CommunitiesList::highlightSelectedCommunity); - connect(list_item, &CommunitiesListItem::isDisabledChanged, this, [this]() { - for (const auto &community : communities_) { - if (community.second->isPressed()) { - emit highlightSelectedCommunity(community.first); - break; - } - } - - auto hiddenTags = hiddenTagsAndCommunities(); - // Qt < 5.14 compat - QStringList hiddenTags_; - for (auto &&t : hiddenTags) - hiddenTags_.push_back(t); - UserSettings::instance()->setHiddenTags(hiddenTags_); - }); - - if (group_id.empty() || group_id.front() != '+') - return; - - nhlog::ui()->debug("Add community: {}", group_id); - - connect(this, - &CommunitiesList::groupProfileRetrieved, - this, - [this](const QString &id, const mtx::responses::GroupProfile &profile) { - if (communities_.find(id) == communities_.end()) - return; - - communities_.at(id)->setName(QString::fromStdString(profile.name)); - - if (!profile.avatar_url.empty()) - fetchCommunityAvatar(id, - QString::fromStdString(profile.avatar_url)); - }); - connect(this, - &CommunitiesList::groupRoomsRetrieved, - this, - [this](const QString &id, const std::set<QString> &rooms) { - nhlog::ui()->info( - "Fetched rooms for {}: {}", id.toStdString(), rooms.size()); - if (communities_.find(id) == communities_.end()) - return; - - communities_.at(id)->setRooms(rooms); - }); - - http::client()->group_profile( - group_id, [id, this](const mtx::responses::GroupProfile &res, mtx::http::RequestErr err) { - if (err) { - return; - } - - emit groupProfileRetrieved(id, res); - }); - - http::client()->group_rooms( - group_id, [id, this](const nlohmann::json &res, mtx::http::RequestErr err) { - if (err) { - return; - } - - std::set<QString> room_ids; - for (const auto &room : res.at("chunk")) - room_ids.emplace(QString::fromStdString(room.at("room_id"))); - - emit groupRoomsRetrieved(id, room_ids); - }); -} - -void -CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) -{ - if (!communityExists(community_id)) { - nhlog::ui()->warn("Avatar update on nonexistent community {}", - community_id.toStdString()); - return; - } - - communities_.at(community_id)->setAvatar(img.toImage()); -} - -void -CommunitiesList::highlightSelectedCommunity(const QString &community_id) -{ - if (!communityExists(community_id)) { - nhlog::ui()->debug("CommunitiesList: clicked unknown community"); - return; - } - - selectedCommunity_ = community_id; - emit communityChanged(community_id); - - for (const auto &community : communities_) { - if (community.first != community_id) { - community.second->setPressedState(false); - } else { - community.second->setPressedState(true); - scrollArea_->ensureWidgetVisible(community.second.data()); - } - } -} - -void -CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) -{ - MxcImageProvider::download( - QString(avatarUrl).remove(QStringLiteral("mxc://")), - QSize(96, 96), - [this, id](QString, QSize, QImage img, QString) { - if (img.isNull()) { - nhlog::net()->warn("failed to download avatar: {})", id.toStdString()); - return; - } - - emit avatarRetrieved(id, QPixmap::fromImage(img)); - }); -} - -std::set<QString> -CommunitiesList::roomList(const QString &id) const -{ - if (communityExists(id)) - return communities_.at(id)->rooms(); - - return {}; -} - -std::vector<std::string> -CommunitiesList::currentTags() const -{ - std::vector<std::string> tags; - for (auto &entry : communities_) { - CommunitiesListItem *item = entry.second.data(); - if (item->is_tag()) - tags.push_back(entry.first.mid(4).toStdString()); - } - return tags; -} - -std::set<QString> -CommunitiesList::hiddenTagsAndCommunities() const -{ - std::set<QString> hiddenTags; - for (auto &entry : communities_) { - if (entry.second->isDisabled()) - hiddenTags.insert(entry.first); - } - - return hiddenTags; -} - -void -CommunitiesList::sortEntries() -{ - std::vector<CommunitiesListItem *> header; - std::vector<CommunitiesListItem *> communities; - std::vector<CommunitiesListItem *> tags; - std::vector<CommunitiesListItem *> footer; - // remove all the contents and sort them in the 4 vectors - for (auto &entry : communities_) { - CommunitiesListItem *item = entry.second.data(); - contentsLayout_->removeWidget(item); - // world is handled separately - if (entry.first == "world") - continue; - // sort the rest - if (item->is_tag()) - if (entry.first == "tag:m.favourite") - header.push_back(item); - else if (entry.first == "tag:m.lowpriority") - footer.push_back(item); - else - tags.push_back(item); - else - communities.push_back(item); - } - - // now there remains only the stretch in the layout, remove it - QLayoutItem *stretch = contentsLayout_->itemAt(0); - contentsLayout_->removeItem(stretch); - - contentsLayout_->addWidget(communities_["world"].data()); - - auto insert_widgets = [this](auto &vec) { - for (auto item : vec) - contentsLayout_->addWidget(item); - }; - insert_widgets(header); - insert_widgets(communities); - insert_widgets(tags); - insert_widgets(footer); - - contentsLayout_->addItem(stretch); -} diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h deleted file mode 100644
index 2586f6f5..00000000 --- a/src/CommunitiesList.h +++ /dev/null
@@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QScrollArea> -#include <QSharedPointer> -#include <QVBoxLayout> - -#include "CacheStructs.h" -#include "CommunitiesListItem.h" -#include "ui/Theme.h" - -namespace mtx::responses { -struct GroupProfile; -struct JoinedGroups; -} - -class CommunitiesList : public QWidget -{ - Q_OBJECT - -public: - CommunitiesList(QWidget *parent = nullptr); - - void clear() { communities_.clear(); } - - void addCommunity(const std::string &id); - void removeCommunity(const QString &id) { communities_.erase(id); }; - std::set<QString> roomList(const QString &id) const; - - void syncTags(const std::map<QString, RoomInfo> &info); - void setTagsForRoom(const QString &id, const std::vector<std::string> &tags); - std::vector<std::string> currentTags() const; - std::set<QString> hiddenTagsAndCommunities() const; - -signals: - void communityChanged(const QString &id); - void avatarRetrieved(const QString &id, const QPixmap &img); - void groupProfileRetrieved(const QString &group_id, const mtx::responses::GroupProfile &); - void groupRoomsRetrieved(const QString &group_id, const std::set<QString> &res); - -public slots: - void updateCommunityAvatar(const QString &id, const QPixmap &img); - void highlightSelectedCommunity(const QString &id); - void setCommunities(const mtx::responses::JoinedGroups &groups); - -private: - void fetchCommunityAvatar(const QString &id, const QString &avatarUrl); - void addGlobalItem() { addCommunity("world"); } - void sortEntries(); - - //! Check whether or not a community id is currently managed. - bool communityExists(const QString &id) const - { - return communities_.find(id) != communities_.end(); - } - - QString selectedCommunity_; - QVBoxLayout *topLayout_; - QVBoxLayout *contentsLayout_; - QScrollArea *scrollArea_; - - std::map<QString, QSharedPointer<CommunitiesListItem>> communities_; -}; diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp deleted file mode 100644
index a2f2777d..00000000 --- a/src/CommunitiesListItem.cpp +++ /dev/null
@@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "CommunitiesListItem.h" - -#include <QMenu> -#include <QMouseEvent> - -#include "Utils.h" -#include "ui/Painter.h" -#include "ui/Ripple.h" -#include "ui/RippleOverlay.h" - -CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent) - : QWidget(parent) - , groupId_(group_id) -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - QPainterPath path; - path.addRect(0, 0, parent->width(), height()); - rippleOverlay_ = new RippleOverlay(this); - rippleOverlay_->setClipPath(path); - rippleOverlay_->setClipping(true); - - menu_ = new QMenu(this); - hideRoomsWithTagAction_ = - new QAction(tr("Hide rooms with this tag or from this community"), this); - hideRoomsWithTagAction_->setCheckable(true); - menu_->addAction(hideRoomsWithTagAction_); - connect(menu_, &QMenu::aboutToShow, this, [this]() { - hideRoomsWithTagAction_->setChecked(isDisabled_); - }); - - connect(hideRoomsWithTagAction_, &QAction::triggered, this, [this](bool checked) { - this->setDisabled(checked); - }); - - updateTooltip(); -} - -void -CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event) -{ - menu_->popup(event->globalPos()); -} - -void -CommunitiesListItem::setName(QString name) -{ - name_ = name; - updateTooltip(); -} - -void -CommunitiesListItem::setPressedState(bool state) -{ - if (isPressed_ != state) { - isPressed_ = state; - update(); - } -} - -void -CommunitiesListItem::setDisabled(bool state) -{ - if (isDisabled_ != state) { - isDisabled_ = state; - update(); - emit isDisabledChanged(); - } -} - -void -CommunitiesListItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() == Qt::RightButton) { - QWidget::mousePressEvent(event); - return; - } - - emit clicked(groupId_); - - setPressedState(true); - - QPoint pos = event->pos(); - qreal radiusEndValue = static_cast<qreal>(width()) / 3; - - auto ripple = new Ripple(pos); - ripple->setRadiusEndValue(radiusEndValue); - ripple->setOpacityStartValue(0.15); - ripple->setColor("white"); - ripple->radiusAnimation()->setDuration(200); - ripple->opacityAnimation()->setDuration(400); - rippleOverlay_->addRipple(ripple); -} - -void -CommunitiesListItem::paintEvent(QPaintEvent *) -{ - Painter p(this); - PainterHighQualityEnabler hq(p); - - if (isPressed_) - p.fillRect(rect(), highlightedBackgroundColor_); - else if (isDisabled_) - p.fillRect(rect(), disabledBackgroundColor_); - else if (underMouse()) - p.fillRect(rect(), hoverBackgroundColor_); - else - p.fillRect(rect(), backgroundColor_); - - if (avatar_.isNull()) { - QPixmap source; - if (groupId_ == "world") - source = QPixmap(":/icons/icons/ui/world.png"); - else if (groupId_ == "tag:m.favourite") - source = QPixmap(":/icons/icons/ui/star.png"); - else if (groupId_ == "tag:m.lowpriority") - source = QPixmap(":/icons/icons/ui/lowprio.png"); - else if (groupId_.startsWith("tag:")) - source = QPixmap(":/icons/icons/ui/tag.png"); - - if (source.isNull()) { - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.3); - p.setFont(font); - - p.drawLetterAvatar(utils::firstChar(resolveName()), - avatarFgColor_, - avatarBgColor_, - width(), - height(), - IconSize); - } else { - QPainter painter(&source); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(source.rect(), avatarFgColor_); - painter.end(); - - const int imageSz = 32; - p.drawPixmap( - QRect( - (width() - imageSz) / 2, (height() - imageSz) / 2, imageSz, imageSz), - source); - } - } else { - p.save(); - - p.drawAvatar(avatar_, width(), height(), IconSize); - p.restore(); - } -} - -void -CommunitiesListItem::setAvatar(const QImage &img) -{ - avatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); -} - -QString -CommunitiesListItem::resolveName() const -{ - if (!name_.isEmpty()) - return name_; - if (groupId_.startsWith("tag:")) - return groupId_.right(static_cast<int>(groupId_.size() - strlen("tag:"))); - if (!groupId_.startsWith("+")) - return QString("Group"); // Group with no name or id. - - // Extract the localpart of the group. - auto firstPart = groupId_.split(':').at(0); - return firstPart.right(firstPart.size() - 1); -} - -void -CommunitiesListItem::updateTooltip() -{ - if (groupId_ == "world") - setToolTip(tr("All rooms")); - else if (is_tag()) { - QStringRef tag = - groupId_.rightRef(static_cast<int>(groupId_.size() - strlen("tag:"))); - if (tag == "m.favourite") - setToolTip(tr("Favourite rooms")); - else if (tag == "m.lowpriority") - setToolTip(tr("Low priority rooms")); - else if (tag == "m.server_notice") - setToolTip(tr("Server Notices", "Tag translation for m.server_notice")); - else if (tag.startsWith("u.")) - setToolTip(tag.right(tag.size() - 2) + tr(" (tag)")); - else - setToolTip(tag + tr(" (tag)")); - } else { - QString name = resolveName(); - setToolTip(name + tr(" (community)")); - } -} diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h deleted file mode 100644
index 006511c8..00000000 --- a/src/CommunitiesListItem.h +++ /dev/null
@@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QSharedPointer> -#include <QWidget> - -#include <set> - -#include "Config.h" -#include "ui/Theme.h" - -class RippleOverlay; -class QMouseEvent; -class QMenu; - -class CommunitiesListItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE - setHighlightedBackgroundColor) - Q_PROPERTY(QColor disabledBackgroundColor READ disabledBackgroundColor WRITE - setDisabledBackgroundColor) - Q_PROPERTY( - QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - - Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor) - Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor) - -public: - CommunitiesListItem(QString group_id, QWidget *parent = nullptr); - - void setName(QString name); - bool isPressed() const { return isPressed_; } - bool isDisabled() const { return isDisabled_; } - void setAvatar(const QImage &img); - - void setRooms(std::set<QString> room_ids) { room_ids_ = std::move(room_ids); } - void addRoom(const QString &id) { room_ids_.insert(id); } - void delRoom(const QString &id) { room_ids_.erase(id); } - std::set<QString> rooms() const { return room_ids_; } - - bool is_tag() const { return groupId_.startsWith("tag:"); } - - QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } - QColor disabledBackgroundColor() const { return disabledBackgroundColor_; } - QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor avatarFgColor() const { return avatarFgColor_; } - QColor avatarBgColor() const { return avatarBgColor_; } - - void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } - void setDisabledBackgroundColor(QColor &color) { disabledBackgroundColor_ = color; } - void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } - void setBackgroundColor(QColor &color) { backgroundColor_ = color; } - - void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; } - void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; } - - QSize sizeHint() const override - { - return QSize(IconSize + IconSize / 3, IconSize + IconSize / 3); - } - -signals: - void clicked(const QString &group_id); - void isDisabledChanged(); - -public slots: - void setPressedState(bool state); - void setDisabled(bool state); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - const int IconSize = 36; - - QString resolveName() const; - void updateTooltip(); - - std::set<QString> room_ids_; - - QString name_; - QString groupId_; - QPixmap avatar_; - - QColor highlightedBackgroundColor_; - QColor disabledBackgroundColor_; - QColor hoverBackgroundColor_; - QColor backgroundColor_; - - QColor avatarFgColor_; - QColor avatarBgColor_; - - bool isPressed_ = false; - bool isDisabled_ = false; - - RippleOverlay *rippleOverlay_; - QMenu *menu_; - QAction *hideRoomsWithTagAction_; -}; diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index 412708a2..e68944c7 100644 --- a/src/CompletionProxyModel.cpp +++ b/src/CompletionProxyModel.cpp
@@ -19,7 +19,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, , max_completions_(max_completions) { setSourceModel(model); - QRegularExpression splitPoints("\\s+|-"); + QChar splitPoints(' '); // insert all the full texts for (int i = 0; i < sourceModel()->rowCount(); i++) { @@ -48,7 +48,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, .toString() .toLower(); - for (const auto &e : string1.split(splitPoints)) { + for (const auto &e : string1.splitRef(splitPoints)) { if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 trie_.insert(e.toUcs4(), i); } @@ -59,7 +59,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, .toLower(); if (!string2.isEmpty()) { - for (const auto &e : string2.split(splitPoints)) { + for (const auto &e : string2.splitRef(splitPoints)) { if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 trie_.insert(e.toUcs4(), i); } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 92f43e03..ed337ca4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -22,7 +22,6 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "RegisterPage.h" -#include "Splitter.h" #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -109,10 +108,6 @@ MainWindow::MainWindow(QWidget *parent) userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); connect( userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); - connect(userSettingsPage_, - &UserSettingsPage::decryptSidebarChanged, - chat_page_, - &ChatPage::decryptSidebarChanged); connect(trayIcon_, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, @@ -176,20 +171,6 @@ MainWindow::setWindowTitle(int notificationCount) QMainWindow::setWindowTitle(name); } -void -MainWindow::showEvent(QShowEvent *event) -{ - adjustSideBars(); - QMainWindow::showEvent(event); -} - -void -MainWindow::resizeEvent(QResizeEvent *event) -{ - adjustSideBars(); - QMainWindow::resizeEvent(event); -} - bool MainWindow::event(QEvent *event) { @@ -204,22 +185,6 @@ MainWindow::event(QEvent *event) } void -MainWindow::adjustSideBars() -{ - const auto sz = splitter::calculateSidebarSizes(QFont{}); - - const uint64_t timelineWidth = chat_page_->timelineWidth(); - const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups; - - nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth); - if (timelineWidth < minAvailableWidth) { - chat_page_->hideSideBars(); - } else { - chat_page_->showSideBars(); - } -} - -void MainWindow::restoreWindowSize() { QSettings settings; @@ -295,6 +260,7 @@ MainWindow::showChatPage() &Cache::secretChanged, userSettingsPage_, &UserSettingsPage::updateSecretStatus); + emit reload(); } void diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4122e4c1..3571f079 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h
@@ -77,13 +77,9 @@ public: protected: void closeEvent(QCloseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void showEvent(QShowEvent *event) override; bool event(QEvent *event) override; private slots: - //! Show or hide the sidebars based on window's size. - void adjustSideBars(); //! Handle interaction with the tray icon. void iconActivated(QSystemTrayIcon::ActivationReason reason); @@ -109,6 +105,7 @@ private slots: signals: void focusChanged(const bool focused); + void reload(); private: bool loadJdenticonPlugin(); diff --git a/src/Olm.cpp b/src/Olm.cpp
index d08c1b3e..ff4c883b 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg) for (const auto &cipher : msg.ciphertext) { // We skip messages not meant for the current device. - if (cipher.first != my_key) + if (cipher.first != my_key) { + nhlog::crypto()->debug( + "Skipping message for {} since we are {}.", cipher.first, my_key); continue; + } const auto type = cipher.second.type; nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); @@ -661,8 +664,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip for (const auto &id : session_ids) { auto session = cache::getOlmSession(sender_key, id); - if (!session) + if (!session) { + nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); continue; + } mtx::crypto::BinaryBuf text; diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp deleted file mode 100644
index ea5de674..00000000 --- a/src/RoomInfoListItem.cpp +++ /dev/null
@@ -1,522 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QDateTime> -#include <QInputDialog> -#include <QMenu> -#include <QMouseEvent> -#include <QPainter> -#include <QtGlobal> - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "RoomInfoListItem.h" -#include "Splitter.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/Ripple.h" -#include "ui/RippleOverlay.h" - -constexpr int MaxUnreadCountDisplayed = 99; - -struct WidgetMetrics -{ - int maxHeight; - int iconSize; - int padding; - int unit; - - int unreadLineWidth; - int unreadLineOffset; - - int inviteBtnX; - int inviteBtnY; -}; - -WidgetMetrics -getMetrics(const QFont &font) -{ - WidgetMetrics m; - - const int height = QFontMetrics(font).lineSpacing(); - - m.unit = height; - m.maxHeight = std::ceil((double)height * 3.8); - m.iconSize = std::ceil((double)height * 2.8); - m.padding = std::ceil((double)height / 2.0); - m.unreadLineWidth = m.padding - m.padding / 3; - m.unreadLineOffset = m.padding - m.padding / 4; - - m.inviteBtnX = m.iconSize + 2 * m.padding; - m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0; - - return m; -} - -void -RoomInfoListItem::init(QWidget *parent) -{ - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - auto wm = getMetrics(QFont{}); - setFixedHeight(wm.maxHeight); - - QPainterPath path; - path.addRect(0, 0, parent->width(), height()); - - ripple_overlay_ = new RippleOverlay(this); - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); - - avatar_ = new Avatar(nullptr, wm.iconSize); - avatar_->setLetter(utils::firstChar(roomName_)); - avatar_->resize(wm.iconSize, wm.iconSize); - - unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8); - unreadCountFont_.setBold(true); - - bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3; - - menu_ = new QMenu(this); - leaveRoom_ = new QAction(tr("Leave room"), this); - connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); - - connect(menu_, &QMenu::aboutToShow, this, [this]() { - menu_->clear(); - menu_->addAction(leaveRoom_); - - menu_->addSection(QIcon(":/icons/icons/ui/tag.png"), tr("Tag room as:")); - - auto roomInfo = cache::singleRoomInfo(roomId_.toStdString()); - - auto tags = ChatPage::instance()->communitiesList()->currentTags(); - - // add default tag, remove server notice tag - if (std::find(tags.begin(), tags.end(), "m.favourite") == tags.end()) - tags.push_back("m.favourite"); - if (std::find(tags.begin(), tags.end(), "m.lowpriority") == tags.end()) - tags.push_back("m.lowpriority"); - if (auto it = std::find(tags.begin(), tags.end(), "m.server_notice"); - it != tags.end()) - tags.erase(it); - - for (const auto &tag : tags) { - QString tagName; - if (tag == "m.favourite") - tagName = tr("Favourite", "Standard matrix tag for favourites"); - else if (tag == "m.lowpriority") - tagName = - tr("Low Priority", "Standard matrix tag for low priority rooms"); - else if (tag == "m.server_notice") - tagName = - tr("Server Notice", "Standard matrix tag for server notices"); - else if ((tag.size() > 2 && tag.substr(0, 2) == "u.") || - tag.find(".") != - std::string::npos) // tag manager creates tags without u., which - // is wrong, but we still want to display them - tagName = QString::fromStdString(tag.substr(2)); - - if (tagName.isEmpty()) - continue; - - auto tagAction = menu_->addAction(tagName); - tagAction->setCheckable(true); - tagAction->setWhatsThis(tr("Adds or removes the specified tag.", - "WhatsThis hint for tag menu actions")); - - for (const auto &riTag : roomInfo.tags) { - if (riTag == tag) { - tagAction->setChecked(true); - break; - } - } - - connect(tagAction, &QAction::triggered, this, [this, tag](bool checked) { - if (checked) - http::client()->put_tag( - roomId_.toStdString(), - tag, - {}, - [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error( - "Failed to add tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - else - http::client()->delete_tag( - roomId_.toStdString(), - tag, - [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error( - "Failed to delete tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - }); - } - - auto newTagAction = menu_->addAction(tr("New tag...", "Add a new tag to the room")); - connect(newTagAction, &QAction::triggered, this, [this]() { - QString tagName = - QInputDialog::getText(this, - tr("New Tag", "Tag name prompt title"), - tr("Tag:", "Tag name prompt")); - if (tagName.isEmpty()) - return; - - std::string tag = "u." + tagName.toStdString(); - - http::client()->put_tag( - roomId_.toStdString(), tag, {}, [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error("Failed to add tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - }); - }); -} - -RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent) - : QWidget(parent) - , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} - , roomId_(std::move(room_id)) - , roomName_{QString::fromStdString(std::move(info.name))} - , isPressed_(false) - , unreadMsgCount_(0) - , unreadHighlightedMsgCount_(0) -{ - init(parent); -} - -void -RoomInfoListItem::resizeEvent(QResizeEvent *) -{ - // Update ripple's clipping path. - QPainterPath path; - path.addRect(0, 0, width(), height()); - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() > sidebarSizes.small) - setToolTip(""); - else - setToolTip(roomName_); - - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); -} - -void -RoomInfoListItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter p(this); - p.setRenderHint(QPainter::TextAntialiasing); - p.setRenderHint(QPainter::SmoothPixmapTransform); - p.setRenderHint(QPainter::Antialiasing); - - QFontMetrics metrics(QFont{}); - - QPen titlePen(titleColor_); - QPen subtitlePen(subtitleColor_); - - auto wm = getMetrics(QFont{}); - - QPixmap pixmap(avatar_->size() * p.device()->devicePixelRatioF()); - pixmap.setDevicePixelRatio(p.device()->devicePixelRatioF()); - if (isPressed_) { - p.fillRect(rect(), highlightedBackgroundColor_); - titlePen.setColor(highlightedTitleColor_); - subtitlePen.setColor(highlightedSubtitleColor_); - pixmap.fill(highlightedBackgroundColor_); - } else if (underMouse()) { - p.fillRect(rect(), hoverBackgroundColor_); - titlePen.setColor(hoverTitleColor_); - subtitlePen.setColor(hoverSubtitleColor_); - pixmap.fill(hoverBackgroundColor_); - } else { - p.fillRect(rect(), backgroundColor_); - titlePen.setColor(titleColor_); - subtitlePen.setColor(subtitleColor_); - pixmap.fill(backgroundColor_); - } - - avatar_->render(&pixmap, QPoint(), QRegion(), RenderFlags(DrawChildren)); - p.drawPixmap(QPoint(wm.padding, wm.padding), pixmap); - - // Description line with the default font. - int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2; - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() > sidebarSizes.small) { - QFont headingFont; - headingFont.setWeight(QFont::Medium); - p.setFont(headingFont); - p.setPen(titlePen); - - QFont tsFont; - tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int msgStampWidth = - QFontMetrics(tsFont).width(lastMsgInfo_.descriptiveTime) + 4; -#else - const int msgStampWidth = - QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.descriptiveTime) + 4; -#endif - // We use the full width of the widget if there is no unread msg bubble. - const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; - - // Name line. - QFontMetrics fontNameMetrics(headingFont); - int top_y = 2 * wm.padding + fontNameMetrics.ascent() / 2; - - const auto name = metrics.elidedText( - roomName(), - Qt::ElideRight, - (width() - wm.iconSize - 2 * wm.padding - msgStampWidth) * 0.8); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, top_y), name); - - if (roomType_ == RoomType::Joined) { - p.setFont(QFont{}); - p.setPen(subtitlePen); - - int descriptionLimit = std::max( - 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); - auto description = - metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); - - // We show the last message timestamp. - p.save(); - if (isPressed_) { - p.setPen(QPen(highlightedTimestampColor_)); - } else if (underMouse()) { - p.setPen(QPen(hoverTimestampColor_)); - } else { - p.setPen(QPen(timestampColor_)); - } - - p.setFont(tsFont); - p.drawText(QPoint(width() - wm.padding - msgStampWidth, top_y), - lastMsgInfo_.descriptiveTime); - p.restore(); - } else { - int btnWidth = (width() - wm.iconSize - 6 * wm.padding) / 2; - - acceptBtnRegion_ = QRectF(wm.inviteBtnX, wm.inviteBtnY, btnWidth, 20); - declineBtnRegion_ = QRectF( - wm.inviteBtnX + btnWidth + 2 * wm.padding, wm.inviteBtnY, btnWidth, 20); - - QPainterPath acceptPath; - acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10); - - p.setPen(Qt::NoPen); - p.fillPath(acceptPath, btnColor_); - p.drawPath(acceptPath); - - QPainterPath declinePath; - declinePath.addRoundedRect(declineBtnRegion_, 10, 10); - - p.setPen(Qt::NoPen); - p.fillPath(declinePath, btnColor_); - p.drawPath(declinePath); - - p.setPen(QPen(btnTextColor_)); - p.setFont(QFont{}); - p.drawText(acceptBtnRegion_, - Qt::AlignCenter, - metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth)); - p.drawText(declineBtnRegion_, - Qt::AlignCenter, - metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth)); - } - } - - p.setPen(Qt::NoPen); - - if (unreadMsgCount_ > 0) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - if (unreadHighlightedMsgCount_ > 0) { - brush.setColor(mentionedColor()); - } else { - brush.setColor(bubbleBgColor()); - } - - if (isPressed_) - brush.setColor(bubbleFgColor()); - - p.setBrush(brush); - p.setPen(Qt::NoPen); - p.setFont(unreadCountFont_); - - // Extra space on the x-axis to accomodate the extra character space - // inside the bubble. - const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed - ? QFontMetrics(p.font()).averageCharWidth() - : 0; - - QRectF r(width() - bubbleDiameter_ - wm.padding - x_width, - bottom_y - bubbleDiameter_ / 2 - 5, - bubbleDiameter_ + x_width, - bubbleDiameter_); - - if (width() == sidebarSizes.small) - r = QRectF(width() - bubbleDiameter_ - 5, - height() - bubbleDiameter_ - 5, - bubbleDiameter_ + x_width, - bubbleDiameter_); - - p.setPen(Qt::NoPen); - p.drawEllipse(r); - - p.setPen(QPen(bubbleFgColor())); - - if (isPressed_) - p.setPen(QPen(bubbleBgColor())); - - auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed - ? QString("99+") - : QString::number(unreadMsgCount_); - - p.setBrush(Qt::NoBrush); - p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt); - } - - if (!isPressed_ && hasUnreadMessages_) { - QPen pen; - pen.setWidth(wm.unreadLineWidth); - pen.setColor(highlightedBackgroundColor_); - - p.setPen(pen); - p.drawLine(0, wm.unreadLineOffset, 0, height() - wm.unreadLineOffset); - } -} - -void -RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount) -{ - unreadMsgCount_ = count; - unreadHighlightedMsgCount_ = highlightedCount; - update(); -} - -enum NotificationImportance : short -{ - ImportanceDisabled = -1, - AllEventsRead = 0, - NewMessage = 1, - NewMentions = 2, - Invite = 3 -}; - -short int -RoomInfoListItem::calculateImportance() const -{ - // Returns the degree of importance of the unread messages in the room. - // If sorting by importance is disabled in settings, this only ever - // returns ImportanceDisabled or Invite - if (isInvite()) { - return Invite; - } else if (!ChatPage::instance()->userSettings()->sortByImportance()) { - return ImportanceDisabled; - } else if (unreadHighlightedMsgCount_) { - return NewMentions; - } else if (unreadMsgCount_) { - return NewMessage; - } else { - return AllEventsRead; - } -} - -void -RoomInfoListItem::setPressedState(bool state) -{ - if (isPressed_ != state) { - isPressed_ = state; - update(); - } -} - -void -RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event) -{ - Q_UNUSED(event); - - if (roomType_ == RoomType::Invited) - return; - - menu_->popup(event->globalPos()); -} - -void -RoomInfoListItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() == Qt::RightButton) { - QWidget::mousePressEvent(event); - return; - } else if (event->buttons() == Qt::LeftButton) { - if (roomType_ == RoomType::Invited) { - const auto point = event->pos(); - - if (acceptBtnRegion_.contains(point)) - emit acceptInvite(roomId_); - - if (declineBtnRegion_.contains(point)) - emit declineInvite(roomId_); - - return; - } - - emit clicked(roomId_); - - setPressedState(true); - - // Ripple on mouse position by default. - QPoint pos = event->pos(); - qreal radiusEndValue = static_cast<qreal>(width()) / 3; - - Ripple *ripple = new Ripple(pos); - - ripple->setRadiusEndValue(radiusEndValue); - ripple->setOpacityStartValue(0.15); - ripple->setColor(QColor("white")); - ripple->radiusAnimation()->setDuration(200); - ripple->opacityAnimation()->setDuration(400); - - ripple_overlay_->addRipple(ripple); - } -} - -void -RoomInfoListItem::setAvatar(const QString &avatar_url) -{ - if (avatar_url.isEmpty()) - avatar_->setLetter(utils::firstChar(roomName_)); - else - avatar_->setImage(avatar_url); -} - -void -RoomInfoListItem::setDescriptionMessage(const DescInfo &info) -{ - lastMsgInfo_ = info; - update(); -} diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h deleted file mode 100644
index a5e0009e..00000000 --- a/src/RoomInfoListItem.h +++ /dev/null
@@ -1,210 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QAction> -#include <QDateTime> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx/responses/sync.hpp> - -#include "CacheStructs.h" -#include "UserSettingsPage.h" -#include "ui/Avatar.h" - -class QMenu; -class RippleOverlay; - -class RoomInfoListItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE - setHighlightedBackgroundColor) - Q_PROPERTY( - QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - - Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor) - Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor) - - Q_PROPERTY(QColor titleColor READ titleColor WRITE setTitleColor) - Q_PROPERTY(QColor subtitleColor READ subtitleColor WRITE setSubtitleColor) - - Q_PROPERTY(QColor timestampColor READ timestampColor WRITE setTimestampColor) - Q_PROPERTY(QColor highlightedTimestampColor READ highlightedTimestampColor WRITE - setHighlightedTimestampColor) - Q_PROPERTY(QColor hoverTimestampColor READ hoverTimestampColor WRITE setHoverTimestampColor) - - Q_PROPERTY( - QColor highlightedTitleColor READ highlightedTitleColor WRITE setHighlightedTitleColor) - Q_PROPERTY(QColor highlightedSubtitleColor READ highlightedSubtitleColor WRITE - setHighlightedSubtitleColor) - - Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor) - Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor) - - Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor) - Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor) - Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) - -public: - RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr); - - void updateUnreadMessageCount(int count, int highlightedCount); - void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; - - short int calculateImportance() const; - - QString roomId() { return roomId_; } - bool isPressed() const { return isPressed_; } - int unreadMessageCount() const { return unreadMsgCount_; } - - void setAvatar(const QString &avatar_url); - void setDescriptionMessage(const DescInfo &info); - DescInfo lastMessageInfo() const { return lastMsgInfo_; } - - QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } - QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } - QColor hoverTitleColor() const { return hoverTitleColor_; } - QColor hoverSubtitleColor() const { return hoverSubtitleColor_; } - QColor hoverTimestampColor() const { return hoverTimestampColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor highlightedTitleColor() const { return highlightedTitleColor_; } - QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; } - QColor highlightedTimestampColor() const { return highlightedTimestampColor_; } - - QColor titleColor() const { return titleColor_; } - QColor subtitleColor() const { return subtitleColor_; } - QColor timestampColor() const { return timestampColor_; } - QColor btnColor() const { return btnColor_; } - QColor btnTextColor() const { return btnTextColor_; } - - QColor bubbleFgColor() const { return bubbleFgColor_; } - QColor bubbleBgColor() const { return bubbleBgColor_; } - QColor mentionedColor() const { return mentionedFontColor_; } - - void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } - void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } - void setHoverSubtitleColor(QColor &color) { hoverSubtitleColor_ = color; } - void setHoverTitleColor(QColor &color) { hoverTitleColor_ = color; } - void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; } - void setBackgroundColor(QColor &color) { backgroundColor_ = color; } - void setTimestampColor(QColor &color) { timestampColor_ = color; } - - void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; } - void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; } - void setHighlightedTimestampColor(QColor &color) { highlightedTimestampColor_ = color; } - - void setTitleColor(QColor &color) { titleColor_ = color; } - void setSubtitleColor(QColor &color) { subtitleColor_ = color; } - - void setBtnColor(QColor &color) { btnColor_ = color; } - void setBtnTextColor(QColor &color) { btnTextColor_ = color; } - - void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; } - void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; } - void setMentionedColor(QColor &color) { mentionedFontColor_ = color; } - - void setRoomName(const QString &name) { roomName_ = name; } - void setRoomType(bool isInvite) - { - if (isInvite) - roomType_ = RoomType::Invited; - else - roomType_ = RoomType::Joined; - } - - bool isInvite() const { return roomType_ == RoomType::Invited; } - void setReadState(bool hasUnreadMessages) - { - if (hasUnreadMessages_ != hasUnreadMessages) { - hasUnreadMessages_ = hasUnreadMessages; - update(); - } - } - -signals: - void clicked(const QString &room_id); - void leaveRoom(const QString &room_id); - void acceptInvite(const QString &room_id); - void declineInvite(const QString &room_id); - -public slots: - void setPressedState(bool state); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - void init(QWidget *parent); - QString roomName() { return roomName_; } - - RippleOverlay *ripple_overlay_; - Avatar *avatar_; - - enum class RoomType - { - Joined, - Invited, - }; - - RoomType roomType_ = RoomType::Joined; - - // State information for the invited rooms. - mtx::responses::InvitedRoom invitedRoom_; - - QString roomId_; - QString roomName_; - - DescInfo lastMsgInfo_; - - QMenu *menu_; - QAction *leaveRoom_; - - bool isPressed_ = false; - bool hasUnreadMessages_ = true; - - int unreadMsgCount_ = 0; - int unreadHighlightedMsgCount_ = 0; - - QColor highlightedBackgroundColor_; - QColor hoverBackgroundColor_; - QColor backgroundColor_; - - QColor highlightedTitleColor_; - QColor highlightedSubtitleColor_; - - QColor titleColor_; - QColor subtitleColor_; - - QColor hoverTitleColor_; - QColor hoverSubtitleColor_; - - QColor btnColor_; - QColor btnTextColor_; - - QRectF acceptBtnRegion_; - QRectF declineBtnRegion_; - - // Fonts - QColor mentionedFontColor_; - QFont unreadCountFont_; - int bubbleDiameter_; - - QColor timestampColor_; - QColor highlightedTimestampColor_; - QColor hoverTimestampColor_; - - QColor bubbleBgColor_; - QColor bubbleFgColor_; - - friend struct room_sort; -}; diff --git a/src/RoomList.cpp b/src/RoomList.cpp deleted file mode 100644
index 8a807e71..00000000 --- a/src/RoomList.cpp +++ /dev/null
@@ -1,540 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <limits> -#include <set> - -#include <QObject> -#include <QPainter> -#include <QScroller> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include "Logging.h" -#include "MainWindow.h" -#include "RoomInfoListItem.h" -#include "RoomList.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/OverlayModal.h" - -RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent) - : QWidget(parent) -{ - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - scrollArea_ = new QScrollArea(this); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - scrollArea_->setAttribute(Qt::WA_AcceptTouchEvents); - - QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); - QScroller::grabGesture(scrollArea_, QScroller::LeftMouseButtonGesture); - -// The scrollbar on macOS will hide itself when not active so it won't interfere -// with the content. -#if not defined(Q_OS_MAC) - scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); -#endif - - scrollAreaContents_ = new QWidget(this); - scrollAreaContents_->setObjectName("roomlist_area"); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); - contentsLayout_->setAlignment(Qt::AlignTop); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addWidget(scrollArea_); - - connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar); - connect(userSettings.data(), - &UserSettings::roomSortingChanged, - this, - &RoomList::sortRoomsByLastMessage); -} - -void -RoomList::addRoom(const QString &room_id, const RoomInfo &info) -{ - auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); - room_item->setRoomName(QString::fromStdString(std::move(info.name))); - - connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); - connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) { - MainWindow::instance()->openLeaveRoomDialog(room_id); - }); - - QSharedPointer<RoomInfoListItem> roomWidget(room_item, &QObject::deleteLater); - rooms_.emplace(room_id, roomWidget); - rooms_sort_cache_.push_back(roomWidget); - - if (!info.avatar_url.empty()) - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - - int pos = contentsLayout_->count() - 1; - contentsLayout_->insertWidget(pos, room_item); -} - -void -RoomList::updateAvatar(const QString &room_id, const QString &url) -{ - emit updateRoomAvatarCb(room_id, url); -} - -void -RoomList::removeRoom(const QString &room_id, bool reset) -{ - auto roomIt = rooms_.find(room_id); - if (roomIt == rooms_.end()) { - return; - } - - for (auto roomSortIt = rooms_sort_cache_.begin(); roomSortIt != rooms_sort_cache_.end(); - ++roomSortIt) { - if (roomIt->second == *roomSortIt) { - rooms_sort_cache_.erase(roomSortIt); - break; - } - } - rooms_.erase(room_id); - - if (rooms_.empty() || !reset) - return; - - auto room = firstRoom(); - - if (room.second.isNull()) - return; - - room.second->setPressedState(true); - emit roomChanged(room.first); -} - -void -RoomList::updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount); - - calculateUnreadMessageCount(); - - sortRoomsByLastMessage(); -} - -void -RoomList::calculateUnreadMessageCount() -{ - int total_unread_msgs = 0; - - for (const auto &room : rooms_) { - if (!room.second.isNull()) - total_unread_msgs += room.second->unreadMessageCount(); - } - - emit totalUnreadMessageCountUpdated(total_unread_msgs); -} - -void -RoomList::initialize(const QMap<QString, RoomInfo> &info) -{ - nhlog::ui()->info("initialize room list"); - - rooms_.clear(); - - // prevent flickering and save time sorting over and over again - setUpdatesEnabled(false); - 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 it = info.begin(); it != info.end(); it++) - updateRoomDescription(it.key(), it.value().msgInfo); - - setUpdatesEnabled(true); - - if (rooms_.empty()) - return; - - sortRoomsByLastMessage(); - - auto room = firstRoom(); - if (room.second.isNull()) - return; - - room.second->setPressedState(true); - emit roomChanged(room.first); -} - -void -RoomList::cleanupInvites(const std::map<QString, bool> &invites) -{ - if (invites.size() == 0) - return; - - utils::erase_if(rooms_, [invites](auto &room) { - auto room_id = room.first; - auto item = room.second; - - if (!item) - return false; - - return item->isInvite() && (invites.find(room_id) == invites.end()); - }); -} - -void -RoomList::sync(const std::map<QString, RoomInfo> &info) - -{ - for (const auto &room : info) - updateRoom(room.first, room.second); - - if (!info.empty()) - sortRoomsByLastMessage(); -} - -void -RoomList::highlightSelectedRoom(const QString &room_id) -{ - emit roomChanged(room_id); - - if (!roomExists(room_id)) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - for (auto const &room : rooms_) { - if (room.second.isNull()) - continue; - - if (room.first != room_id) { - room.second->setPressedState(false); - } else { - room.second->setPressedState(true); - scrollArea_->ensureWidgetVisible(room.second.data()); - } - } - - selectedRoom_ = room_id; -} - -void -RoomList::nextRoom() -{ - for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) { - auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); - - if (!room) - continue; - - if (room->roomId() == selectedRoom_) { - auto nextRoom = qobject_cast<RoomInfoListItem *>( - contentsLayout_->itemAt(ii + 1)->widget()); - - // Not a room message. - if (!nextRoom || nextRoom->isInvite()) - return; - - emit roomChanged(nextRoom->roomId()); - if (!roomExists(nextRoom->roomId())) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - room->setPressedState(false); - nextRoom->setPressedState(true); - - scrollArea_->ensureWidgetVisible(nextRoom); - selectedRoom_ = nextRoom->roomId(); - return; - } - } -} - -void -RoomList::previousRoom() -{ - for (int ii = 1; ii < contentsLayout_->count(); ++ii) { - auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); - - if (!room) - continue; - - if (room->roomId() == selectedRoom_) { - auto nextRoom = qobject_cast<RoomInfoListItem *>( - contentsLayout_->itemAt(ii - 1)->widget()); - - // Not a room message. - if (!nextRoom || nextRoom->isInvite()) - return; - - emit roomChanged(nextRoom->roomId()); - if (!roomExists(nextRoom->roomId())) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - room->setPressedState(false); - nextRoom->setPressedState(true); - - scrollArea_->ensureWidgetVisible(nextRoom); - selectedRoom_ = nextRoom->roomId(); - return; - } - } -} - -void -RoomList::updateRoomAvatar(const QString &roomid, const QString &img) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("avatar update on non-existent room_id: {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->setAvatar(img); - - // Used to inform other widgets for the new image data. - emit roomAvatarChanged(roomid, img); -} - -void -RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("description update on non-existent room_id: {}, {}", - roomid.toStdString(), - info.body.toStdString()); - return; - } - - rooms_[roomid]->setDescriptionMessage(info); - - if (underMouse()) { - // When the user hover out of the roomlist a sort will be triggered. - isSortPending_ = true; - return; - } - - isSortPending_ = false; - - emit sortRoomsByLastMessage(); -} - -struct room_sort -{ - bool operator()(const QSharedPointer<RoomInfoListItem> &a, - const QSharedPointer<RoomInfoListItem> &b) const - { - // Sort by "importance" (i.e. invites before mentions before - // notifs before new events before old events), then secondly - // by recency. - - // Checking importance first - const auto a_importance = a->calculateImportance(); - const auto b_importance = b->calculateImportance(); - if (a_importance != b_importance) { - return a_importance > b_importance; - } - - // Now sort by recency - // Zero if empty, otherwise the time that the event occured - const uint64_t a_recency = - a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp; - const uint64_t b_recency = - b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp; - return a_recency > b_recency; - } -}; - -void -RoomList::sortRoomsByLastMessage() -{ - isSortPending_ = false; - - std::stable_sort(begin(rooms_sort_cache_), end(rooms_sort_cache_), room_sort{}); - - int newIndex = 0; - for (const auto &roomWidget : rooms_sort_cache_) { - const auto currentIndex = contentsLayout_->indexOf(roomWidget.data()); - - if (currentIndex != newIndex) { - contentsLayout_->removeWidget(roomWidget.data()); - contentsLayout_->insertWidget(newIndex, roomWidget.data()); - } - newIndex++; - } -} - -void -RoomList::leaveEvent(QEvent *event) -{ - if (isSortPending_) - QTimer::singleShot(700, this, &RoomList::sortRoomsByLastMessage); - - QWidget::leaveEvent(event); -} - -void -RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias) -{ - joinRoomModal_->hide(); - - if (isJoining) - emit joinRoom(roomAlias); -} - -void -RoomList::removeFilter(const std::set<QString> &roomsToHide) -{ - setUpdatesEnabled(false); - for (int i = 0; i < contentsLayout_->count(); i++) { - auto widget = - qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - if (widget) { - if (roomsToHide.find(widget->roomId()) == roomsToHide.end()) - widget->show(); - else - widget->hide(); - } - } - setUpdatesEnabled(true); -} - -void -RoomList::applyFilter(const std::set<QString> &filter) -{ - // Disabling paint updates will resolve issues with screen flickering on big room lists. - setUpdatesEnabled(false); - - for (int i = 0; i < contentsLayout_->count(); i++) { - // If filter contains the room for the current RoomInfoListItem, - // show the list item, otherwise hide it - auto listitem = - qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (!listitem) - continue; - - if (filter.find(listitem->roomId()) != filter.end()) - listitem->show(); - else - listitem->hide(); - } - - setUpdatesEnabled(true); - - // If the already selected room is part of the group, make sure it's visible. - if (!selectedRoom_.isEmpty() && (filter.find(selectedRoom_) != filter.end())) - return; - - selectFirstVisibleRoom(); -} - -void -RoomList::selectFirstVisibleRoom() -{ - for (int i = 0; i < contentsLayout_->count(); i++) { - auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (item && item->isVisible()) { - highlightSelectedRoom(item->roomId()); - break; - } - } -} - -void -RoomList::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -RoomList::updateRoom(const QString &room_id, const RoomInfo &info) -{ - 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 -RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) -{ - 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); - - QSharedPointer<RoomInfoListItem> roomWidget(room_item); - rooms_.emplace(room_id, roomWidget); - rooms_sort_cache_.push_back(roomWidget); - - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - - int pos = contentsLayout_->count() - 1; - contentsLayout_->insertWidget(pos, room_item); -} - -std::pair<QString, QSharedPointer<RoomInfoListItem>> -RoomList::firstRoom() const -{ - for (int i = 0; i < contentsLayout_->count(); i++) { - auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (item) { - auto topRoom = rooms_.find(item->roomId()); - if (topRoom != rooms_.end()) { - return std::pair<QString, QSharedPointer<RoomInfoListItem>>( - item->roomId(), topRoom->second); - } - } - } - - return {}; -} - -void -RoomList::updateReadStatus(const std::map<QString, bool> &status) -{ - for (const auto &room : status) { - if (roomExists(room.first)) { - auto item = rooms_.at(room.first); - - if (item) - item->setReadState(room.second); - } - } -} diff --git a/src/RoomList.h b/src/RoomList.h deleted file mode 100644
index 74152c55..00000000 --- a/src/RoomList.h +++ /dev/null
@@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QPushButton> -#include <QScrollArea> -#include <QSharedPointer> -#include <QVBoxLayout> -#include <QWidget> - -#include <set> - -#include "CacheStructs.h" -#include "UserSettingsPage.h" - -class LeaveRoomDialog; -class OverlayModal; -class RoomInfoListItem; -class Sync; -struct DescInfo; -struct RoomInfo; - -class RoomList : public QWidget -{ - Q_OBJECT - -public: - explicit RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); - - void initialize(const QMap<QString, RoomInfo> &info); - void sync(const std::map<QString, RoomInfo> &info); - - void clear() - { - rooms_.clear(); - rooms_sort_cache_.clear(); - }; - void updateAvatar(const QString &room_id, const QString &url); - - 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); - //! Hide rooms that are not present in the given filter. - void applyFilter(const std::set<QString> &rooms); - //! Show all the available rooms. - void removeFilter(const std::set<QString> &roomsToHide); - void updateRoom(const QString &room_id, const RoomInfo &info); - void cleanupInvites(const std::map<QString, bool> &invites); - -signals: - void roomChanged(const QString &room_id); - void totalUnreadMessageCountUpdated(int count); - void acceptInvite(const QString &room_id); - void declineInvite(const QString &room_id); - void roomAvatarChanged(const QString &room_id, const QString &img); - void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QString &img); - -public slots: - void updateRoomAvatar(const QString &roomid, const QString &img); - void highlightSelectedRoom(const QString &room_id); - void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); - void updateRoomDescription(const QString &roomid, const DescInfo &info); - void closeJoinRoomDialog(bool isJoining, QString roomAlias); - void updateReadStatus(const std::map<QString, bool> &status); - void nextRoom(); - void previousRoom(); - -protected: - void paintEvent(QPaintEvent *event) override; - void leaveEvent(QEvent *event) override; - -private slots: - void sortRoomsByLastMessage(); - -private: - //! Return the first non-null room. - std::pair<QString, QSharedPointer<RoomInfoListItem>> firstRoom() const; - void calculateUnreadMessageCount(); - bool roomExists(const QString &room_id) { return rooms_.find(room_id) != rooms_.end(); } - //! Select the first visible room in the room list. - void selectFirstVisibleRoom(); - - QVBoxLayout *topLayout_; - QVBoxLayout *contentsLayout_; - QScrollArea *scrollArea_; - QWidget *scrollAreaContents_; - - QPushButton *joinRoomButton_; - - OverlayModal *joinRoomModal_; - - std::map<QString, QSharedPointer<RoomInfoListItem>> rooms_; - std::vector<QSharedPointer<RoomInfoListItem>> rooms_sort_cache_; - QString selectedRoom_; - - bool isSortPending_ = false; -}; diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp deleted file mode 100644
index 0b7756f0..00000000 --- a/src/SideBarActions.cpp +++ /dev/null
@@ -1,120 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QIcon> -#include <QPainter> -#include <QResizeEvent> -#include <QStyle> -#include <QStyleOption> - -#include <mtx/requests.hpp> - -#include "Config.h" -#include "MainWindow.h" -#include "SideBarActions.h" -#include "Splitter.h" -#include "ui/FlatButton.h" -#include "ui/Menu.h" - -SideBarActions::SideBarActions(QWidget *parent) - : QWidget{parent} -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - const int fontHeight = QFontMetrics(f).height(); - const int contentHeight = fontHeight * 2.5; - - setFixedHeight(contentHeight); - - layout_ = new QHBoxLayout(this); - layout_->setMargin(0); - - QIcon settingsIcon; - settingsIcon.addFile(":/icons/icons/ui/settings.png"); - - QIcon createRoomIcon; - createRoomIcon.addFile(":/icons/icons/ui/add-square-button.png"); - - QIcon joinRoomIcon; - joinRoomIcon.addFile(":/icons/icons/ui/speech-bubbles-comment-option.png"); - - settingsBtn_ = new FlatButton(this); - settingsBtn_->setToolTip(tr("User settings")); - settingsBtn_->setIcon(settingsIcon); - settingsBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); - settingsBtn_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - addMenu_ = new Menu(this); - createRoomAction_ = new QAction(tr("Create new room"), this); - joinRoomAction_ = new QAction(tr("Join a room"), this); - - connect(joinRoomAction_, &QAction::triggered, this, [this]() { - MainWindow::instance()->openJoinRoomDialog( - [this](const QString &room_id) { emit joinRoom(room_id); }); - }); - - connect(createRoomAction_, &QAction::triggered, this, [this]() { - MainWindow::instance()->openCreateRoomDialog( - [this](const mtx::requests::CreateRoom &req) { emit createRoom(req); }); - }); - - addMenu_->addAction(createRoomAction_); - addMenu_->addAction(joinRoomAction_); - - createRoomBtn_ = new FlatButton(this); - createRoomBtn_->setToolTip(tr("Start a new chat")); - createRoomBtn_->setIcon(createRoomIcon); - createRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); - createRoomBtn_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - connect(createRoomBtn_, &QPushButton::clicked, this, [this]() { - auto pos = mapToGlobal(createRoomBtn_->pos()); - auto padding = conf::sidebarActions::iconSize / 2; - - addMenu_->popup( - QPoint(pos.x() + padding, pos.y() - padding - addMenu_->sizeHint().height())); - }); - - roomDirectory_ = new FlatButton(this); - roomDirectory_->setToolTip(tr("Room directory")); - roomDirectory_->setEnabled(false); - roomDirectory_->setIcon(joinRoomIcon); - roomDirectory_->setCornerRadius(conf::sidebarActions::iconSize / 2); - roomDirectory_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - layout_->addWidget(createRoomBtn_); - layout_->addWidget(roomDirectory_); - layout_->addWidget(settingsBtn_); - - connect(settingsBtn_, &QPushButton::clicked, this, &SideBarActions::showSettings); -} - -void -SideBarActions::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() <= sidebarSizes.small) { - roomDirectory_->hide(); - createRoomBtn_->hide(); - } else { - roomDirectory_->show(); - createRoomBtn_->show(); - } -} - -void -SideBarActions::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/SideBarActions.h b/src/SideBarActions.h deleted file mode 100644
index 566aa76b..00000000 --- a/src/SideBarActions.h +++ /dev/null
@@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QAction> -#include <QHBoxLayout> -#include <QWidget> - -namespace mtx { -namespace requests { -struct CreateRoom; -} -} - -class Menu; -class FlatButton; -class QResizeEvent; - -class SideBarActions : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) - -public: - SideBarActions(QWidget *parent = nullptr); - - QColor borderColor() const { return borderColor_; } - void setBorderColor(QColor &color) { borderColor_ = color; } - -signals: - void showSettings(); - void joinRoom(const QString &room); - void createRoom(const mtx::requests::CreateRoom &request); - -protected: - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -private: - QHBoxLayout *layout_; - - Menu *addMenu_; - QAction *createRoomAction_; - QAction *joinRoomAction_; - - FlatButton *settingsBtn_; - FlatButton *createRoomBtn_; - FlatButton *roomDirectory_; - - QColor borderColor_; -}; diff --git a/src/Splitter.cpp b/src/Splitter.cpp deleted file mode 100644
index 15e3f5c5..00000000 --- a/src/Splitter.cpp +++ /dev/null
@@ -1,166 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QSettings> - -#include "Logging.h" -#include "Splitter.h" - -constexpr auto MaxWidth = (1 << 24) - 1; - -Splitter::Splitter(QWidget *parent) - : QSplitter(parent) - , sz_{splitter::calculateSidebarSizes(QFont{})} -{ - connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); - setChildrenCollapsible(false); -} - -void -Splitter::restoreSizes(int fallback) -{ - QSettings settings; - int savedWidth = settings.value("sidebar/width").toInt(); - - auto left = widget(0); - if (savedWidth <= 0) { - hideSidebar(); - return; - } else if (savedWidth <= sz_.small) { - if (left) { - left->setMinimumWidth(sz_.small); - left->setMaximumWidth(sz_.small); - return; - } - } else if (savedWidth < sz_.normal) { - savedWidth = sz_.normal; - } - - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - setSizes({savedWidth, fallback - savedWidth}); - - setStretchFactor(0, 0); - setStretchFactor(1, 1); -} - -Splitter::~Splitter() -{ - auto left = widget(0); - - if (left) { - QSettings settings; - settings.setValue("sidebar/width", left->width()); - } -} - -void -Splitter::onSplitterMoved(int pos, int index) -{ - Q_UNUSED(pos); - Q_UNUSED(index); - - auto s = sizes(); - - if (s.count() < 2) { - nhlog::ui()->warn("Splitter needs at least two children"); - return; - } - - if (s[0] == sz_.normal) { - rightMoveCount_ += 1; - - if (rightMoveCount_ > moveEventLimit_) { - auto left = widget(0); - auto cursorPosition = left->mapFromGlobal(QCursor::pos()); - - // if we are coming from the right, the cursor should - // end up on the first widget. - if (left->rect().contains(cursorPosition)) { - left->setMinimumWidth(sz_.small); - left->setMaximumWidth(sz_.small); - - rightMoveCount_ = 0; - } - } - } else if (s[0] == sz_.small) { - leftMoveCount_ += 1; - - if (leftMoveCount_ > moveEventLimit_) { - auto left = widget(0); - auto right = widget(1); - auto cursorPosition = right->mapFromGlobal(QCursor::pos()); - - // We move the start a little further so the transition isn't so abrupt. - auto extended = right->rect(); - extended.translate(100, 0); - - // if we are coming from the left, the cursor should - // end up on the second widget. - if (extended.contains(cursorPosition) && - right->size().width() >= sz_.collapsePoint + sz_.normal) { - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - - leftMoveCount_ = 0; - } - } - } -} - -void -Splitter::hideSidebar() -{ - auto left = widget(0); - if (left) - left->hide(); -} - -void -Splitter::showChatView() -{ - auto left = widget(0); - auto right = widget(1); - - if (right->isHidden()) { - left->hide(); - right->show(); - - // Restore previous size. - if (left->minimumWidth() == sz_.small) { - left->setMinimumWidth(sz_.small); - left->setMaximumWidth(sz_.small); - } else { - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - } - } -} - -void -Splitter::showFullRoomList() -{ - auto left = widget(0); - auto right = widget(1); - - right->hide(); - - left->show(); - left->setMaximumWidth(MaxWidth); -} - -splitter::SideBarSizes -splitter::calculateSidebarSizes(const QFont &f) -{ - const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); - - SideBarSizes sz; - sz.small = std::ceil(3.8 * height); - sz.normal = std::ceil(16 * height); - sz.groups = std::ceil(3 * height); - sz.collapsePoint = 2 * sz.normal; - - return sz; -} diff --git a/src/Splitter.h b/src/Splitter.h deleted file mode 100644
index 94622f89..00000000 --- a/src/Splitter.h +++ /dev/null
@@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QSplitter> - -namespace splitter { -struct SideBarSizes -{ - int small; - int normal; - int groups; - int collapsePoint; -}; - -SideBarSizes -calculateSidebarSizes(const QFont &f); -} - -class Splitter : public QSplitter -{ - Q_OBJECT -public: - explicit Splitter(QWidget *parent = nullptr); - ~Splitter() override; - - void restoreSizes(int fallback); - -public slots: - void hideSidebar(); - void showFullRoomList(); - void showChatView(); - -signals: - void hiddenSidebar(); - -private: - void onSplitterMoved(int pos, int index); - - int moveEventLimit_ = 50; - - int leftMoveCount_ = 0; - int rightMoveCount_ = 0; - - splitter::SideBarSizes sz_; -}; diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp deleted file mode 100644
index 3d526b8b..00000000 --- a/src/UserInfoWidget.cpp +++ /dev/null
@@ -1,219 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QInputDialog> -#include <QLabel> -#include <QMenu> -#include <QPainter> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include <iostream> - -#include "ChatPage.h" -#include "Config.h" -#include "MainWindow.h" -#include "Splitter.h" -#include "UserInfoWidget.h" -#include "UserSettingsPage.h" -#include "ui/Avatar.h" -#include "ui/FlatButton.h" -#include "ui/OverlayModal.h" - -UserInfoWidget::UserInfoWidget(QWidget *parent) - : QWidget(parent) - , display_name_("User") - , user_id_("@user:homeserver.org") -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - const int fontHeight = QFontMetrics(f).height(); - const int widgetMargin = fontHeight / 3; - const int contentHeight = fontHeight * 3; - - logoutButtonSize_ = std::min(fontHeight, 20); - - setFixedHeight(contentHeight + widgetMargin); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(widgetMargin); - - avatarLayout_ = new QHBoxLayout(); - textLayout_ = new QVBoxLayout(); - textLayout_->setSpacing(widgetMargin / 2); - textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - - userAvatar_ = new Avatar(this, fontHeight * 2.5); - userAvatar_->setObjectName("userAvatar"); - userAvatar_->setLetter(QChar('?')); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - nameFont.setWeight(QFont::Medium); - - displayNameLabel_ = new QLabel(this); - displayNameLabel_->setFont(nameFont); - displayNameLabel_->setObjectName("displayNameLabel"); - displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); - - userIdLabel_ = new QLabel(this); - userIdLabel_->setFont(f); - userIdLabel_->setObjectName("userIdLabel"); - userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); - - avatarLayout_->addWidget(userAvatar_); - textLayout_->addWidget(displayNameLabel_, 0, Qt::AlignBottom); - textLayout_->addWidget(userIdLabel_, 0, Qt::AlignTop); - - topLayout_->addLayout(avatarLayout_); - topLayout_->addLayout(textLayout_); - topLayout_->addStretch(1); - - buttonLayout_ = new QHBoxLayout(); - buttonLayout_->setSpacing(0); - buttonLayout_->setMargin(0); - - logoutButton_ = new FlatButton(this); - logoutButton_->setToolTip(tr("Logout")); - logoutButton_->setCornerRadius(logoutButtonSize_ / 2); - - QIcon icon; - icon.addFile(":/icons/icons/ui/power-button-off.png"); - - logoutButton_->setIcon(icon); - logoutButton_->setIconSize(QSize(logoutButtonSize_, logoutButtonSize_)); - - buttonLayout_->addWidget(logoutButton_); - - topLayout_->addLayout(buttonLayout_); - - // Show the confirmation dialog. - connect(logoutButton_, &QPushButton::clicked, this, []() { - MainWindow::instance()->openLogoutDialog(); - }); - - menu = new QMenu(this); - - auto setStatusAction = menu->addAction(tr("Set custom status message")); - connect(setStatusAction, &QAction::triggered, this, [this]() { - bool ok = false; - QString text = QInputDialog::getText(this, - tr("Custom status message"), - tr("Status:"), - QLineEdit::Normal, - ChatPage::instance()->status(), - &ok); - if (ok) - ChatPage::instance()->setStatus(text); - }); - - auto userProfileAction = menu->addAction(tr("User Profile Settings")); - connect( - userProfileAction, &QAction::triggered, this, [this]() { emit openGlobalUserProfile(); }); - -#if 0 // disable presence menu until issues in synapse are resolved - auto setAutoPresence = menu->addAction(tr("Set presence automatically")); - connect(setAutoPresence, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence( - UserSettings::Presence::AutomaticPresence); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setOnline = menu->addAction(tr("Online")); - connect(setOnline, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Online); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setUnavailable = menu->addAction(tr("Unavailable")); - connect(setUnavailable, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence( - UserSettings::Presence::Unavailable); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setOffline = menu->addAction(tr("Offline")); - connect(setOffline, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Offline); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); -#endif -} - -void -UserInfoWidget::contextMenuEvent(QContextMenuEvent *event) -{ - menu->popup(event->globalPos()); -} - -void -UserInfoWidget::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - const auto sz = splitter::calculateSidebarSizes(QFont{}); - - if (width() <= sz.small) { - topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0); - - userAvatar_->hide(); - displayNameLabel_->hide(); - userIdLabel_->hide(); - } else { - topLayout_->setMargin(5); - userAvatar_->show(); - displayNameLabel_->show(); - userIdLabel_->show(); - } - - QWidget::resizeEvent(event); -} - -void -UserInfoWidget::reset() -{ - displayNameLabel_->setText(""); - userIdLabel_->setText(""); - userAvatar_->setLetter(QChar('?')); -} - -void -UserInfoWidget::setDisplayName(const QString &name) -{ - if (name.isEmpty()) - display_name_ = user_id_.split(':')[0].split('@')[1]; - else - display_name_ = name; - - displayNameLabel_->setText(display_name_); - userAvatar_->setLetter(QChar(display_name_[0])); - update(); -} - -void -UserInfoWidget::setUserId(const QString &userid) -{ - user_id_ = userid; - userIdLabel_->setText(userid); - update(); -} - -void -UserInfoWidget::setAvatar(const QString &url) -{ - userAvatar_->setImage(url); - update(); -} - -void -UserInfoWidget::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h deleted file mode 100644
index 5aec1cda..00000000 --- a/src/UserInfoWidget.h +++ /dev/null
@@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -class Avatar; -class FlatButton; -class OverlayModal; - -class QLabel; -class QHBoxLayout; -class QVBoxLayout; -class QMenu; - -class UserInfoWidget : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) - -public: - UserInfoWidget(QWidget *parent = nullptr); - - void setDisplayName(const QString &name); - void setUserId(const QString &userid); - void setAvatar(const QString &url); - - void reset(); - - QColor borderColor() const { return borderColor_; } - void setBorderColor(QColor &color) { borderColor_ = color; } - -protected: - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *) override; - -signals: - void openGlobalUserProfile(); - -private: - Avatar *userAvatar_; - - QHBoxLayout *topLayout_; - QHBoxLayout *avatarLayout_; - QVBoxLayout *textLayout_; - QHBoxLayout *buttonLayout_; - - FlatButton *logoutButton_; - - QLabel *displayNameLabel_; - QLabel *userIdLabel_; - - QString display_name_; - QString user_id_; - - QImage avatar_image_; - - int logoutButtonSize_; - - QColor borderColor_; - - QMenu *menu = nullptr; -}; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 0edc1288..9b906555 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -64,10 +64,14 @@ void UserSettings::load(std::optional<QString> profile) { QSettings settings; - tray_ = settings.value("user/window/tray", false).toBool(); + tray_ = settings.value("user/window/tray", false).toBool(); + startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); + + roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); + communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); + hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); - startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); groupView_ = settings.value("user/group_view", true).toBool(); hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); @@ -248,6 +252,24 @@ UserSettings::setTimelineMaxWidth(int state) emit timelineMaxWidthChanged(state); save(); } +void +UserSettings::setCommunityListWidth(int state) +{ + if (state == communityListWidth_) + return; + communityListWidth_ = state; + emit communityListWidthChanged(state); + save(); +} +void +UserSettings::setRoomListWidth(int state) +{ + if (state == roomListWidth_) + return; + roomListWidth_ = state; + emit roomListWidthChanged(state); + save(); +} void UserSettings::setDesktopNotifications(bool state) @@ -545,49 +567,14 @@ UserSettings::applyTheme() { QFile stylefile; - static QPalette original; if (this->theme() == "light") { stylefile.setFileName(":/styles/styles/nheko.qss"); - QPalette lightActive( - /*windowText*/ QColor("#333"), - /*button*/ QColor("white"), - /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(110, 110, 110), - /*mid*/ QColor(220, 220, 220), - /*text*/ QColor("#333"), - /*bright_text*/ QColor("#333"), - /*base*/ QColor("#fff"), - /*window*/ QColor("white")); - lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); - lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); - lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); - lightActive.setColor(QPalette::Link, QColor("#0077b5")); - lightActive.setColor(QPalette::ButtonText, QColor("#333")); - QApplication::setPalette(lightActive); } else if (this->theme() == "dark") { stylefile.setFileName(":/styles/styles/nheko-dark.qss"); - QPalette darkActive( - /*windowText*/ QColor("#caccd1"), - /*button*/ QColor(0xff, 0xff, 0xff), - /*light*/ QColor("#caccd1"), - /*dark*/ QColor(110, 110, 110), - /*mid*/ QColor("#202228"), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(0xff, 0xff, 0xff), - /*base*/ QColor("#202228"), - /*window*/ QColor("#2d3139")); - darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); - darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); - darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); - darkActive.setColor(QPalette::Link, QColor("#38a3d8")); - darkActive.setColor(QPalette::ButtonText, "#727274"); - QApplication::setPalette(darkActive); } else { stylefile.setFileName(":/styles/styles/system.qss"); - QApplication::setPalette(original); } + QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); stylefile.open(QFile::ReadOnly); QString stylesheet = QString(stylefile.readAll()); @@ -606,6 +593,11 @@ UserSettings::save() settings.setValue("start_in_tray", startInTray_); settings.endGroup(); // window + settings.beginGroup("sidebar"); + settings.setValue("community_list_width", communityListWidth_); + settings.setValue("room_list_width", roomListWidth_); + settings.endGroup(); // window + settings.beginGroup("timeline"); settings.setValue("buttons", buttonsInTimeline_); settings.setValue("message_hover_highlight", messageHoverHighlight_); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 3ad0293b..acb08569 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h
@@ -61,6 +61,10 @@ class UserSettings : public QObject NOTIFY privacyScreenTimeoutChanged) Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY timelineMaxWidthChanged) + Q_PROPERTY( + int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) + Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY + communityListWidthChanged) Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) @@ -129,6 +133,8 @@ public: void setSortByImportance(bool state); void setButtonsInTimeline(bool state); void setTimelineMaxWidth(int state); + void setCommunityListWidth(int state); + void setRoomListWidth(int state); void setDesktopNotifications(bool state); void setAlertOnNotification(bool state); void setAvatarCircles(bool state); @@ -178,6 +184,8 @@ public: return hasDesktopNotifications() || hasAlertOnNotification(); } int timelineMaxWidth() const { return timelineMaxWidth_; } + int communityListWidth() const { return communityListWidth_; } + int roomListWidth() const { return roomListWidth_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } QString emojiFont() const @@ -227,6 +235,8 @@ signals: void privacyScreenChanged(bool state); void privacyScreenTimeoutChanged(int state); void timelineMaxWidthChanged(int state); + void roomListWidthChanged(int state); + void communityListWidthChanged(int state); void mobileModeChanged(bool mode); void fontSizeChanged(double state); void fontChanged(QString state); @@ -276,6 +286,8 @@ private: bool shareKeysWithTrustedUsers_; bool mobileMode_; int timelineMaxWidth_; + int roomListWidth_; + int communityListWidth_; double baseFontSize_; QString font_; QString emojiFont_; diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp deleted file mode 100644
index 2daa6143..00000000 --- a/src/popups/PopupItem.cpp +++ /dev/null
@@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QLabel> -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "../Utils.h" -#include "../ui/Avatar.h" -#include "PopupItem.h" - -constexpr int PopupHMargin = 4; -constexpr int PopupItemMargin = 3; - -PopupItem::PopupItem(QWidget *parent) - : QWidget(parent) - , avatar_{new Avatar(this, conf::popup::avatar)} - , hovering_{false} -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setContentsMargins( - PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); -} - -void -PopupItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - if (underMouse() || hovering_) - p.fillRect(rect(), hoverColor_); -} - -RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) - : PopupItem(parent) - , roomId_{QString::fromStdString(res.room_id)} -{ - auto name = QFontMetrics(QFont()).elidedText( - QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - - avatar_->setLetter(utils::firstChar(name)); - - roomName_ = new QLabel(name, this); - roomName_->setMargin(0); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(roomName_, 1); - - if (!res.info.avatar_url.empty()) - avatar_->setImage(QString::fromStdString(res.info.avatar_url)); -} - -void -RoomItem::updateItem(const RoomSearchResult &result) -{ - roomId_ = QString::fromStdString(std::move(result.room_id)); - - auto name = - QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), - Qt::ElideRight, - parentWidget()->width() - 10); - - roomName_->setText(name); - - // if there is not an avatar set for the room, we want to at least show the letter - // correctly! - avatar_->setLetter(utils::firstChar(name)); - if (!result.info.avatar_url.empty()) - avatar_->setImage(QString::fromStdString(result.info.avatar_url)); -} - -void -RoomItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked(selectedText()); - - QWidget::mousePressEvent(event); -} diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h deleted file mode 100644
index fc24915e..00000000 --- a/src/popups/PopupItem.h +++ /dev/null
@@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -#include "../AvatarProvider.h" -#include "../ChatPage.h" - -class Avatar; -struct SearchResult; -class QLabel; -class QHBoxLayout; - -class PopupItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) - Q_PROPERTY(bool hovering READ hovering WRITE setHovering) - -public: - PopupItem(QWidget *parent); - - QString selectedText() const { return QString(); } - QColor hoverColor() const { return hoverColor_; } - void setHoverColor(QColor &color) { hoverColor_ = color; } - - bool hovering() const { return hovering_; } - void setHovering(const bool hover) { hovering_ = hover; }; - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void clicked(const QString &text); - -protected: - QHBoxLayout *topLayout_; - Avatar *avatar_; - QColor hoverColor_; - - //! Set if the item is currently being - //! hovered during tab completion (cycling). - bool hovering_; -}; - -class RoomItem : public PopupItem -{ - Q_OBJECT - -public: - RoomItem(QWidget *parent, const RoomSearchResult &res); - QString selectedText() const { return roomId_; } - void updateItem(const RoomSearchResult &res); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - QLabel *roomName_; - QString roomId_; - RoomSearchResult info_; -}; diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp deleted file mode 100644
index 7b545d61..00000000 --- a/src/popups/SuggestionsPopup.cpp +++ /dev/null
@@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "../Config.h" -#include "../Utils.h" -#include "../ui/Avatar.h" -#include "../ui/DropShadow.h" -#include "ChatPage.h" -#include "PopupItem.h" -#include "SuggestionsPopup.h" - -SuggestionsPopup::SuggestionsPopup(QWidget *parent) - : QWidget(parent) -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); - - layout_ = new QVBoxLayout(this); - layout_->setMargin(0); - layout_->setSpacing(0); -} - -QString -SuggestionsPopup::displayName(QString room, QString user) -{ - return cache::displayName(room, user); -} - -void -SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) -{ - if (rooms.empty()) { - hide(); - return; - } - - const int layoutCount = (int)layout_->count(); - const int roomCount = (int)rooms.size(); - - // Remove the extra widgets from the layout. - if (roomCount < layoutCount) - removeLayoutItemsAfter(roomCount - 1); - - for (int i = 0; i < roomCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto room = new RoomItem(this, rooms.at(i)); - connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(room); - } else { - // Update the current widget with the new data. - auto room = qobject_cast<RoomItem *>(item->widget()); - if (room) - room->updateItem(rooms.at(i)); - } - } - - resetSelection(); - adjustSize(); - - resize(geometry().width(), 40 * (int)rooms.size()); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::hoverSelection() -{ - resetHovering(); - setHovering(selectedItem_); - update(); -} - -void -SuggestionsPopup::selectHoveredSuggestion() -{ - const auto item = layout_->itemAt(selectedItem_); - if (!item) - return; - - const auto &widget = qobject_cast<RoomItem *>(item->widget()); - emit itemSelected(displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); - - resetSelection(); -} - -void -SuggestionsPopup::selectNextSuggestion() -{ - selectedItem_++; - if (selectedItem_ >= layout_->count()) - selectFirstItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::selectPreviousSuggestion() -{ - selectedItem_--; - if (selectedItem_ < 0) - selectLastItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::resetHovering() -{ - for (int i = 0; i < layout_->count(); ++i) { - const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); - - if (item) - item->setHovering(false); - } -} - -void -SuggestionsPopup::setHovering(int pos) -{ - const auto &item = layout_->itemAt(pos); - const auto &widget = qobject_cast<PopupItem *>(item->widget()); - - if (widget) - widget->setHovering(true); -} - -void -SuggestionsPopup::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -SuggestionsPopup::selectLastItem() -{ - selectedItem_ = layout_->count() - 1; -} - -void -SuggestionsPopup::removeLayoutItemsAfter(size_t startingPos) -{ - size_t posToRemove = layout_->count() - 1; - - QLayoutItem *item; - while (startingPos <= posToRemove && - (item = layout_->takeAt((int)posToRemove)) != nullptr) { - delete item->widget(); - delete item; - - posToRemove = layout_->count() - 1; - } -} diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h deleted file mode 100644
index 281edddb..00000000 --- a/src/popups/SuggestionsPopup.h +++ /dev/null
@@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -#include "CacheStructs.h" - -class QVBoxLayout; -class QLayoutItem; - -class SuggestionsPopup : public QWidget -{ - Q_OBJECT - -public: - explicit SuggestionsPopup(QWidget *parent = nullptr); - - void selectHoveredSuggestion(); - -public slots: - void addRooms(const std::vector<RoomSearchResult> &rooms); - - //! Move to the next available suggestion item. - void selectNextSuggestion(); - //! Move to the previous available suggestion item. - void selectPreviousSuggestion(); - //! Remove hovering from all items. - void resetHovering(); - //! Set hovering to the item in the given layout position. - void setHovering(int pos); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void itemSelected(const QString &user); - -private: - QString displayName(QString roomid, QString userid); - void hoverSelection(); - void resetSelection() { selectedItem_ = -1; } - void selectFirstItem() { selectedItem_ = 0; } - void selectLastItem(); - void removeLayoutItemsAfter(size_t startingPos); - - QVBoxLayout *layout_; - - //! Counter for tab completion (cycling). - int selectedItem_ = -1; -}; diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp deleted file mode 100644
index 56b57503..00000000 --- a/src/popups/UserMentions.cpp +++ /dev/null
@@ -1,178 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QPaintEvent> -#include <QPainter> -#include <QScrollArea> -#include <QStyleOption> -#include <QTabWidget> -#include <QTimer> -#include <QVBoxLayout> - -#include "Cache.h" -#include "ChatPage.h" -#include "EventAccessors.h" -#include "Logging.h" -#include "UserMentions.h" - -using namespace popups; - -UserMentions::UserMentions(QWidget *parent) - : QWidget{parent} -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::FramelessWindowHint | Qt::Popup); - - tab_layout_ = new QTabWidget(this); - - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - local_scroll_area_ = new QScrollArea(this); - local_scroll_area_->setWidgetResizable(true); - local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - local_scroll_widget_ = new QWidget(this); - local_scroll_widget_->setObjectName("local_scroll_widget"); - - all_scroll_area_ = new QScrollArea(this); - all_scroll_area_->setWidgetResizable(true); - all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - all_scroll_widget_ = new QWidget(this); - all_scroll_widget_->setObjectName("all_scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_); - local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - local_scroll_layout_->setSpacing(0); - local_scroll_layout_->setObjectName("localscrollarea"); - - all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_); - all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - all_scroll_layout_->setSpacing(0); - all_scroll_layout_->setObjectName("allscrollarea"); - - local_scroll_area_->setWidget(local_scroll_widget_); - local_scroll_area_->setAlignment(Qt::AlignBottom); - - all_scroll_area_->setWidget(all_scroll_widget_); - all_scroll_area_->setAlignment(Qt::AlignBottom); - - tab_layout_->addTab(local_scroll_area_, tr("This Room")); - tab_layout_->addTab(all_scroll_area_, tr("All Rooms")); - top_layout_->addWidget(tab_layout_); - - setLayout(top_layout_); -} - -void -UserMentions::initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs) -{ - nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications."); - - for (const auto &item : notifs) { - for (const auto &notif : item.notifications) { - const auto event_id = - QString::fromStdString(mtx::accessors::event_id(notif.event)); - - try { - const auto room_id = QString::fromStdString(notif.room_id); - const auto user_id = - QString::fromStdString(mtx::accessors::sender(notif.event)); - const auto body = - QString::fromStdString(mtx::accessors::body(notif.event)); - - pushItem(event_id, - user_id, - body, - room_id, - ChatPage::instance()->currentRoom()); - - } catch (const lmdb::error &e) { - nhlog::db()->warn("error while sending desktop notification: {}", - e.what()); - } - } - } -} - -void -UserMentions::showPopup() -{ - for (auto widget : all_scroll_layout_->findChildren<QWidget *>()) { - delete widget; - } - for (auto widget : local_scroll_layout_->findChildren<QWidget *>()) { - delete widget; - } - - auto notifs = cache::getTimelineMentions(); - - initializeMentions(notifs); - show(); -} - -void -UserMentions::pushItem(const QString &event_id, - const QString &user_id, - const QString &body, - const QString &room_id, - const QString &current_room_id) -{ - (void)event_id; - (void)user_id; - (void)body; - (void)room_id; - (void)current_room_id; - // setUpdatesEnabled(false); - // - // // Add to the 'all' section - // TimelineItem *view_item = new TimelineItem( - // mtx::events::MessageType::Text, user_id, body, true, room_id, - // all_scroll_widget_); - // view_item->setEventId(event_id); - // view_item->hide(); - // - // all_scroll_layout_->addWidget(view_item); - // QTimer::singleShot(0, this, [view_item, this]() { - // view_item->show(); - // view_item->adjustSize(); - // setUpdatesEnabled(true); - // }); - // - // // if it matches the current room... add it to the current room as well. - // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { - // // Add to the 'local' section - // TimelineItem *local_view_item = new - // TimelineItem(mtx::events::MessageType::Text, - // user_id, - // body, - // true, - // room_id, - // local_scroll_widget_); - // local_view_item->setEventId(event_id); - // local_view_item->hide(); - // local_scroll_layout_->addWidget(local_view_item); - // - // QTimer::singleShot(0, this, [local_view_item]() { - // local_view_item->show(); - // local_view_item->adjustSize(); - // }); - // } -} - -void -UserMentions::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h deleted file mode 100644
index f0b662d8..00000000 --- a/src/popups/UserMentions.h +++ /dev/null
@@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <mtx/responses/notifications.hpp> - -#include <QMap> -#include <QString> -#include <QWidget> - -class QPaintEvent; -class QTabWidget; -class QScrollArea; -class QVBoxLayout; - -namespace popups { - -class UserMentions : public QWidget -{ - Q_OBJECT -public: - UserMentions(QWidget *parent = nullptr); - - void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); - void showPopup(); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - void pushItem(const QString &event_id, - const QString &user_id, - const QString &body, - const QString &room_id, - const QString &current_room_id); - QTabWidget *tab_layout_; - QVBoxLayout *top_layout_; - QVBoxLayout *local_scroll_layout_; - QVBoxLayout *all_scroll_layout_; - - QScrollArea *local_scroll_area_; - QWidget *local_scroll_widget_; - - QScrollArea *all_scroll_area_; - QWidget *all_scroll_widget_; -}; -} diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp new file mode 100644
index 00000000..96a450ea --- /dev/null +++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CommunitiesModel.h" + +#include <set> + +#include "Cache.h" +#include "UserSettingsPage.h" + +CommunitiesModel::CommunitiesModel(QObject *parent) + : QAbstractListModel(parent) +{} + +QHash<int, QByteArray> +CommunitiesModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {DisplayName, "displayName"}, + {Tooltip, "tooltip"}, + {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, + }; +} + +QVariant +CommunitiesModel::data(const QModelIndex &index, int role) const +{ + if (index.row() == 0) { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/world.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("All rooms"); + case CommunitiesModel::Roles::Tooltip: + return tr("Shows all rooms without filtering."); + case CommunitiesModel::Roles::ChildrenHidden: + return false; + case CommunitiesModel::Roles::Hidden: + return false; + case CommunitiesModel::Roles::Id: + return ""; + } + } else if (index.row() - 1 < tags_.size()) { + auto tag = tags_.at(index.row() - 1); + if (tag == "m.favourite") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Favourites"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms you have favourited."); + } + } else if (tag == "m.lowpriority") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Low Priority"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms with low priority."); + } + } else if (tag == "m.server_notice") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Server Notices"); + case CommunitiesModel::Roles::Tooltip: + return tr("Messages from your server or administrator."); + } + } else { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tag.mid(2); + case CommunitiesModel::Roles::Tooltip: + return tag.mid(2); + } + } + + switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Id: + return "tag:" + tag; + } + } + return QVariant(); +} + +void +CommunitiesModel::initializeSidebar() +{ + std::set<std::string> ts; + for (const auto &e : cache::roomInfo()) { + for (const auto &t : e.tags) { + if (t.find("u.") == 0 || t.find("m." == 0)) { + ts.insert(t); + } + } + } + + beginResetModel(); + tags_.clear(); + for (const auto &t : ts) + tags_.push_back(QString::fromStdString(t)); + + hiddentTagIds_ = UserSettings::instance()->hiddenTags(); + endResetModel(); + + emit tagsChanged(); + emit hiddenTagsChanged(); +} + +void +CommunitiesModel::clear() +{ + beginResetModel(); + tags_.clear(); + endResetModel(); + resetCurrentTagId(); + + emit tagsChanged(); +} + +void +CommunitiesModel::sync(const mtx::responses::Rooms &rooms) +{ + bool tagsUpdated = false; + + for (const auto &[roomid, room] : rooms.join) { + (void)roomid; + for (const auto &e : room.account_data.events) + if (std::holds_alternative< + mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { + tagsUpdated = true; + } + } + + if (tagsUpdated) + initializeSidebar(); +} + +void +CommunitiesModel::setCurrentTagId(QString tagId) +{ + if (tagId.startsWith("tag:")) { + auto tag = tagId.mid(4); + for (const auto &t : tags_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } + } + } + + this->currentTagId_ = ""; + emit currentTagIdChanged(currentTagId_); +} + +void +CommunitiesModel::toggleTagId(QString tagId) +{ + if (hiddentTagIds_.contains(tagId)) { + hiddentTagIds_.removeOne(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } else { + hiddentTagIds_.push_back(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } + + if (tagId.startsWith("tag:")) { + auto idx = tags_.indexOf(tagId.mid(4)); + if (idx != -1) + emit dataChanged(index(idx), index(idx), {Hidden}); + } + + emit hiddenTagsChanged(); +} diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h new file mode 100644
index 00000000..c98b5955 --- /dev/null +++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractListModel> +#include <QHash> +#include <QString> +#include <QStringList> + +#include <mtx/responses/sync.hpp> + +class CommunitiesModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY + currentTagIdChanged RESET resetCurrentTagId) + Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) + +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + Tooltip, + ChildrenHidden, + Hidden, + Id, + }; + + CommunitiesModel(QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return 1 + tags_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void initializeSidebar(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + QString currentTagId() const { return currentTagId_; } + void setCurrentTagId(QString tagId); + void resetCurrentTagId() + { + currentTagId_.clear(); + emit currentTagIdChanged(currentTagId_); + } + QStringList tags() const { return tags_; } + void toggleTagId(QString tagId); + +signals: + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); + void tagsChanged(); + +private: + QStringList tags_; + QString currentTagId_; + QStringList hiddentTagIds_; +}; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 883d384c..4a9f0fff 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -770,7 +770,7 @@ EventStore::decryptEvent(const IdIndex &idx, } mtx::events::collections::TimelineEvents * -EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits) +EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); @@ -778,7 +778,7 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, if (id.empty()) return nullptr; - IdIndex index{room_id_, std::string(id)}; + IdIndex index{room_id_, std::move(id)}; if (resolve_edits) { auto edits_ = edits(index.id); if (!edits_.empty()) { @@ -796,14 +796,12 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, http::client()->get_event( room_id_, index.id, - [this, - relatedTo = std::string(related_to.data(), related_to.size()), - id = index.id](const mtx::events::collections::TimelineEvents &timeline, - mtx::http::RequestErr err) { + [this, relatedTo = std::string(related_to), id = index.id]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { if (err) { nhlog::net()->error( - "Failed to retrieve event with id {}, which " - "was " + "Failed to retrieve event with id {}, which was " "requested to show the replyTo for event {}", relatedTo, id); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index c7a7588b..d9bb86cb 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -70,7 +70,7 @@ public: // optionally returns the event or nullptr and fetches it, after which it emits a // relatedFetched event - mtx::events::collections::TimelineEvents *get(std::string_view id, + mtx::events::collections::TimelineEvents *get(std::string id, std::string_view related_to, bool decrypt = true, bool resolve_edits = true); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..c309daab 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -20,6 +20,7 @@ #include "Cache.h" #include "ChatPage.h" #include "CompletionProxyModel.h" +#include "Config.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -508,8 +509,7 @@ InputBar::command(QString command, QString args) } else if (command == "react") { auto eventId = room->reply(); if (!eventId.isEmpty()) - ChatPage::instance()->timelineManager()->queueReactionMessage( - eventId, args.trimmed()); + reaction(eventId, args.trimmed()); } else if (command == "join") { ChatPage::instance()->joinRoom(args); } else if (command == "part" || command == "leave") { @@ -715,3 +715,35 @@ InputBar::stopTyping() } }); } + +void +InputBar::reaction(const QString &reactedEvent, const QString &reactionKey) +{ + auto reactions = room->reactions(reactedEvent.toStdString()); + + QString selfReactedEvent; + for (const auto &reaction : reactions) { + if (reactionKey == reaction.key_) { + selfReactedEvent = reaction.selfReactedEvent_; + break; + } + } + + if (selfReactedEvent.startsWith("m")) + return; + + // If selfReactedEvent is empty, that means we haven't previously reacted + if (selfReactedEvent.isEmpty()) { + mtx::events::msg::Reaction reaction; + mtx::common::Relation rel; + rel.rel_type = mtx::common::RelationType::Annotation; + rel.event_id = reactedEvent.toStdString(); + rel.key = reactionKey.toStdString(); + reaction.relations.relations.push_back(rel); + + room->sendMessageEvent(reaction, mtx::events::EventType::Reaction); + // Otherwise, we have previously reacted and the reaction should be redacted + } else { + room->redactEvent(selfReactedEvent); + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots: void message(QString body, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); + void reaction(const QString &reactedEvent, const QString &reactionKey); private slots: void startTyping(); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp new file mode 100644
index 00000000..0f980c6c --- /dev/null +++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,590 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomlistModel.h" + +#include "Cache_p.h" +#include "ChatPage.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "TimelineModel.h" +#include "TimelineViewManager.h" +#include "UserSettingsPage.h" + +RoomlistModel::RoomlistModel(TimelineViewManager *parent) + : QAbstractListModel(parent) + , manager(parent) +{ + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); + QHash<QString, QSharedPointer<TimelineModel>>::iterator i; + for (i = models.begin(); i != models.end(); ++i) { + auto ptr = i.value(); + + if (!ptr.isNull()) { + ptr->setDecryptDescription(decrypt); + ptr->updateLastMessage(); + } + } + }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); +} + +QHash<int, QByteArray> +RoomlistModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {RoomName, "roomName"}, + {RoomId, "roomId"}, + {LastMessage, "lastMessage"}, + {Time, "time"}, + {Timestamp, "timestamp"}, + {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, + {NotificationCount, "notificationCount"}, + {IsInvite, "isInvite"}, + {IsSpace, "isSpace"}, + {Tags, "tags"}, + }; +} + +QVariant +RoomlistModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { + auto roomid = roomids.at(index.row()); + + if (models.contains(roomid)) { + auto room = models.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->plainRoomName(); + case Roles::RoomId: + return room->roomId(); + case Roles::LastMessage: + return room->lastMessage().body; + case Roles::Time: + return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant( + static_cast<quint64>(room->lastMessage().timestamp)); + case Roles::HasUnreadMessages: + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); + case Roles::NotificationCount: + return room->notificationCount(); + case Roles::IsInvite: + case Roles::IsSpace: + return false; + case Roles::Tags: { + auto info = cache::singleRoomInfo(roomid.toStdString()); + QStringList list; + for (const auto &t : info.tags) + list.push_back(QString::fromStdString(t)); + return list; + } + default: + return {}; + } + } else if (invites.contains(roomid)) { + auto room = invites.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::RoomId: + return roomid; + case Roles::LastMessage: + return room.msgInfo.body; + case Roles::Time: + return room.msgInfo.descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast<quint64>(room.msgInfo.timestamp)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return true; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } + } else { + return {}; + } + } else { + return {}; + } +} + +void +RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_) +{ + std::vector<int> roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); + } + + this->roomReadStatus[roomid] = roomUnread; + } + + for (auto idx : roomsToUpdate) { + emit dataChanged(index(idx), + index(idx), + { + Roles::HasUnreadMessages, + }); + } +} +void +RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) +{ + if (!models.contains(room_id)) { + // ensure we get read status updates and are only connected once + connect(cache::client(), + &Cache::roomReadStatus, + this, + &RoomlistModel::updateReadStatus, + Qt::UniqueConnection); + + QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); + newRoom->setDecryptDescription( + ChatPage::instance()->userSettings()->decryptSidebar()); + + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + manager->imageProvider(), + &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + manager, + &TimelineViewManager::forwardMessageToRoom); + connect( + newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::LastMessage, + Roles::Timestamp, + Roles::NotificationCount, + Qt::DisplayRole, + }); + }); + connect( + newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::AvatarUrl, + }); + }); + connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + }); + }); + connect( + newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::NotificationCount, + Qt::DisplayRole, + }); + + int total_unread_msgs = 0; + + for (const auto &room : models) { + if (!room.isNull()) + total_unread_msgs += room->notificationCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); + }); + + newRoom->updateLastMessage(); + + bool wasInvite = invites.contains(room_id); + if (!suppressInsertNotification && !wasInvite) + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + + models.insert(room_id, std::move(newRoom)); + + if (wasInvite) { + auto idx = roomidToIndex(room_id); + invites.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else { + roomids.push_back(room_id); + } + + if (!suppressInsertNotification && !wasInvite) + endInsertRows(); + } +} + +void +RoomlistModel::sync(const mtx::responses::Rooms &rooms) +{ + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->sync(room); + // room_model->addEvents(room.timeline); + connect(room_model.data(), + &TimelineModel::newCallEvent, + manager->callManager(), + &CallManager::syncEvent, + Qt::UniqueConnection); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + for (const auto &ev : room.ephemeral.events) { + if (auto t = std::get_if< + mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( + &ev)) { + std::vector<QString> typing; + typing.reserve(t->content.user_ids.size()); + for (const auto &user : t->content.user_ids) { + if (user != http::client()->user_id().to_string()) + typing.push_back( + QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } + } + } + + for (const auto &[room_id, room] : rooms.leave) { + (void)room; + auto idx = this->roomidToIndex(QString::fromStdString(room_id)); + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + if (models.contains(QString::fromStdString(room_id))) + models.remove(QString::fromStdString(room_id)); + else if (invites.contains(QString::fromStdString(room_id))) + invites.remove(QString::fromStdString(room_id)); + endRemoveRows(); + } + } + + for (const auto &[room_id, room] : rooms.invite) { + (void)room; + auto qroomid = QString::fromStdString(room_id); + + auto invite = cache::client()->invite(room_id); + if (!invite) + continue; + + if (invites.contains(qroomid)) { + invites[qroomid] = *invite; + auto idx = roomidToIndex(qroomid); + emit dataChanged(index(idx), index(idx)); + } else { + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + invites.insert(qroomid, *invite); + roomids.push_back(std::move(qroomid)); + endInsertRows(); + } + } +} + +void +RoomlistModel::initializeRooms() +{ + beginResetModel(); + models.clear(); + roomids.clear(); + invites.clear(); + currentRoom_ = nullptr; + + invites = cache::client()->invites(); + for (const auto &id : invites.keys()) + roomids.push_back(id); + + for (const auto &id : cache::client()->roomIds()) + addRoom(id, true); + + endResetModel(); +} + +void +RoomlistModel::clear() +{ + beginResetModel(); + models.clear(); + invites.clear(); + roomids.clear(); + currentRoom_ = nullptr; + emit currentRoomChanged(); + endResetModel(); +} + +void +RoomlistModel::acceptInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->joinRoom(roomid); + } + } +} +void +RoomlistModel::declineInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} +void +RoomlistModel::leave(QString roomid) +{ + if (models.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + models.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} + +void +RoomlistModel::setCurrentRoom(QString roomid) +{ + nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); + if (models.contains(roomid)) { + currentRoom_ = models.value(roomid); + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } +} + +namespace { +enum NotificationImportance : short +{ + ImportanceDisabled = -1, + AllEventsRead = 0, + NewMessage = 1, + NewMentions = 2, + Invite = 3 +}; +} + +short int +FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const +{ + // Returns the degree of importance of the unread messages in the room. + // If sorting by importance is disabled in settings, this only ever + // returns ImportanceDisabled or Invite + if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + return Invite; + } else if (!this->sortByImportance) { + return ImportanceDisabled; + } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { + return NewMentions; + } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { + return NewMessage; + } else { + return AllEventsRead; + } +} +bool +FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); + QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); + + // Sort by "importance" (i.e. invites before mentions before + // notifs before new events before old events), then secondly + // by recency. + + // Checking importance first + const auto a_importance = calculateImportance(left_idx); + const auto b_importance = calculateImportance(right_idx); + if (a_importance != b_importance) { + return a_importance > b_importance; + } + + // Now sort by recency + // Zero if empty, otherwise the time that the event occured + uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); + uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); + + if (a_recency != b_recency) + return a_recency > b_recency; + else + return left.row() < right.row(); +} + +FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent) + : QSortFilterProxyModel(parent) + , roomlistmodel(model) +{ + this->sortByImportance = UserSettings::instance()->sortByImportance(); + setSourceModel(model); + setDynamicSortFilter(true); + + QObject::connect(UserSettings::instance().get(), + &UserSettings::roomSortingChanged, + this, + [this](bool sortByImportance_) { + this->sortByImportance = sortByImportance_; + invalidate(); + }); + + connect(roomlistmodel, + &RoomlistModel::currentRoomChanged, + this, + &FilteredRoomlistModel::currentRoomChanged); + + sort(0); +} + +void +FilteredRoomlistModel::updateHiddenTagsAndSpaces() +{ + hiddenTags.clear(); + hiddenSpaces.clear(); + for (const auto &t : UserSettings::instance()->hiddenTags()) { + if (t.startsWith("tag:")) + hiddenTags.push_back(t.mid(4)); + else if (t.startsWith("space:")) + hiddenSpaces.push_back(t.mid(6)); + } + + invalidateFilter(); +} + +bool +FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const +{ + if (filterType == FilterBy::Nothing) { + if (!hiddenTags.empty()) { + auto tags = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Tag) { + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + if (!tags.contains(filterStr)) + return false; + else if (!hiddenTags.empty()) { + for (const auto &t : tags) + if (t != filterStr && hiddenTags.contains(t)) + return false; + } + return true; + } else { + return true; + } +} + +void +FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) +{ + if (on) { + http::client()->put_tag( + roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to add tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } else { + http::client()->delete_tag( + roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to delete tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } +} + +void +FilteredRoomlistModel::nextRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx++; + if (idx < rowCount()) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} + +void +FilteredRoomlistModel::previousRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx--; + if (idx > 0) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h new file mode 100644
index 00000000..b0244886 --- /dev/null +++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <CacheStructs.h> +#include <QAbstractListModel> +#include <QHash> +#include <QSharedPointer> +#include <QSortFilterProxyModel> +#include <QString> +#include <set> + +#include <mtx/responses/sync.hpp> + +#include "TimelineModel.h" + +class TimelineViewManager; + +class RoomlistModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomName, + RoomId, + LastMessage, + Time, + Timestamp, + HasUnreadMessages, + HasLoudNotification, + NotificationCount, + IsInvite, + IsSpace, + Tags, + }; + + RoomlistModel(TimelineViewManager *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + QSharedPointer<TimelineModel> getRoomById(QString id) const + { + if (models.contains(id)) + return models.value(id); + else + return {}; + } + +public slots: + void initializeRooms(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + int roomidToIndex(QString roomid) + { + for (int i = 0; i < (int)roomids.size(); i++) { + if (roomids[i] == roomid) + return i; + } + + return -1; + } + void acceptInvite(QString roomid); + void declineInvite(QString roomid); + void leave(QString roomid); + TimelineModel *currentRoom() const { return currentRoom_.get(); } + void setCurrentRoom(QString roomid); + void resetCurrentRoom() + { + currentRoom_ = nullptr; + emit currentRoomChanged(); + } + +private slots: + void updateReadStatus(const std::map<QString, bool> roomReadStatus_); + +signals: + void totalUnreadMessageCountUpdated(int unreadMessages); + void currentRoomChanged(); + +private: + void addRoom(const QString &room_id, bool suppressInsertNotification = false); + + TimelineViewManager *manager = nullptr; + std::vector<QString> roomids; + QHash<QString, RoomInfo> invites; + QHash<QString, QSharedPointer<TimelineModel>> models; + std::map<QString, bool> roomReadStatus; + + QSharedPointer<TimelineModel> currentRoom_; + + friend class FilteredRoomlistModel; +}; + +class FilteredRoomlistModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; + +public slots: + int roomidToIndex(QString roomid) + { + return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) + .row(); + } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } + void leave(QString roomid) { roomlistmodel->leave(roomid); } + void toggleTag(QString roomid, QString tag, bool on); + + TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } + void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } + void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); } + + void nextRoom(); + void previousRoom(); + + void updateFilterTag(QString tagId) + { + if (tagId.startsWith("tag:")) { + filterType = FilterBy::Tag; + filterStr = tagId.mid(4); + } else { + filterType = FilterBy::Nothing; + filterStr.clear(); + } + + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); + +signals: + void currentRoomChanged(); + +private: + short int calculateImportance(const QModelIndex &idx) const; + RoomlistModel *roomlistmodel; + bool sortByImportance = true; + + enum class FilterBy + { + Tag, + Space, + Nothing, + }; + QString filterStr = ""; + FilterBy filterType = FilterBy::Nothing; + QStringList hiddenTags, hiddenSpaces; +}; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8df17457..f29f929e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , room_id_(room_id) , manager_(manager) { + lastMessage_.timestamp = 0; + connect( this, &TimelineModel::redactionFailed, @@ -572,7 +574,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r !event_id(event).empty() && event_id(event).front() == '$'); case IsEncrypted: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); return encrypted_event && std::holds_alternative< mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -581,7 +583,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case Trustlevel: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); if (encrypted_event) { if (auto encrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -724,6 +726,20 @@ TimelineModel::fetchMore(const QModelIndex &) } void +TimelineModel::sync(const mtx::responses::JoinedRoom &room) +{ + this->syncState(room.state); + this->addEvents(room.timeline); + + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } +} + +void TimelineModel::syncState(const mtx::responses::State &s) { using namespace mtx::events; @@ -866,14 +882,17 @@ TimelineModel::updateLastMessage() if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { auto time = mtx::accessors::origin_server_ts(*event); uint64_t ts = time.toMSecsSinceEpoch(); - emit manager_->updateRoomsLastMessage( - room_id_, + auto description = DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), QString::fromStdString(http::client()->user_id().to_string()), tr("You joined this room."), utils::descriptiveTime(time), ts, - time}); + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) @@ -884,7 +903,10 @@ TimelineModel::updateLastMessage() QString::fromStdString(http::client()->user_id().to_string()), cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } } @@ -1867,6 +1889,17 @@ TimelineModel::roomName() const } QString +TimelineModel::plainRoomName() const +{ + auto info = cache::getRoomInfo({room_id_.toStdString()}); + + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); +} + +QString TimelineModel::roomAvatarUrl() const { auto info = cache::getRoomInfo({room_id_.toStdString()}); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 92fccd2d..3ebbe120 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -14,6 +14,7 @@ #include <mtxclient/http/errors.hpp> #include "CacheCryptoStructs.h" +#include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" #include "Permissions.h" @@ -253,12 +254,15 @@ public: } void updateLastMessage(); + void sync(const mtx::responses::JoinedRoom &room); void addEvents(const mtx::responses::Timeline &events); void syncState(const mtx::responses::State &state); template<class T> void sendMessageEvent(const T &content, mtx::events::EventType eventType); RelatedInfo relatedInfo(QString id); + DescInfo lastMessage() const { return lastMessage_; } + public slots: void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } @@ -303,12 +307,16 @@ public slots: } QString roomName() const; + QString plainRoomName() const; QString roomTopic() const; InputBar *input() { return &input_; } Permissions *permissions() { return &permissions_; } QString roomAvatarUrl() const; QString roomId() const { return room_id_; } + bool hasMentions() { return highlight_count > 0; } + int notificationCount() { return notification_count; } + QString scrollTarget() const; private slots: @@ -328,6 +336,9 @@ signals: void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); + void lastMessageChanged(); + void notificationsChanged(); + void openRoomSettingsDialog(RoomSettings *settings); void newMessageToSend(mtx::events::collections::TimelineEvents event); @@ -372,7 +383,11 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; + DescInfo lastMessage_{}; + friend struct SendMessageVisitor; + + int notification_count = 0, highlight_count = 0; }; template<class T> diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 0785e3e1..a45294d1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -4,7 +4,6 @@ #include "TimelineViewManager.h" -#include <QDesktopServices> #include <QDropEvent> #include <QMetaType> #include <QPalette> @@ -32,6 +31,7 @@ #include "emoji/Provider.h" #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" +#include "ui/NhekoGlobalObject.h" Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector<DeviceInfo>) @@ -87,21 +87,6 @@ removeReplyFallback(mtx::events::Event<T> &e) } void -TimelineViewManager::updateEncryptedDescriptions() -{ - auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); - QHash<QString, QSharedPointer<TimelineModel>>::iterator i; - for (i = models.begin(); i != models.end(); ++i) { - auto ptr = i.value(); - - if (!ptr.isNull()) { - ptr->setDecryptDescription(decrypt); - ptr->updateLastMessage(); - } - } -} - -void TimelineViewManager::updateColorPalette() { userColors.clear(); @@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const } TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) - : imgProvider(new MxcImageProvider()) + : QObject(parent) + , imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) , blurhashProvider(new BlurhashProvider()) , callManager_(callManager) + , rooms_(new RoomlistModel(this)) + , communities_(new CommunitiesModel(this)) { qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); @@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); return ptr; }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = new FilteredRoomlistModel(self->rooms_); + + connect(self->communities_, + &CommunitiesModel::currentTagIdChanged, + ptr, + &FilteredRoomlistModel::updateFilterTag); + connect(self->communities_, + &CommunitiesModel::hiddenTagsChanged, + ptr, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); + return ptr; + }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = self->communities_; + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); qmlRegisterSingletonType<UserSettings>( "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { auto ptr = ChatPage::instance()->userSettings().data(); @@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Clipboard(); }); + qmlRegisterSingletonType<Nheko>( + "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Nheko(); + }); qRegisterMetaType<mtx::events::collections::TimelineEvents>(); qRegisterMetaType<std::vector<DeviceInfo>>(); @@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Error: Only enums"); #ifdef USE_QUICK_VIEW - view = new QQuickView(); + view = new QQuickView(parent); container = QWidget::createWindowContainer(view, parent); #else view = new QQuickWidget(parent); @@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("blurhash", blurhashProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + view->setSource(QUrl("qrc:///qml/Root.qml")); connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); - connect(parent, - &ChatPage::decryptSidebarChanged, - this, - &TimelineViewManager::updateEncryptedDescriptions); connect( dynamic_cast<ChatPage *>(parent), &ChatPage::receivedRoomDeviceVerificationRequest, @@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem() } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res) { - for (const auto &[room_id, room] : rooms.join) { - // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(room_id)); - const auto &room_model = models.value(QString::fromStdString(room_id)); - if (!isInitialSync_) - connect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); - room_model->syncState(room.state); - room_model->addEvents(room.timeline); - if (!isInitialSync_) - disconnect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); + this->rooms_->sync(rooms_res); + this->communities_->sync(rooms_res); - if (ChatPage::instance()->userSettings()->typingNotifications()) { - for (const auto &ev : room.ephemeral.events) { - if (auto t = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( - &ev)) { - std::vector<QString> typing; - typing.reserve(t->content.user_ids.size()); - for (const auto &user : t->content.user_ids) { - if (user != http::client()->user_id().to_string()) - typing.push_back( - QString::fromStdString(user)); - } - room_model->updateTypingUsers(typing); - } - } - } - } - - this->isInitialSync_ = false; - emit initialSyncChanged(false); -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) { - QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); - newRoom->setDecryptDescription( - ChatPage::instance()->userSettings()->decryptSidebar()); - - connect(newRoom.data(), - &TimelineModel::newEncryptedImage, - imgProvider, - &MxcImageProvider::addEncryptionInfo); - connect(newRoom.data(), - &TimelineModel::forwardToRoom, - this, - &TimelineViewManager::forwardMessageToRoom); - models.insert(room_id, std::move(newRoom)); + if (isInitialSync_) { + this->isInitialSync_ = false; + emit initialSyncChanged(false); } } void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - container->setFocus(); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::highlightRoom(const QString &room_id) -{ - ChatPage::instance()->highlightRoom(room_id); -} - -void TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) { - auto room = models.find(room_id); - if (room != models.end()) { - if (timeline_ != room.value().data()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); + if (auto room = rooms_->getRoomById(room_id)) { + if (rooms_->currentRoom() != room) { + rooms_->setCurrentRoom(room_id); container->setFocus(); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } - timeline_->showEvent(event_id); + room->showEvent(event_id); } } @@ -457,12 +393,14 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) auto imgDialog = new dialogs::ImageOverlay(pixmap); imgDialog->showFullScreen(); - connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() { + + auto room = rooms_->currentRoom(); + connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { // hide the overlay while presenting the save dialog for better // cross platform support. imgDialog->hide(); - if (!timeline_->saveMedia(eventId)) { + if (!room->saveMedia(eventId)) { imgDialog->show(); } else { imgDialog->close(); @@ -471,70 +409,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) } void -TimelineViewManager::openLink(QString link) const -{ - QUrl url(link); - if (url.scheme() == "https" && url.host() == "matrix.to") { - // handle matrix.to links internally - QString p = url.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray uri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("#")) { - QByteArray uri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("!")) { - QByteArray uri = "matrix:roomid/" + - QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } - } - } else { - QDesktopServices::openUrl(url); - } -} - -void TimelineViewManager::openInviteUsersDialog() { MainWindow::instance()->openInviteUsersDialog( [this](const QStringList &invitees) { emit inviteUsers(invitees); }); } void -TimelineViewManager::openMemberListDialog() const +TimelineViewManager::openMemberListDialog(QString roomid) const { - MainWindow::instance()->openMemberListDialog(timeline_->roomId()); + MainWindow::instance()->openMemberListDialog(roomid); } void -TimelineViewManager::openLeaveRoomDialog() const +TimelineViewManager::openLeaveRoomDialog(QString roomid) const { - MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId()); + MainWindow::instance()->openLeaveRoomDialog(roomid); } void @@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid) if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != room_members.end()) { - auto model = models.value(QString::fromStdString(room_id)); - auto flow = DeviceVerificationFlow::InitiateUserVerification( - this, model.data(), userid); - connect(model.data(), - &TimelineModel::updateFlowEventId, - this, - [this, flow](std::string eventId) { - dvList[QString::fromStdString(eventId)] = flow; - }); - emit newDeviceVerificationRequest(flow.data()); - return; + if (auto model = + rooms_->getRoomById(QString::fromStdString(room_id))) { + auto flow = + DeviceVerificationFlow::InitiateUserVerification( + this, model.data(), userid); + connect(model.data(), + &TimelineModel::updateFlowEventId, + this, + [this, flow](std::string eventId) { + dvList[QString::fromStdString(eventId)] = + flow; + }); + emit newDeviceVerificationRequest(flow.data()); + return; + } } } } @@ -593,26 +485,24 @@ void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids) { - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); + if (auto room = rooms_->getRoomById(room_id)) { + room->markEventsAsRead(event_ids); } } void TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) { - auto room = models.find(QString::fromStdString(room_id)); - if (room != models.end()) { - room.value()->receivedSessionKey(session_id); + if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) { + room->receivedSessionKey(session_id); } } void -TimelineViewManager::initWithMessages(const std::vector<QString> &roomIds) +TimelineViewManager::initializeRoomlist() { - for (const auto &roomId : roomIds) - addRoom(roomId); + rooms_->initializeRooms(); + communities_->initializeSidebar(); } void @@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody) { - auto room = models.find(roomid); - if (room != models.end()) { - room.value()->setReply(repliedToEvent); - room.value()->input()->message(replyBody); + if (auto room = rooms_->getRoomById(roomid)) { + room->setReply(repliedToEvent); + room->input()->message(replyBody); } } void -TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey) -{ - if (!timeline_) - return; - - auto reactions = timeline_->reactions(reactedEvent.toStdString()); - - QString selfReactedEvent; - for (const auto &reaction : reactions) { - if (reactionKey == reaction.key_) { - selfReactedEvent = reaction.selfReactedEvent_; - break; - } - } - - if (selfReactedEvent.startsWith("m")) - return; - - // If selfReactedEvent is empty, that means we haven't previously reacted - if (selfReactedEvent.isEmpty()) { - mtx::events::msg::Reaction reaction; - mtx::common::Relation rel; - rel.rel_type = mtx::common::RelationType::Annotation; - rel.event_id = reactedEvent.toStdString(); - rel.key = reactionKey.toStdString(); - reaction.relations.relations.push_back(rel); - - timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); - // Otherwise, we have previously reacted and the reaction should be redacted - } else { - timeline_->redactEvent(selfReactedEvent); - } -} -void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) { - models.value(roomid)->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &callCandidates) { - models.value(roomid)->sendMessageEvent(callCandidates, - mtx::events::EventType::CallCandidates); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &callAnswer) { - models.value(roomid)->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &callHangUp) { - models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } void @@ -738,7 +596,7 @@ void TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId) { - auto room = models.find(roomId); + auto room = rooms_->getRoomById(roomId); auto content = mtx::accessors::url(*e); std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); @@ -781,12 +639,15 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven ev.content.url = url; } - auto room = models.find(roomId); - removeReplyFallback(ev); - ev.content.relations.relations.clear(); - room.value()->sendMessageEvent( - ev.content, - mtx::events::EventType::RoomMessage); + if (auto room = rooms_->getRoomById(roomId)) { + removeReplyFallback(ev); + ev.content.relations.relations + .clear(); + room->sendMessageEvent( + ev.content, + mtx::events::EventType:: + RoomMessage); + } } }, *e); @@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven mtx::events::EventType::RoomMessage) { e.content.relations.relations.clear(); removeReplyFallback(e); - room.value()->sendMessageEvent(e.content, - mtx::events::EventType::RoomMessage); + room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage); } }, *e); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b23a61db..556bcf4c 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,8 @@ #include "WebRTCSession.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "timeline/CommunitiesModel.h" +#include "timeline/RoomlistModel.h" class MxcImageProvider; class BlurhashProvider; @@ -35,12 +37,8 @@ class TimelineViewManager : public QObject Q_OBJECT Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - Q_PROPERTY( bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) Q_PROPERTY( - bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged) - Q_PROPERTY( bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged) public: @@ -48,48 +46,38 @@ public: QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - void clearAll() - { - timeline_ = nullptr; - emit activeTimelineChanged(nullptr); - models.clear(); - } + MxcImageProvider *imageProvider() { return imgProvider; } + CallManager *callManager() { return callManager_; } + + void clearAll() { rooms_->clear(); } - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - bool isNarrowView() const { return isNarrowView_; } bool isWindowFocused() const { return isWindowFocused_; } Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; - Q_INVOKABLE void openLink(QString link) const; - Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); - Q_INVOKABLE void openMemberListDialog() const; - Q_INVOKABLE void openLeaveRoomDialog() const; + Q_INVOKABLE void openMemberListDialog(QString roomid) const; + Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); void verifyUser(QString userid); void verifyDevice(QString userid, QString deviceid); signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); void replyingEventChanged(QString replyingEvent); void replyClosed(); void newDeviceVerificationRequest(DeviceVerificationFlow *flow); void inviteUsers(QStringList users); - void showRoomList(); - void narrowViewChanged(); void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); @@ -98,58 +86,32 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void initWithMessages(const std::vector<QString> &roomIds); + void initializeRoomlist(); void chatFocusChanged(bool focused) { isWindowFocused_ = focused; emit focusChanged(); } - void setHistoryView(const QString &room_id); - void highlightRoom(const QString &room_id); void showEvent(const QString &room_id, const QString &event_id); void focusTimeline(); - TimelineModel *getHistoryView(const QString &room_id) - { - auto room = models.find(room_id); - if (room != models.end()) - return room.value().data(); - else - return nullptr; - } void updateColorPalette(); void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody); - void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); - void updateEncryptedDescriptions(); void setVideoCallItem(); - void enableBackButton() - { - if (isNarrowView_) - return; - isNarrowView_ = true; - emit narrowViewChanged(); - } - void disableBackButton() - { - if (!isNarrowView_) - return; - isNarrowView_ = false; - emit narrowViewChanged(); - } - - void backToRooms() { emit showRoomList(); } QObject *completerFor(QString completerName, QString roomId = ""); void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); + RoomlistModel *rooms() { return rooms_; } + private slots: void openImageOverlayInternal(QString eventId, QImage img); @@ -165,14 +127,14 @@ private: ColorImageProvider *colorImgProvider; BlurhashProvider *blurhashProvider; - QHash<QString, QSharedPointer<TimelineModel>> models; - TimelineModel *timeline_ = nullptr; CallManager *callManager_ = nullptr; bool isInitialSync_ = true; - bool isNarrowView_ = false; bool isWindowFocused_ = false; + RoomlistModel *rooms_ = nullptr; + CommunitiesModel *communities_ = nullptr; + QHash<QString, QColor> userColors; QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList; diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index 54f48d3c..bbcedd7e 100644 --- a/src/ui/NhekoDropArea.cpp +++ b/src/ui/NhekoDropArea.cpp
@@ -35,7 +35,7 @@ void NhekoDropArea::dropEvent(QDropEvent *event) { if (event) { - auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_); + auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_); if (model) { model->input()->insertMimeData(event->mimeData()); } diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp new file mode 100644
index 00000000..fea10839 --- /dev/null +++ b/src/ui/NhekoGlobalObject.cpp
@@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "NhekoGlobalObject.h" + +#include <QDesktopServices> +#include <QUrl> + +#include "Cache_p.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "UserSettingsPage.h" +#include "Utils.h" + +Nheko::Nheko() +{ + connect( + UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged); + connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile); +} + +void +Nheko::updateUserProfile() +{ + if (cache::client() && cache::client()->isInitialized()) + currentUser_.reset( + new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager())); + else + currentUser_.reset(); + emit profileChanged(); +} + +QPalette +Nheko::colors() const +{ + return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString()); +} + +QPalette +Nheko::inactiveColors() const +{ + auto p = colors(); + p.setCurrentColorGroup(QPalette::ColorGroup::Inactive); + return p; +} + +Theme +Nheko::theme() const +{ + return Theme(UserSettings::instance()->theme().toStdString()); +} + +void +Nheko::openLink(QString link) const +{ + QUrl url(link); + if (url.scheme() == "https" && url.host() == "matrix.to") { + // handle matrix.to links internally + QString p = url.fragment(QUrl::FullyEncoded); + if (p.startsWith("/")) + p.remove(0, 1); + + auto temp = p.split("?"); + QString query; + if (temp.size() >= 2) + query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); + + temp = temp.first().split("/"); + auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); + QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); + if (!identifier.isEmpty()) { + if (identifier.startsWith("@")) { + QByteArray uri = + "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("#")) { + QByteArray uri = + "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("!")) { + QByteArray uri = "matrix:roomid/" + + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } + } + } else { + QDesktopServices::openUrl(url); + } +} +void +Nheko::setStatusMessage(QString msg) const +{ + ChatPage::instance()->setStatus(msg); +} + +UserProfile * +Nheko::currentUser() const +{ + nhlog::ui()->debug("Profile requested"); + + return currentUser_.get(); +} + +void +Nheko::showUserSettingsPage() const +{ + ChatPage::instance()->showUserSettingsPage(); +} + +void +Nheko::openLogoutDialog() const +{ + MainWindow::instance()->openLogoutDialog(); +} + +void +Nheko::openCreateRoomDialog() const +{ + MainWindow::instance()->openCreateRoomDialog( + [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); }); +} + +void +Nheko::openJoinRoomDialog() const +{ + MainWindow::instance()->openJoinRoomDialog( + [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h new file mode 100644
index 00000000..14135fd1 --- /dev/null +++ b/src/ui/NhekoGlobalObject.h
@@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QObject> +#include <QPalette> + +#include "Theme.h" +#include "UserProfile.h" + +class Nheko : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged) + Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged) + Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged) + Q_PROPERTY(int avatarSize READ avatarSize CONSTANT) + Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT) + Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT) + Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT) + + Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged) + +public: + Nheko(); + + QPalette colors() const; + QPalette inactiveColors() const; + Theme theme() const; + + int avatarSize() const { return 40; } + + int paddingSmall() const { return 4; } + int paddingMedium() const { return 8; } + int paddingLarge() const { return 20; } + UserProfile *currentUser() const; + + Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void setStatusMessage(QString msg) const; + Q_INVOKABLE void showUserSettingsPage() const; + Q_INVOKABLE void openLogoutDialog() const; + Q_INVOKABLE void openCreateRoomDialog() const; + Q_INVOKABLE void openJoinRoomDialog() const; + +public slots: + void updateUserProfile(); + +signals: + void colorsChanged(); + void profileChanged(); + +private: + QScopedPointer<UserProfile> currentUser_; +}; diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 4341bd63..26119393 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp
@@ -2,76 +2,73 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include <QDebug> - #include "Theme.h" -Theme::Theme(QObject *parent) - : QObject(parent) -{ - setColor("Black", ui::Color::Black); - - setColor("BrightWhite", ui::Color::BrightWhite); - setColor("FadedWhite", ui::Color::FadedWhite); - setColor("MediumWhite", ui::Color::MediumWhite); - - setColor("BrightGreen", ui::Color::BrightGreen); - setColor("DarkGreen", ui::Color::DarkGreen); - setColor("LightGreen", ui::Color::LightGreen); - - setColor("Gray", ui::Color::Gray); - setColor("Red", ui::Color::Red); - setColor("Blue", ui::Color::Blue); - - setColor("Transparent", ui::Color::Transparent); -} - -QColor -Theme::rgba(int r, int g, int b, qreal a) const -{ - QColor color(r, g, b); - color.setAlphaF(a); - - return color; -} +Q_DECLARE_METATYPE(Theme) -QColor -Theme::getColor(const QString &key) const +QPalette +Theme::paletteFromTheme(std::string_view theme) { - if (!colors_.contains(key)) { - qWarning() << "Color with key" << key << "could not be found"; - return QColor(); + [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme"); + static QPalette original; + if (theme == "light") { + QPalette lightActive( + /*windowText*/ QColor("#333"), + /*button*/ QColor("white"), + /*light*/ QColor(0xef, 0xef, 0xef), + /*dark*/ QColor(70, 77, 93), + /*mid*/ QColor(220, 220, 220), + /*text*/ QColor("#333"), + /*bright_text*/ QColor("#f2f5f8"), + /*base*/ QColor("#fff"), + /*window*/ QColor("white")); + lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); + lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5")); + lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); + lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); + lightActive.setColor(QPalette::Link, QColor("#0077b5")); + lightActive.setColor(QPalette::ButtonText, QColor("#555459")); + return lightActive; + } else if (theme == "dark") { + QPalette darkActive( + /*windowText*/ QColor("#caccd1"), + /*button*/ QColor(0xff, 0xff, 0xff), + /*light*/ QColor("#caccd1"), + /*dark*/ QColor(60, 70, 77), + /*mid*/ QColor("#202228"), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor("#f4f5f8"), + /*base*/ QColor("#202228"), + /*window*/ QColor("#2d3139")); + darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); + darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8")); + darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); + darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); + darkActive.setColor(QPalette::Link, QColor("#38a3d8")); + darkActive.setColor(QPalette::ButtonText, "#727274"); + return darkActive; + } else { + return original; } - - return colors_.value(key); } -void -Theme::setColor(const QString &key, const QColor &color) +Theme::Theme(std::string_view theme) { - colors_.insert(key, color); -} - -void -Theme::setColor(const QString &key, ui::Color color) -{ - static const QColor palette[] = { - QColor("#171919"), - - QColor("#EBEBEB"), - QColor("#C9C9C9"), - QColor("#929292"), - - QColor("#1C3133"), - QColor("#577275"), - QColor("#46A451"), - - QColor("#5D6565"), - QColor("#E22826"), - QColor("#81B3A9"), - - rgba(0, 0, 0, 0), - }; - - colors_.insert(key, palette[static_cast<int>(color)]); + auto p = paletteFromTheme(theme); + separator_ = p.mid().color(); + if (theme == "light") { + sidebarBackground_ = QColor("#233649"); + alternateButton_ = QColor("#ccc"); + red_ = QColor("#a82353"); + } else if (theme == "dark") { + sidebarBackground_ = QColor("#2d3139"); + alternateButton_ = QColor("#414A59"); + red_ = QColor("#a82353"); + } else { + sidebarBackground_ = p.window().color(); + alternateButton_ = p.dark().color(); + red_ = QColor("red"); + } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 3243c076..b5bcd4dd 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h
@@ -5,8 +5,7 @@ #pragma once #include <QColor> -#include <QHash> -#include <QObject> +#include <QPalette> namespace ui { enum class AvatarType @@ -60,36 +59,25 @@ enum class ProgressType IndeterminateProgress }; -enum class Color -{ - Black, - BrightWhite, - FadedWhite, - MediumWhite, - DarkGreen, - LightGreen, - BrightGreen, - Gray, - Red, - Blue, - Transparent -}; - } // namespace ui -class Theme : public QObject +class Theme : public QPalette { - Q_OBJECT + Q_GADGET + Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) + Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) + Q_PROPERTY(QColor separator READ separator CONSTANT) + Q_PROPERTY(QColor red READ red CONSTANT) public: - explicit Theme(QObject *parent = nullptr); + Theme() {} + explicit Theme(std::string_view theme); + static QPalette paletteFromTheme(std::string_view theme); - QColor getColor(const QString &key) const; - - void setColor(const QString &key, const QColor &color); - void setColor(const QString &key, ui::Color color); + QColor sidebarBackground() const { return sidebarBackground_; } + QColor alternateButton() const { return alternateButton_; } + QColor separator() const { return separator_; } + QColor red() const { return red_; } private: - QColor rgba(int r, int g, int b, qreal a) const; - - QHash<QString, QColor> colors_; + QColor sidebarBackground_, separator_, red_, alternateButton_; }; diff --git a/src/ui/ThemeManager.cpp b/src/ui/ThemeManager.cpp
index 834f5083..b7b3df40 100644 --- a/src/ui/ThemeManager.cpp +++ b/src/ui/ThemeManager.cpp
@@ -6,18 +6,37 @@ #include "ThemeManager.h" -ThemeManager::ThemeManager() { setTheme(new Theme); } - -void -ThemeManager::setTheme(Theme *theme) -{ - theme_ = theme; - theme_->setParent(this); -} +ThemeManager::ThemeManager() {} QColor ThemeManager::themeColor(const QString &key) const { - Q_ASSERT(theme_); - return theme_->getColor(key); + if (key == "Black") + return QColor("#171919"); + + else if (key == "BrightWhite") + return QColor("#EBEBEB"); + else if (key == "FadedWhite") + return QColor("#C9C9C9"); + else if (key == "MediumWhite") + return QColor("#929292"); + + else if (key == "BrightGreen") + return QColor("#1C3133"); + else if (key == "DarkGreen") + return QColor("#577275"); + else if (key == "LightGreen") + return QColor("#46A451"); + + else if (key == "Gray") + return QColor("#5D6565"); + else if (key == "Red") + return QColor("#E22826"); + else if (key == "Blue") + return QColor("#81B3A9"); + + else if (key == "Transparent") + return QColor(0, 0, 0, 0); + + return (QColor(0, 0, 0, 0)); } diff --git a/src/ui/ThemeManager.h b/src/ui/ThemeManager.h
index f2099730..cbb355fd 100644 --- a/src/ui/ThemeManager.h +++ b/src/ui/ThemeManager.h
@@ -6,8 +6,6 @@ #include <QCommonStyle> -#include "Theme.h" - class ThemeManager : public QCommonStyle { Q_OBJECT @@ -15,7 +13,6 @@ class ThemeManager : public QCommonStyle public: inline static ThemeManager &instance(); - void setTheme(Theme *theme); QColor themeColor(const QString &key) const; private: @@ -23,8 +20,6 @@ private: ThemeManager(ThemeManager const &); void operator=(ThemeManager const &); - - Theme *theme_; }; inline ThemeManager & diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index 0f330964..3d9c4b6a 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp
@@ -27,9 +27,22 @@ UserProfile::UserProfile(QString roomid, , manager(manager_) , model(parent) { - fetchDeviceList(this->userid_); globalAvatarUrl = ""; + connect(this, + &UserProfile::globalUsernameRetrieved, + this, + &UserProfile::setGlobalUsername, + Qt::QueuedConnection); + + if (isGlobalUserProfile()) { + getGlobalProfileData(); + } + + if (!cache::client() || !cache::client()->isDatabaseReady() || + !ChatPage::instance()->timelineManager()) + return; + connect(cache::client(), &Cache::verificationStatusChanged, this, @@ -53,17 +66,9 @@ UserProfile::UserProfile(QString roomid, : verification::VERIFIED; } deviceList_.reset(deviceList_.deviceList_); + emit devicesChanged(); }); - - connect(this, - &UserProfile::globalUsernameRetrieved, - this, - &UserProfile::setGlobalUsername, - Qt::QueuedConnection); - - if (isGlobalUserProfile()) { - getGlobalProfileData(); - } + fetchDeviceList(this->userid_); } QHash<int, QByteArray> @@ -123,10 +128,7 @@ UserProfile::displayName() QString UserProfile::avatarUrl() { - return (isGlobalUserProfile() && globalAvatarUrl != "") - ? globalAvatarUrl - : cache::avatarUrl(roomid_, userid_); - ; + return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_); } bool @@ -157,6 +159,9 @@ UserProfile::fetchDeviceList(const QString &userID) { auto localUser = utils::localUser(); + if (!cache::client() || !cache::client()->isDatabaseReady()) + return; + cache::client()->query_keys( userID.toStdString(), [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys, @@ -217,6 +222,7 @@ UserProfile::fetchDeviceList(const QString &userID) } this->deviceList_.queueReset(std::move(deviceInfo)); + emit devicesChanged(); }); }); } diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
index bf71d0de..721d7230 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h
@@ -90,7 +90,7 @@ class UserProfile : public QObject Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) Q_PROPERTY(QString userid READ userid CONSTANT) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT) + Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) @@ -133,6 +133,7 @@ signals: void avatarUrlChanged(); void displayError(const QString &errorMessage); void globalUsernameRetrieved(const QString &globalUser); + void devicesChanged(); public slots: void updateAvatarUrl();