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(¬ificationsManager,
&NotificationsManager::notificationClicked,
this,
[this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid)
- room_list_->highlightSelectedRoom(roomid);
+ view_manager_->rooms()->setCurrentRoom(roomid);
activateWindow();
});
connect(¬ificationsManager,
&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,
¬ificationsManager,
@@ -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> ¬ifs);
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> ¬ifs)
-{
- nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
-
- for (const auto &item : notifs) {
- for (const auto ¬if : 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 ¤t_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> ¬ifs);
- 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 ¤t_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();
|