From 312df6f3bbcba0ad502864b13f9c51b4854ea2ce Mon Sep 17 00:00:00 2001 From: Max Sandholm Date: Tue, 9 Jan 2018 15:07:32 +0200 Subject: [PATCH] Communities (#195) --- .gitignore | 3 + CMakeLists.txt | 6 + include/ChatPage.h | 18 ++- include/CommunitiesList.h | 42 +++++++ include/CommunitiesListItem.h | 98 ++++++++++++++++ include/Community.h | 62 ++++++++++ include/MatrixClient.h | 8 ++ include/RoomInfoListItem.h | 13 ++- include/RoomList.h | 6 + include/ui/Theme.h | 5 +- resources/icons/ui/world.png | Bin 0 -> 2863 bytes resources/icons/ui/world.svg | 98 ++++++++++++++++ resources/res.qrc | 1 + resources/styles/nheko-dark.qss | 10 ++ resources/styles/nheko.qss | 11 ++ resources/styles/system.qss | 6 + src/ChatPage.cc | 80 ++++++++++++- src/CommunitiesList.cc | 150 ++++++++++++++++++++++++ src/CommunitiesListItem.cc | 200 ++++++++++++++++++++++++++++++++ src/Community.cc | 44 +++++++ src/MatrixClient.cc | 148 ++++++++++++++++++++++- src/RoomInfoListItem.cc | 5 +- src/RoomList.cc | 53 ++++++++- 23 files changed, 1054 insertions(+), 13 deletions(-) create mode 100644 include/CommunitiesList.h create mode 100644 include/CommunitiesListItem.h create mode 100644 include/Community.h create mode 100644 resources/icons/ui/world.png create mode 100644 resources/icons/ui/world.svg create mode 100644 src/CommunitiesList.cc create mode 100644 src/CommunitiesListItem.cc create mode 100644 src/Community.cc diff --git a/.gitignore b/.gitignore index 96ace50f..e7df9077 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ ui_*.h *.autosave +# VSCode +.vscode/* + #QtCtreator Qml *.qmlproject.user *.qmlproject.user.* diff --git a/CMakeLists.txt b/CMakeLists.txt index b0a5c610..29c28527 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,9 @@ set(SRC_FILES src/AvatarProvider.cc src/Cache.cc src/ChatPage.cc + src/CommunitiesListItem.cc + src/CommunitiesList.cc + src/Community.cc src/Deserializable.cc src/InviteeItem.cc src/InputValidator.cc @@ -265,6 +268,9 @@ qt5_wrap_cpp(MOC_HEADERS include/AvatarProvider.h include/ChatPage.h + include/CommunitiesListItem.h + include/CommunitiesList.h + include/Community.h include/LoginPage.h include/MainWindow.h include/InviteeItem.h diff --git a/include/ChatPage.h b/include/ChatPage.h index 584424c0..754ee0f4 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -24,6 +24,8 @@ #include #include +#include "CommunitiesList.h" +#include "Community.h" #include class Cache; @@ -80,6 +82,7 @@ private slots: void showUnreadMessageNotification(int count); void updateTopBarAvatar(const QString &roomid, const QPixmap &img); void updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_name); + void updateOwnCommunitiesInfo(const QList &own_communities); void setOwnAvatar(const QPixmap &img); void initialSyncCompleted(const mtx::responses::Sync &response); void syncCompleted(const mtx::responses::Sync &response); @@ -126,13 +129,21 @@ private: QHBoxLayout *topLayout_; Splitter *splitter; - QFrame *sideBar_; + QWidget *sideBar_; + QWidget *communitiesSideBar_; + QVBoxLayout *communitiesSideBarLayout_; QVBoxLayout *sideBarLayout_; + QVBoxLayout *sideBarTopLayout_; + QVBoxLayout *sideBarMainLayout_; + QWidget *sideBarTopWidget_; + QVBoxLayout *sideBarTopWidgetLayout_; QFrame *content_; QVBoxLayout *contentLayout_; + CommunitiesList *communitiesList_; RoomList *room_list_; + TimelineViewManager *view_manager_; SideBarActions *sidebarActions_; @@ -145,13 +156,18 @@ private: QTimer *consensusTimer_; QString current_room_; + QString current_community_; + QMap room_avatars_; + QMap community_avatars_; UserInfoWidget *user_info_widget_; QMap state_manager_; QMap> settingsManager_; + QMap> communityManager_; + // Keeps track of the users currently typing on each room. QMap> typingUsers_; QTimer *typingRefresher_; diff --git a/include/CommunitiesList.h b/include/CommunitiesList.h new file mode 100644 index 00000000..53715363 --- /dev/null +++ b/include/CommunitiesList.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include + +#include "CommunitiesListItem.h" +#include "Community.h" +#include "MatrixClient.h" +#include "ui/Theme.h" + +class CommunitiesList : public QWidget +{ + Q_OBJECT + +public: + CommunitiesList(QSharedPointer client, QWidget *parent = nullptr); + ~CommunitiesList(); + + void setCommunities(const QMap> &communities); + void clear(); + + void addCommunity(QSharedPointer community, const QString &community_id); + void removeCommunity(const QString &community_id); +signals: + void communityChanged(const QString &community_id); + +public slots: + void updateCommunityAvatar(const QString &community_id, const QPixmap &img); + void highlightSelectedCommunity(const QString &community_id); + +private: + QVBoxLayout *topLayout_; + QVBoxLayout *contentsLayout_; + QWidget *scrollAreaContents_; + QScrollArea *scrollArea_; + + QMap> communities_; + + QSharedPointer client_; +}; diff --git a/include/CommunitiesListItem.h b/include/CommunitiesListItem.h new file mode 100644 index 00000000..099b4fa2 --- /dev/null +++ b/include/CommunitiesListItem.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Community.h" +#include "Menu.h" +#include "ui/Theme.h" + +class CommunitiesListItem : 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) + +public: + CommunitiesListItem(QSharedPointer community, + QString community_id, + QWidget *parent = nullptr); + + ~CommunitiesListItem(); + + void setCommunity(QSharedPointer community); + + inline bool isPressed() const; + inline void setAvatar(const QImage &avatar_image); + + QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } + QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } + QColor backgroundColor() const { return backgroundColor_; } + + void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } + void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } + void setBackgroundColor(QColor &color) { backgroundColor_ = color; } + + QColor highlightedBackgroundColor_; + QColor hoverBackgroundColor_; + QColor backgroundColor_; + +signals: + void clicked(const QString &community_id); + +public slots: + void setPressedState(bool state); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + const int IconSize = 55; + + QSharedPointer community_; + QString communityId_; + QString communityName_; + QString communityShortDescription; + + QPixmap communityAvatar_; + + Menu *menu_; + bool isPressed_ = false; +}; + +inline bool +CommunitiesListItem::isPressed() const +{ + return isPressed_; +} + +inline void +CommunitiesListItem::setAvatar(const QImage &avatar_image) +{ + communityAvatar_ = QPixmap::fromImage( + avatar_image.scaled(IconSize, IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + update(); +} + +class WorldCommunityListItem : public CommunitiesListItem +{ + Q_OBJECT +public: + WorldCommunityListItem(QWidget *parent = nullptr); + ~WorldCommunityListItem(); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + const int IconSize = 55; +}; diff --git a/include/Community.h b/include/Community.h new file mode 100644 index 00000000..0d70dee1 --- /dev/null +++ b/include/Community.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +class Community : public QObject +{ + Q_OBJECT + +public: + void parseProfile(const QJsonObject &profile); + void parseRooms(const QJsonObject &rooms); + + inline QUrl getAvatar() const; + inline QString getName() const; + inline QString getShortDescription() const; + inline QString getLongDescription() const; + inline const QList getRoomList() const; + +signals: + void roomsChanged(QList &rooms); + +private: + QUrl avatar_; + QString name_; + QString short_description_; + QString long_description_; + + QList rooms_; +}; + +inline QUrl +Community::getAvatar() const +{ + return avatar_; +} + +inline QString +Community::getName() const +{ + return name_; +} + +inline QString +Community::getShortDescription() const +{ + return short_description_; +} + +inline QString +Community::getLongDescription() const +{ + return long_description_; +} + +inline const QList +Community::getRoomList() const +{ + return rooms_; +} diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 2627f578..8936003f 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -48,6 +48,9 @@ public: void versions() noexcept; void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); void fetchUserAvatar(const QString &userId, const QUrl &avatarUrl); + void fetchCommunityAvatar(const QString &communityId, const QUrl &avatarUrl); + void fetchCommunityProfile(const QString &communityId); + void fetchCommunityRooms(const QString &communityId); void fetchOwnAvatar(const QUrl &avatar_url); void downloadImage(const QString &event_id, const QUrl &url); void downloadFile(const QString &event_id, const QUrl &url); @@ -71,6 +74,7 @@ public: public slots: void getOwnProfile() noexcept; + void getOwnCommunities() noexcept; void logout() noexcept; void setServer(const QString &server) @@ -103,12 +107,16 @@ signals: const QString &url, const QByteArray &data); void userAvatarRetrieved(const QString &userId, const QImage &img); + void communityAvatarRetrieved(const QString &communityId, const QPixmap &img); + void communityProfileRetrieved(const QString &communityId, const QJsonObject &profile); + void communityRoomsRetrieved(const QString &communityId, const QJsonObject &rooms); void ownAvatarRetrieved(const QPixmap &img); void imageDownloaded(const QString &event_id, const QPixmap &img); void fileDownloaded(const QString &event_id, const QByteArray &data); // Returned profile data for the user's account. void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name); + void getOwnCommunitiesResponse(const QList &own_communities); void initialSyncCompleted(const mtx::responses::Sync &response); void initialSyncFailed(const QString &msg); void syncCompleted(const mtx::responses::Sync &response); diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h index 799e95bb..5cfea783 100644 --- a/include/RoomInfoListItem.h +++ b/include/RoomInfoListItem.h @@ -73,9 +73,10 @@ public: void clearUnreadMessageCount(); void setState(const RoomState &state); - bool isPressed() const { return isPressed_; } - RoomState state() const { return state_; } - int unreadMessageCount() const { return unreadMsgCount_; } + QString roomId(); + bool isPressed() const { return isPressed_; }; + RoomState state() const { return state_; }; + int unreadMessageCount() const { return unreadMsgCount_; }; void setAvatar(const QImage &avatar_image); void setDescriptionMessage(const DescInfo &info); @@ -182,3 +183,9 @@ private: QRectF acceptBtnRegion_; QRectF declineBtnRegion_; }; + +inline QString +RoomInfoListItem::roomId() +{ + return roomId_; +} diff --git a/include/RoomList.h b/include/RoomList.h index 6b2151a2..d10cf5db 100644 --- a/include/RoomList.h +++ b/include/RoomList.h @@ -64,6 +64,8 @@ public: const QString &room_id); void addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room); void removeRoom(const QString &room_id, bool reset); + void setFilterRooms(bool filterRooms); + void setRoomFilter(QList room_ids); signals: void roomChanged(const QString &room_id); @@ -105,6 +107,10 @@ private: QSharedPointer leaveRoomDialog_; QMap> rooms_; + QString selectedRoom_; + + bool filterRooms_ = false; + QList roomFilter_ = QList(); // which rooms to include in the room list QSharedPointer client_; QSharedPointer cache_; diff --git a/include/ui/Theme.h b/include/ui/Theme.h index c6c39553..c2e4ab59 100644 --- a/include/ui/Theme.h +++ b/include/ui/Theme.h @@ -13,8 +13,9 @@ enum class AvatarType }; namespace sidebar { -static const int SmallSize = 60; -static const int NormalSize = 300; +static const int SmallSize = 60; +static const int NormalSize = 300; +static const int CommunitiesSidebarSize = 64; } // Default font size. const int FontSize = 16; diff --git a/resources/icons/ui/world.png b/resources/icons/ui/world.png new file mode 100644 index 0000000000000000000000000000000000000000..d687d1413cc1c6345e6f8e9831541b90520eed14 GIT binary patch literal 2863 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 literal 0 HcmV?d00001 diff --git a/resources/icons/ui/world.svg b/resources/icons/ui/world.svg new file mode 100644 index 00000000..c3acf162 --- /dev/null +++ b/resources/icons/ui/world.svg @@ -0,0 +1,98 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/resources/res.qrc b/resources/res.qrc index 83415e9b..a5461718 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -22,6 +22,7 @@ icons/ui/paper-clip-outline@2x.png icons/ui/angle-pointing-to-left.png icons/ui/angle-pointing-to-left@2x.png + icons/ui/world.png icons/ui/angle-arrow-down.png icons/ui/angle-arrow-down@2x.png icons/ui/arrow-pointing-down.png diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index a78fb612..26425590 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -17,6 +17,10 @@ RoomList > * { background-color: #383c4a; } +CommunitiesList, +CommunitiesList > * { + background-color: #383c4a; +} FlatButton { qproperty-foregroundColor: #caccd1; qproperty-backgroundColor: #333; @@ -54,6 +58,12 @@ RoomInfoListItem { qproperty-btnTextColor: white; } +CommunitiesListItem { + qproperty-highlightedBackgroundColor: #5294e2; + qproperty-hoverBackgroundColor: #39679e; + qproperty-backgroundColor: #383c4a; +} + LoadingIndicator { qproperty-color: #caccd1; } diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index ce86e212..c135c12a 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -17,6 +17,11 @@ RoomList > * { background-color: white; } +CommunitiesList, +CommunitiesList > * { + background-color: white; +} + FlatButton { qproperty-foregroundColor: #333; } @@ -52,6 +57,12 @@ RoomInfoListItem { qproperty-btnTextColor: #333; } +CommunitiesListItem { + qproperty-highlightedBackgroundColor: #38A3D8; + qproperty-hoverBackgroundColor: rgba(200, 200, 200, 128); + qproperty-backgroundColor: white; +} + #ChatPageLoadSpinner { qproperty-color: #acc7dc; } diff --git a/resources/styles/system.qss b/resources/styles/system.qss index afb2ad26..42aba09d 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -60,6 +60,12 @@ RoomInfoListItem { qproperty-btnTextColor: palette(text); } +CommunitiesListItem { + qproperty-highlightedBackgroundColor: palette(highlight); + qproperty-hoverBackgroundColor: palette(mid); + qproperty-backgroundColor: palette(window); +} + LoadingIndicator { qproperty-color: palette(highlight); } diff --git a/src/ChatPage.cc b/src/ChatPage.cc index 3958e2c2..3a78e1cc 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -60,6 +60,17 @@ ChatPage::ChatPage(QSharedPointer client, topLayout_->setSpacing(0); topLayout_->setMargin(0); + communitiesSideBar_ = new QWidget(this); + communitiesSideBar_->setFixedWidth(ui::sidebar::CommunitiesSidebarSize); + communitiesSideBarLayout_ = new QVBoxLayout(communitiesSideBar_); + communitiesSideBarLayout_->setSpacing(0); + communitiesSideBarLayout_->setMargin(0); + + communitiesList_ = new CommunitiesList(client, this); + communitiesSideBarLayout_->addWidget(communitiesList_); + // communitiesSideBarLayout_->addStretch(1); + topLayout_->addWidget(communitiesSideBar_); + auto splitter = new Splitter(this); splitter->setHandleWidth(0); @@ -72,7 +83,18 @@ ChatPage::ChatPage(QSharedPointer client, sideBarLayout_->setSpacing(0); sideBarLayout_->setMargin(0); - sidebarActions_ = new SideBarActions(this); + sideBarTopLayout_ = new QVBoxLayout(); + sideBarTopLayout_->setSpacing(0); + sideBarTopLayout_->setMargin(0); + sideBarMainLayout_ = new QVBoxLayout(); + sideBarMainLayout_->setSpacing(0); + sideBarMainLayout_->setMargin(0); + + sideBarLayout_->addLayout(sideBarTopLayout_); + sideBarLayout_->addLayout(sideBarMainLayout_); + + sideBarTopWidget_ = new QWidget(sideBar_); + sidebarActions_ = new SideBarActions(this); connect( sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); connect( @@ -87,6 +109,10 @@ ChatPage::ChatPage(QSharedPointer client, 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"); @@ -274,6 +300,32 @@ ChatPage::ChatPage(QSharedPointer client, &MatrixClient::getOwnProfileResponse, this, &ChatPage::updateOwnProfileInfo); + connect(client_.data(), + SIGNAL(getOwnCommunitiesResponse(QList)), + this, + SLOT(updateOwnCommunitiesInfo(QList))); + connect(client_.data(), + &MatrixClient::communityProfileRetrieved, + this, + [=](QString communityId, QJsonObject profile) { + communityManager_[communityId]->parseProfile(profile); + }); + connect(client_.data(), + &MatrixClient::communityRoomsRetrieved, + this, + [=](QString communityId, QJsonObject rooms) { + communityManager_[communityId]->parseRooms(rooms); + + if (communityId == current_community_) { + if (communityId == "world") { + room_list_->setFilterRooms(false); + } else { + room_list_->setRoomFilter( + communityManager_[communityId]->getRoomList()); + } + } + }); + connect(client_.data(), &MatrixClient::ownAvatarRetrieved, this, &ChatPage::setOwnAvatar); connect(client_.data(), &MatrixClient::joinedRoom, this, [=](const QString &room_id) { emit showNotification("You joined the room."); @@ -304,6 +356,19 @@ ChatPage::ChatPage(QSharedPointer client, } }); + connect(communitiesList_, + &CommunitiesList::communityChanged, + this, + [=](const QString &communityId) { + current_community_ = communityId; + if (communityId == "world") { + room_list_->setFilterRooms(false); + } else { + room_list_->setRoomFilter( + communityManager_[communityId]->getRoomList()); + } + }); + AvatarProvider::init(client); instance_ = this; @@ -359,6 +424,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) client_->setServer(homeserver); client_->setAccessToken(token); client_->getOwnProfile(); + client_->getOwnCommunities(); cache_ = QSharedPointer(new Cache(userid)); room_list_->setCache(cache_); @@ -500,6 +566,18 @@ ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &display_na client_->fetchOwnAvatar(avatar_url); } +void +ChatPage::updateOwnCommunitiesInfo(const QList &own_communities) +{ + for (int i = 0; i < own_communities.size(); i++) { + QSharedPointer community = QSharedPointer(new Community()); + + communityManager_[own_communities[i]] = community; + } + + communitiesList_->setCommunities(communityManager_); +} + void ChatPage::changeTopRoomInfo(const QString &room_id) { diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc new file mode 100644 index 00000000..c40155e5 --- /dev/null +++ b/src/CommunitiesList.cc @@ -0,0 +1,150 @@ +#include "CommunitiesList.h" + +#include + +CommunitiesList::CommunitiesList(QSharedPointer client, QWidget *parent) + : QWidget(parent) + , client_(client) +{ + QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(1); + setSizePolicy(sizePolicy); + + setStyleSheet("border-style: none;"); + + topLayout_ = new QVBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); + + setFixedWidth(ui::sidebar::CommunitiesSidebarSize); + + 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); + + scrollAreaContents_ = new QWidget(); + + contentsLayout_ = new QVBoxLayout(scrollAreaContents_); + contentsLayout_->setSpacing(0); + contentsLayout_->setMargin(0); + + WorldCommunityListItem *world_list_item = new WorldCommunityListItem(); + contentsLayout_->addWidget(world_list_item); + communities_.insert("world", QSharedPointer(world_list_item)); + connect(world_list_item, + &WorldCommunityListItem::clicked, + this, + &CommunitiesList::highlightSelectedCommunity); + contentsLayout_->addStretch(1); + + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addWidget(scrollArea_); + + connect(client_.data(), + &MatrixClient::communityProfileRetrieved, + this, + [=](QString communityId, QJsonObject profile) { + client_->fetchCommunityAvatar(communityId, + QUrl(profile["avatar_url"].toString())); + }); + connect(client_.data(), + SIGNAL(communityAvatarRetrieved(const QString &, const QPixmap &)), + this, + SLOT(updateCommunityAvatar(const QString &, const QPixmap &))); +} + +CommunitiesList::~CommunitiesList() {} + +void +CommunitiesList::setCommunities(const QMap> &communities) +{ + communities_.clear(); + + // TODO: still not sure how to handle the "world" special-case + WorldCommunityListItem *world_list_item = new WorldCommunityListItem(); + communities_.insert("world", QSharedPointer(world_list_item)); + connect(world_list_item, + &WorldCommunityListItem::clicked, + this, + &CommunitiesList::highlightSelectedCommunity); + contentsLayout_->insertWidget(0, world_list_item); + + for (auto it = communities.constBegin(); it != communities.constEnd(); it++) { + const auto community_id = it.key(); + const auto community = it.value(); + + addCommunity(community, community_id); + + client_->fetchCommunityProfile(community_id); + client_->fetchCommunityRooms(community_id); + } + + world_list_item->setPressedState(true); + emit communityChanged("world"); +} + +void +CommunitiesList::clear() +{ + communities_.clear(); +} + +void +CommunitiesList::addCommunity(QSharedPointer community, const QString &community_id) +{ + CommunitiesListItem *list_item = + new CommunitiesListItem(community, community_id, scrollArea_); + + communities_.insert(community_id, QSharedPointer(list_item)); + + client_->fetchCommunityAvatar(community_id, community->getAvatar()); + + contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); + + connect(list_item, + &CommunitiesListItem::clicked, + this, + &CommunitiesList::highlightSelectedCommunity); +} + +void +CommunitiesList::removeCommunity(const QString &community_id) +{ + communities_.remove(community_id); +} + +void +CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) +{ + if (!communities_.contains(community_id)) { + qWarning() << "Avatar update on nonexistent community" << community_id; + return; + } + + communities_.value(community_id)->setAvatar(img.toImage()); +} + +void +CommunitiesList::highlightSelectedCommunity(const QString &community_id) +{ + emit communityChanged(community_id); + + if (!communities_.contains(community_id)) { + qDebug() << "CommunitiesList: clicked unknown community"; + return; + } + + for (auto it = communities_.constBegin(); it != communities_.constEnd(); it++) { + if (it.key() != community_id) { + it.value()->setPressedState(false); + } else { + it.value()->setPressedState(true); + scrollArea_->ensureWidgetVisible( + qobject_cast(it.value().data())); + } + } +} diff --git a/src/CommunitiesListItem.cc b/src/CommunitiesListItem.cc new file mode 100644 index 00000000..a7789df7 --- /dev/null +++ b/src/CommunitiesListItem.cc @@ -0,0 +1,200 @@ +#include "CommunitiesListItem.h" + +CommunitiesListItem::CommunitiesListItem(QSharedPointer community, + QString community_id, + QWidget *parent) + : QWidget(parent) + , community_(community) + , communityId_(community_id) +{ + // menu_ = new Menu(this); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setFixedHeight(ui::sidebar::CommunitiesSidebarSize); + setFixedWidth(ui::sidebar::CommunitiesSidebarSize); +} + +CommunitiesListItem::~CommunitiesListItem() {} + +void +CommunitiesListItem::setCommunity(QSharedPointer community) +{ + community_ = community; +} + +void +CommunitiesListItem::setPressedState(bool state) +{ + if (isPressed_ != state) { + isPressed_ = state; + update(); + } +} + +void +CommunitiesListItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::RightButton) { + QWidget::mousePressEvent(event); + return; + } + + emit clicked(communityId_); + + setPressedState(true); +} + +void +CommunitiesListItem::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter p(this); + p.setRenderHint(QPainter::TextAntialiasing); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::Antialiasing); + + if (isPressed_) + p.fillRect(rect(), highlightedBackgroundColor_); + else if (underMouse()) + p.fillRect(rect(), hoverBackgroundColor_); + else + p.fillRect(rect(), backgroundColor_); + + QFont font; + font.setPixelSize(conf::fontSize); + + p.setPen(QColor("#333")); + + QRect avatarRegion((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize); + + font.setBold(false); + p.setPen(Qt::NoPen); + + // We using the first letter of room's name. + if (communityAvatar_.isNull()) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor("#eee"); + + p.setPen(Qt::NoPen); + p.setBrush(brush); + + p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2); + + font.setPixelSize(conf::roomlist::fonts::bubble); + p.setFont(font); + p.setPen(QColor("#000")); + p.setBrush(Qt::NoBrush); + p.drawText( + avatarRegion.translated(0, -1), Qt::AlignCenter, QChar(community_->getName()[0])); + } else { + p.save(); + + QPainterPath path; + path.addEllipse( + (width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize); + p.setClipPath(path); + + p.drawPixmap(avatarRegion, communityAvatar_); + p.restore(); + } + + // TODO: Discord-style community ping counts? + /*if (unreadMsgCount_ > 0) { + QColor textColor("white"); + QColor backgroundColor("#38A3D8"); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor); + + if (isPressed_) + brush.setColor(textColor); + + QFont unreadCountFont; + unreadCountFont.setPixelSize(conf::roomlist::fonts::badge); + unreadCountFont.setBold(true); + + p.setBrush(brush); + p.setPen(Qt::NoPen); + p.setFont(unreadCountFont); + + int diameter = 20; + + QRectF r( + width() - diameter - 5, height() - diameter - 5, diameter, diameter); + + p.setPen(Qt::NoPen); + p.drawEllipse(r); + + p.setPen(QPen(textColor)); + + if (isPressed_) + p.setPen(QPen(backgroundColor)); + + p.setBrush(Qt::NoBrush); + p.drawText( + r.translated(0, -0.5), Qt::AlignCenter, QString::number(unreadMsgCount_)); + }*/ +} + +void +CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event) +{ + Q_UNUSED(event); + + // menu_->popup(event->globalPos()); +} + +WorldCommunityListItem::WorldCommunityListItem(QWidget *parent) + : CommunitiesListItem(QSharedPointer(), "", parent) +{} + +WorldCommunityListItem::~WorldCommunityListItem() {} + +void +WorldCommunityListItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::RightButton) { + QWidget::mousePressEvent(event); + return; + } + + emit CommunitiesListItem::clicked("world"); + + setPressedState(true); +} + +void +WorldCommunityListItem::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + static QPixmap worldIcon(":/icons/icons/ui/world.png"); + + QPainter p(this); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::Antialiasing); + + if (isPressed()) + p.fillRect(rect(), highlightedBackgroundColor_); + else if (underMouse()) + p.fillRect(rect(), hoverBackgroundColor_); + else + p.fillRect(rect(), backgroundColor_); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor("#FFFFFF"); + + p.setPen(Qt::NoPen); + p.setBrush(brush); + + QRect avatarRegion((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize); + p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2); + QPainterPath path; + path.addEllipse((width() - IconSize) / 2, (height() - IconSize) / 2, IconSize, IconSize); + p.setClipPath(path); + + p.drawPixmap(avatarRegion, worldIcon); +} diff --git a/src/Community.cc b/src/Community.cc new file mode 100644 index 00000000..df425e88 --- /dev/null +++ b/src/Community.cc @@ -0,0 +1,44 @@ +#include "include/Community.h" + +#include +#include + +void +Community::parseProfile(const QJsonObject &profile) +{ + if (profile["name"].type() == QJsonValue::Type::String) { + name_ = profile["name"].toString(); + } else { + name_ = "Unnamed Community"; // TODO: what is correct here? + } + + if (profile["avatar_url"].type() == QJsonValue::Type::String) { + avatar_ = QUrl(profile["avatar_url"].toString()); + } else { + avatar_ = QUrl(); + } + + if (profile["short_description"].type() == QJsonValue::Type::String) { + short_description_ = profile["short_description"].toString(); + } else { + short_description_ = ""; + } + + if (profile["long_description"].type() == QJsonValue::Type::String) { + long_description_ = profile["long_description"].toString(); + } else { + long_description_ = ""; + } +} + +void +Community::parseRooms(const QJsonObject &rooms) +{ + rooms_.clear(); + + for (auto i = 0; i < rooms["chunk"].toArray().size(); i++) { + rooms_.append(rooms["chunk"].toArray()[i].toObject()["room_id"].toString()); + } + + emit roomsChanged(rooms_); +} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index 1b2e020d..72467385 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -112,7 +112,6 @@ MatrixClient::login(const QString &username, const QString &password) noexcept } }); } - void MatrixClient::logout() noexcept { @@ -444,6 +443,46 @@ MatrixClient::getOwnProfile() noexcept }); } +void +MatrixClient::getOwnCommunities() noexcept +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(clientApiUrl_ + "/joined_groups"); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + auto json = QJsonDocument::fromJson(data).object(); + + try { + QList response; + for (auto it = json["groups"].toArray().constBegin(); + it != json["groups"].toArray().constEnd(); + it++) { + response.append(it->toString()); + } + emit getOwnCommunitiesResponse(response); + } catch (DeserializationException &e) { + qWarning() << "Own communities:" << e.what(); + } + }); +} + void MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url) { @@ -490,6 +529,113 @@ MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url) }); } +void +MatrixClient::fetchCommunityAvatar(const QString &communityId, const QUrl &avatar_url) +{ + QList url_parts = avatar_url.toString().split("mxc://"); + + if (url_parts.size() != 2) { + qDebug() << "Invalid format for community avatar " << avatar_url.toString(); + return; + } + + QUrlQuery query; + query.addQueryItem("width", "512"); + query.addQueryItem("height", "512"); + query.addQueryItem("method", "crop"); + + QString media_url = + QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]); + + QUrl endpoint(media_url); + endpoint.setQuery(query); + + QNetworkRequest avatar_request(endpoint); + + QNetworkReply *reply = get(avatar_request); + connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto img = reply->readAll(); + + if (img.size() == 0) + return; + + QPixmap pixmap; + pixmap.loadFromData(img); + + emit communityAvatarRetrieved(communityId, pixmap); + }); +} + +void +MatrixClient::fetchCommunityProfile(const QString &communityId) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/profile"); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + + connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + const auto json = QJsonDocument::fromJson(data).object(); + + emit communityProfileRetrieved(communityId, json); + }); +} + +void +MatrixClient::fetchCommunityRooms(const QString &communityId) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(clientApiUrl_ + "/groups/" + communityId + "/rooms"); + endpoint.setQuery(query); + + QNetworkRequest request(QString(endpoint.toEncoded())); + + QNetworkReply *reply = get(request); + connect(reply, &QNetworkReply::finished, this, [this, reply, communityId]() { + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto data = reply->readAll(); + const auto json = QJsonDocument::fromJson(data).object(); + + emit communityRoomsRetrieved(communityId, json); + }); +} + void MatrixClient::fetchUserAvatar(const QString &userId, const QUrl &avatarUrl) { diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc index 551895d6..f8989948 100644 --- a/src/RoomInfoListItem.cc +++ b/src/RoomInfoListItem.cc @@ -315,10 +315,7 @@ RoomInfoListItem::clearUnreadMessageCount() void RoomInfoListItem::setPressedState(bool state) { - if (!isPressed_ && state) { - isPressed_ = state; - update(); - } else if (isPressed_ && !state) { + if (isPressed_ != state) { isPressed_ = state; update(); } diff --git a/src/RoomList.cc b/src/RoomList.cc index 0274cefe..30be6cf6 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -47,7 +47,7 @@ RoomList::RoomList(QSharedPointer client, scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); + scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); scrollAreaContents_ = new QWidget(this); @@ -181,6 +181,8 @@ RoomList::setInitialRooms(const QMap> &set if (rooms_.isEmpty()) return; + setFilterRooms(filterRooms_); + auto first_room = rooms_.first(); first_room->setPressedState(true); @@ -271,6 +273,8 @@ RoomList::highlightSelectedRoom(const QString &room_id) qobject_cast(it.value().data())); } } + + selectedRoom_ = room_id; } void @@ -373,6 +377,46 @@ RoomList::closeLeaveRoomDialog(bool leaving, const QString &room_id) } } +void +RoomList::setFilterRooms(bool filterRooms) +{ + filterRooms_ = filterRooms; + + for (int i = 0; i < contentsLayout_->count(); i++) { + // If roomFilter_ contains the room for the current RoomInfoListItem, + // show the list item, otherwise hide it + RoomInfoListItem *listitem = + (RoomInfoListItem *)contentsLayout_->itemAt(i)->widget(); + + if (listitem != nullptr) { + if (!filterRooms) { + contentsLayout_->itemAt(i)->widget()->show(); + } else if (roomFilter_.contains(listitem->roomId())) { + contentsLayout_->itemAt(i)->widget()->show(); + } else { + contentsLayout_->itemAt(i)->widget()->hide(); + } + } + } + + if (filterRooms_ && !roomFilter_.contains(selectedRoom_)) { + RoomInfoListItem *firstVisibleRoom = nullptr; + for (int i = 0; i < contentsLayout_->count(); i++) { + QWidget *item = contentsLayout_->itemAt(i)->widget(); + if (item != nullptr && item->isVisible()) { + firstVisibleRoom = (RoomInfoListItem *)item; + break; + } + } + if (firstVisibleRoom != nullptr) { + highlightSelectedRoom(firstVisibleRoom->roomId()); + } + } else { + scrollArea_->ensureWidgetVisible( + qobject_cast(rooms_.value(selectedRoom_).data())); + } +} + void RoomList::paintEvent(QPaintEvent *) { @@ -393,6 +437,13 @@ RoomList::syncInvites(const std::map & } } +void +RoomList::setRoomFilter(QList room_ids) +{ + roomFilter_ = room_ids; + setFilterRooms(true); +} + void RoomList::addInvitedRoom(const QString &room_id, const mtx::responses::InvitedRoom &room) {