diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 87a27517..bb8deda6 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -2,13 +2,15 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.9 +import QtQuick 2.13 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.3 import im.nheko 1.0 Page { ListView { + id: roomlist + anchors.left: parent.left anchors.right: parent.right height: parent.height @@ -20,26 +22,80 @@ Page { enabled: !Settings.mobileMode } + Connections { + onActiveTimelineChanged: { + roomlist.positionViewAtIndex(Rooms.roomidToIndex(TimelineManager.timeline.roomId()), ListView.Contain); + console.log("Test" + TimelineManager.timeline.roomId() + " " + Rooms.roomidToIndex(TimelineManager.timeline.roomId)); + } + target: TimelineManager + } + delegate: Rectangle { - color: Nheko.colors.window - height: fontMetrics.lineSpacing * 2.5 + Nheko.paddingMedium * 2 + id: roomItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: Math.ceil(fontMetrics.lineSpacing * 2.3 + Nheko.paddingMedium * 2) width: ListView.view.width + state: "normal" + states: [ + State { + name: "highlight" + when: hovered.hovered + + PropertyChanges { + target: roomItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId() + + PropertyChanges { + target: roomItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + HoverHandler { + id: hovered + } + + TapHandler { + onSingleTapped: TimelineManager.setHistoryView(model.roomId) + } RowLayout { - //id: userInfoGrid - spacing: Nheko.paddingMedium anchors.fill: parent anchors.margins: Nheko.paddingMedium Avatar { + // In the future we could show an online indicator by setting the userid for the avatar //userid: Nheko.currentUser.userid id: avatar Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2.5 - Layout.preferredHeight: fontMetrics.lineSpacing * 2.5 + height: Math.ceil(fontMetrics.lineSpacing * 2.3) + width: Math.ceil(fontMetrics.lineSpacing * 2.3) url: model.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: model.roomName } @@ -52,7 +108,7 @@ Page { Layout.minimumWidth: 100 width: parent.width - avatar.width Layout.preferredWidth: parent.width - avatar.width - spacing: 0 + spacing: Nheko.paddingSmall RowLayout { Layout.fillWidth: true @@ -60,9 +116,9 @@ Page { ElidedLabel { Layout.alignment: Qt.AlignBottom - color: Nheko.colors.text + color: roomItem.importantText elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium - fullText: model.roomName + ": " + model.notificationCount + fullText: model.roomName } Item { @@ -74,8 +130,8 @@ Page { Layout.alignment: Qt.AlignRight | Qt.AlignBottom font.pixelSize: fontMetrics.font.pixelSize * 0.9 - color: Nheko.colors.buttonText - text: "14:32" + color: roomItem.unimportantText + text: model.timestamp } } @@ -85,10 +141,10 @@ Page { spacing: 0 ElidedLabel { - color: Nheko.colors.buttonText + color: roomItem.unimportantText font.weight: Font.Thin font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: textContent.width - notificationBubble.width + elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall fullText: model.lastMessage } @@ -99,19 +155,24 @@ Page { Rectangle { id: notificationBubble + visible: model.notificationCount > 0 Layout.alignment: Qt.AlignRight - height: fontMetrics.font.pixelSize * 1.3 + height: fontMetrics.averageCharacterWidth * 3 width: height radius: height / 2 - color: Nheko.colors.highlight + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground Label { - anchors.fill: parent + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter fontSizeMode: Text.Fit - color: Nheko.colors.highlightedText - text: model.notificationCount + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.notificationCount } } 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/ChatPage.cpp b/src/ChatPage.cpp index c5199ff1..58b76174 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -233,11 +233,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) room_list_, &RoomList::updateRoomDescription); - connect(room_list_, - SIGNAL(totalUnreadMessageCountUpdated(int)), - this, - SIGNAL(unreadMessages(int))); - connect( this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 8a807e71..5c41a7a1 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -305,8 +305,6 @@ 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; } @@ -320,9 +318,6 @@ 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; } diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 6a1fc3c5..5fc4dc65 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -4,6 +4,7 @@ #include "RoomlistModel.h" +#include "Cache_p.h" #include "ChatPage.h" #include "MatrixClient.h" #include "MxcImageProvider.h" @@ -26,6 +27,11 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) } } }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); } QHash @@ -34,8 +40,11 @@ RoomlistModel::roleNames() const return { {AvatarUrl, "avatarUrl"}, {RoomName, "roomName"}, + {RoomId, "roomId"}, {LastMessage, "lastMessage"}, + {Timestamp, "timestamp"}, {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, {NotificationCount, "notificationCount"}, }; } @@ -44,18 +53,26 @@ QVariant RoomlistModel::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) { - auto room = models.value(roomids.at(index.row())); + auto roomid = roomids.at(index.row()); + auto room = models.value(roomid); switch (role) { case Roles::AvatarUrl: return room->roomAvatarUrl(); case Roles::RoomName: return room->roomName(); + case Roles::RoomId: + return room->roomId(); case Roles::LastMessage: - return QString("Nico: Hahaha, this is funny!"); + return room->lastMessage().body; + case Roles::Timestamp: + return room->lastMessage().descriptiveTime; case Roles::HasUnreadMessages: - return true; + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); case Roles::NotificationCount: - return 5; + return room->notificationCount(); default: return {}; } @@ -64,10 +81,38 @@ RoomlistModel::data(const QModelIndex &index, int role) const } } +void +RoomlistModel::updateReadStatus(const std::map roomReadStatus_) +{ + std::vector roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); + } + } + + this->roomReadStatus = roomReadStatus_; + + 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 newRoom(new TimelineModel(manager, room_id)); newRoom->setDecryptDescription( ChatPage::instance()->userSettings()->decryptSidebar()); @@ -80,6 +125,56 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) &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, + }); + }); + 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, + }); + + 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(); if (!suppressInsertNotification) beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); @@ -97,8 +192,8 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms) // 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->syncState(room.state); - room_model->addEvents(room.timeline); + room_model->sync(room); + // room_model->addEvents(room.timeline); connect(room_model.data(), &TimelineModel::newCallEvent, manager->callManager(), diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 44fcf032..c4c9d9ba 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -22,8 +22,11 @@ public: { AvatarUrl = Qt::UserRole, RoomName, + RoomId, LastMessage, + Timestamp, HasUnreadMessages, + HasLoudNotification, NotificationCount, }; @@ -47,6 +50,21 @@ public slots: void initializeRooms(const std::vector &roomids); 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; + } + +private slots: + void updateReadStatus(const std::map roomReadStatus_); + +signals: + void totalUnreadMessageCountUpdated(int unreadMessages); private: void addRoom(const QString &room_id, bool suppressInsertNotification = false); @@ -54,5 +72,5 @@ private: TimelineViewManager *manager = nullptr; std::vector roomids; QHash> models; + std::map roomReadStatus; }; - diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 8df17457..19c3fb30 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -723,6 +723,20 @@ TimelineModel::fetchMore(const QModelIndex &) events.fetchMore(); } +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) { @@ -866,14 +880,18 @@ 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 manager_->updateRoomsLastMessage(room_id_, lastMessage_); + emit lastMessageChanged(); + } return; } if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) @@ -884,7 +902,11 @@ 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 manager_->updateRoomsLastMessage(room_id_, description); + emit lastMessageChanged(); + } return; } } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 92fccd2d..5c1065cb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -14,6 +14,7 @@ #include #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 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); } @@ -309,6 +313,9 @@ public slots: 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 +335,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 +382,11 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; + DescInfo lastMessage_; + friend struct SendMessageVisitor; + + int notification_count = 0, highlight_count = 0; }; template diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp index ca2a4ce0..b6c9579a 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp @@ -16,14 +16,15 @@ Theme::paletteFromTheme(std::string_view theme) /*windowText*/ QColor("#333"), /*button*/ QColor("white"), /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(110, 110, 110), + /*dark*/ QColor(70, 77, 93), /*mid*/ QColor(220, 220, 220), /*text*/ QColor("#333"), - /*bright_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")); @@ -34,14 +35,15 @@ Theme::paletteFromTheme(std::string_view theme) /*windowText*/ QColor("#caccd1"), /*button*/ QColor(0xff, 0xff, 0xff), /*light*/ QColor("#caccd1"), - /*dark*/ QColor(110, 110, 110), + /*dark*/ QColor(60, 70, 77), /*mid*/ QColor("#202228"), /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(0xff, 0xff, 0xff), + /*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")); @@ -58,9 +60,12 @@ Theme::Theme(std::string_view theme) separator_ = p.mid().color(); if (theme == "light") { sidebarBackground_ = QColor("#233649"); + red_ = QColor("#a82353"); } else if (theme == "dark") { sidebarBackground_ = QColor("#2d3139"); + red_ = QColor("#a82353"); } else { sidebarBackground_ = p.window().color(); + red_ = QColor("red"); } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h index 64bc8273..834571c0 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -66,6 +66,7 @@ class Theme : public QPalette Q_GADGET Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) Q_PROPERTY(QColor separator READ separator CONSTANT) + Q_PROPERTY(QColor red READ red CONSTANT) public: Theme() {} explicit Theme(std::string_view theme); @@ -73,7 +74,8 @@ public: QColor sidebarBackground() const { return sidebarBackground_; } QColor separator() const { return separator_; } + QColor red() const { return red_; } private: - QColor sidebarBackground_, separator_; + QColor sidebarBackground_, separator_, red_; };