diff --git a/include/ChatPage.h b/include/ChatPage.h index 12eaf6b7..68495276 100644 --- a/include/ChatPage.h +++ b/include/ChatPage.h @@ -59,6 +59,7 @@ private slots: void logout(); private: + void updateDisplayNames(const RoomState &state); void updateRoomState(RoomState &room_state, const QJsonArray &events); QHBoxLayout *topLayout_; diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h index 75a90ff3..17c75fc3 100644 --- a/include/RoomInfoListItem.h +++ b/include/RoomInfoListItem.h @@ -50,8 +50,8 @@ protected: void paintEvent(QPaintEvent *event) override; private: - const int Padding = 10; - const int IconSize = 45; + const int Padding = 7; + const int IconSize = 46; RippleOverlay *ripple_overlay_; @@ -66,7 +66,7 @@ private: bool isPressed_ = false; - int maxHeight_ = 60; + int maxHeight_; int unreadMsgCount_ = 0; }; @@ -87,5 +87,5 @@ inline RoomState RoomInfoListItem::state() const inline void RoomInfoListItem::setAvatar(const QImage &img) { - roomAvatar_ = QPixmap::fromImage(img.scaled(IconSize, IconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + roomAvatar_ = QPixmap::fromImage(img.scaled(IconSize, IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } diff --git a/include/RoomState.h b/include/RoomState.h index 788940e2..0389a6df 100644 --- a/include/RoomState.h +++ b/include/RoomState.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include "AliasesEventContent.h" #include "AvatarEventContent.h" @@ -25,6 +26,7 @@ #include "CreateEventContent.h" #include "HistoryVisibilityEventContent.h" #include "JoinRulesEventContent.h" +#include "MemberEventContent.h" #include "NameEventContent.h" #include "PowerLevelsEventContent.h" #include "TopicEventContent.h" @@ -38,11 +40,20 @@ namespace events = matrix::events; class RoomState { public: - QString resolveName() const; - inline QString resolveTopic() const; + // Calculate room data that are not immediatly accessible. Like room name and avatar. + // + // e.g If the room is 1-on-1 name and avatar should be extracted from a user. + void resolveName(); + void resolveAvatar(); - QPixmap avatar_img_; + inline QUrl getAvatar() const; + inline QString getName() const; + inline QString getTopic() const; + void removeLeaveMemberships(); + void update(const RoomState &state); + + // The latest state events. events::StateEvent aliases; events::StateEvent avatar; events::StateEvent canonical_alias; @@ -52,9 +63,30 @@ public: events::StateEvent name; events::StateEvent power_levels; events::StateEvent topic; + + // Contains the m.room.member events for all the joined users. + QMap> memberships; + +private: + QUrl avatar_; + QString name_; + + // It defines the user whose avatar is used for the room. If the room has an avatar + // event this should be empty. + QString userAvatar_; }; -inline QString RoomState::resolveTopic() const +inline QString RoomState::getTopic() const { return topic.content().topic().simplified(); } + +inline QString RoomState::getName() const +{ + return name_; +} + +inline QUrl RoomState::getAvatar() const +{ + return avatar_; +} diff --git a/include/events/MemberEventContent.h b/include/events/MemberEventContent.h index f3714462..e61d0cda 100644 --- a/include/events/MemberEventContent.h +++ b/include/events/MemberEventContent.h @@ -28,19 +28,19 @@ namespace events { enum class Membership { // The user is banned. - BanState, + Ban, // The user has been invited. - InviteState, + Invite, // The user has joined. - JoinState, + Join, // The user has requested to join. - KnockState, + Knock, // The user has left. - LeaveState, + Leave, }; /* diff --git a/src/ChatPage.cc b/src/ChatPage.cc index b5012818..d318d086 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc @@ -220,6 +220,17 @@ void ChatPage::syncFailed(const QString &msg) sync_timer_->start(sync_interval_ * 5); } +// TODO: Should be moved in another class that manages this global list. +void ChatPage::updateDisplayNames(const RoomState &state) +{ + for (const auto member : state.memberships) { + auto displayName = member.content().displayName(); + + if (!displayName.isEmpty()) + TimelineViewManager::DISPLAY_NAMES.insert(member.stateKey(), displayName); + } +} + void ChatPage::syncCompleted(const SyncResponse &response) { client_->setNextBatchToken(response.nextBatch()); @@ -234,8 +245,16 @@ void ChatPage::syncCompleted(const SyncResponse &response) updateRoomState(room_state, it.value().state().events()); updateRoomState(room_state, it.value().timeline().events()); + updateDisplayNames(room_state); - state_manager_.insert(it.key(), room_state); + if (state_manager_.contains(it.key())) { + // TODO: Use pointers instead of copying. + auto oldState = state_manager_[it.key()]; + oldState.update(room_state); + state_manager_.insert(it.key(), oldState); + } else { + qWarning() << "New rooms cannot be added after initial sync, yet."; + } if (it.key() == current_room_) changeTopRoomInfo(it.key()); @@ -260,6 +279,12 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response) updateRoomState(room_state, it.value().state().events()); updateRoomState(room_state, it.value().timeline().events()); + room_state.removeLeaveMemberships(); + room_state.resolveName(); + room_state.resolveAvatar(); + + updateDisplayNames(room_state); + state_manager_.insert(it.key(), room_state); } @@ -298,13 +323,13 @@ void ChatPage::changeTopRoomInfo(const QString &room_id) auto state = state_manager_[room_id]; - top_bar_->updateRoomName(state.resolveName()); - top_bar_->updateRoomTopic(state.resolveTopic()); + top_bar_->updateRoomName(state.getName()); + top_bar_->updateRoomTopic(state.getTopic()); if (room_avatars_.contains(room_id)) top_bar_->updateRoomAvatar(room_avatars_.value(room_id).toImage()); else - top_bar_->updateRoomAvatarFromName(state.resolveName()); + top_bar_->updateRoomAvatarFromName(state.getName()); current_room_ = room_id; } @@ -383,15 +408,7 @@ void ChatPage::updateRoomState(RoomState &room_state, const QJsonArray &events) events::StateEvent member; member.deserialize(event); - auto display_name = member.content().displayName(); - - if (display_name.isEmpty()) - display_name = member.stateKey(); - - auto current_name = TimelineViewManager::DISPLAY_NAMES.value(member.stateKey()); - - if (current_name.isEmpty() || current_name == member.stateKey()) - TimelineViewManager::DISPLAY_NAMES.insert(member.stateKey(), display_name); + room_state.memberships.insert(member.stateKey(), member); break; } diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc index 9385f927..7753536e 100644 --- a/src/RoomInfoListItem.cc +++ b/src/RoomInfoListItem.cc @@ -29,7 +29,7 @@ RoomInfoListItem::RoomInfoListItem(RoomState state, QString room_id, QWidget *pa , state_(state) , roomId_(room_id) , isPressed_(false) - , maxHeight_(60) + , maxHeight_(IconSize + 2 * Padding) , unreadMsgCount_(0) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); @@ -71,7 +71,7 @@ void RoomInfoListItem::paintEvent(QPaintEvent *event) QRect avatarRegion(Padding, Padding, IconSize, IconSize); // Description line - int bottom_y = avatarRegion.center().y() + metrics.height() / 2 + Padding / 2; + int bottom_y = maxHeight_ - Padding - metrics.height() / 2 + Padding / 2; if (width() > ui::sidebar::SmallSize) { if (isPressed_) { @@ -79,8 +79,8 @@ void RoomInfoListItem::paintEvent(QPaintEvent *event) p.setPen(pen); } - auto name = metrics.elidedText(state_.resolveName(), Qt::ElideRight, (width() - IconSize - 2 * Padding) * 0.8); - p.drawText(QPoint(2 * Padding + IconSize, avatarRegion.center().y() - metrics.height() / 2), name); + auto name = metrics.elidedText(state_.getName(), Qt::ElideRight, (width() - IconSize - 2 * Padding) * 0.8); + p.drawText(QPoint(2 * Padding + IconSize, Padding + metrics.height()), name); if (!isPressed_) { QPen pen(QColor("#5d6565")); @@ -92,7 +92,7 @@ void RoomInfoListItem::paintEvent(QPaintEvent *event) if (unreadMsgCount_ > 0) descPercentage = 0.8; - auto description = metrics.elidedText(state_.resolveTopic(), Qt::ElideRight, width() * descPercentage - 2 * Padding - IconSize); + auto description = metrics.elidedText(state_.getTopic(), Qt::ElideRight, width() * descPercentage - 2 * Padding - IconSize); p.drawText(QPoint(2 * Padding + IconSize, bottom_y), description); } @@ -113,7 +113,7 @@ void RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(font); p.setPen(QColor("#333")); p.setBrush(Qt::NoBrush); - p.drawText(avatarRegion.translated(0, -1), Qt::AlignCenter, QChar(state_.resolveName()[0])); + p.drawText(avatarRegion.translated(0, -1), Qt::AlignCenter, QChar(state_.getName()[0])); } else { p.save(); diff --git a/src/RoomList.cc b/src/RoomList.cc index 3e381340..6d0e185b 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc @@ -100,8 +100,8 @@ void RoomList::setInitialRooms(const QMap &states) auto room_id = it.key(); auto state = it.value(); - if (!state.avatar.content().url().toString().isEmpty()) - client_->fetchRoomAvatar(room_id, state.avatar.content().url()); + if (!state.getAvatar().toString().isEmpty()) + client_->fetchRoomAvatar(room_id, state.getAvatar()); RoomInfoListItem *room_item = new RoomInfoListItem(state, room_id, scrollArea_); connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); @@ -133,8 +133,8 @@ void RoomList::sync(const QMap &states) auto room = rooms_[room_id]; - auto current_avatar = room->state().avatar.content().url(); - auto new_avatar = state.avatar.content().url(); + auto current_avatar = room->state().getAvatar(); + auto new_avatar = state.getAvatar(); if (current_avatar != new_avatar && !new_avatar.toString().isEmpty()) client_->fetchRoomAvatar(room_id, new_avatar); diff --git a/src/RoomState.cc b/src/RoomState.cc index 98f418e3..3eaff452 100644 --- a/src/RoomState.cc +++ b/src/RoomState.cc @@ -15,18 +15,138 @@ * along with this program. If not, see . */ +#include +#include + #include "RoomState.h" -QString RoomState::resolveName() const +namespace events = matrix::events; + +void RoomState::resolveName() { - if (!name.content().name().isEmpty()) - return name.content().name().simplified(); + name_ = "Empty Room"; + userAvatar_.clear(); - if (!canonical_alias.content().alias().isEmpty()) - return canonical_alias.content().alias().simplified(); + if (!name.content().name().isEmpty()) { + name_ = name.content().name().simplified(); + return; + } - if (aliases.content().aliases().size() != 0) - return aliases.content().aliases()[0].simplified(); + if (!canonical_alias.content().alias().isEmpty()) { + name_ = canonical_alias.content().alias().simplified(); + return; + } - return "Unknown Room Name"; + // FIXME: Doesn't follow the spec guidelines. + if (aliases.content().aliases().size() != 0) { + name_ = aliases.content().aliases()[0].simplified(); + return; + } + + QSettings settings; + auto user_id = settings.value("auth/user_id"); + + // TODO: Display names should be sorted alphabetically. + for (const auto membership : memberships) { + if (membership.stateKey() == user_id) + continue; + + if (membership.content().membershipState() == events::Membership::Join) { + userAvatar_ = membership.stateKey(); + + if (membership.content().displayName().isEmpty()) + name_ = membership.stateKey(); + else + name_ = membership.content().displayName(); + + break; + } + } + + // TODO: pluralization + if (memberships.size() > 2) + name_ = QString("%1 and %2 others").arg(name_).arg(memberships.size()); +} + +void RoomState::resolveAvatar() +{ + if (userAvatar_.isEmpty()) { + avatar_ = avatar.content().url(); + return; + } + + if (memberships.contains(userAvatar_)) { + avatar_ = memberships[userAvatar_].content().avatarUrl(); + } else { + qWarning() << "Setting room avatar from unknown user id" << userAvatar_; + } +} + +// Should be used only after initial sync. +void RoomState::removeLeaveMemberships() +{ + for (auto it = memberships.begin(); it != memberships.end();) { + if (it.value().content().membershipState() == events::Membership::Leave) { + it = memberships.erase(it); + } else { + ++it; + } + } +} + +void RoomState::update(const RoomState &state) +{ + bool needsNameCalculation = false; + bool needsAvatarCalculation = false; + + if (aliases.eventId() != state.aliases.eventId()) { + aliases = state.aliases; + } + + if (avatar.eventId() != state.avatar.eventId()) { + avatar = state.avatar; + needsAvatarCalculation = true; + } + + if (canonical_alias.eventId() != state.canonical_alias.eventId()) { + canonical_alias = state.canonical_alias; + needsNameCalculation = true; + } + + if (create.eventId() != state.create.eventId()) + create = state.create; + if (history_visibility.eventId() != state.history_visibility.eventId()) + history_visibility = state.history_visibility; + if (join_rules.eventId() != state.join_rules.eventId()) + join_rules = state.join_rules; + + if (name.eventId() != state.name.eventId()) { + name = state.name; + needsNameCalculation = true; + } + + if (power_levels.eventId() != state.power_levels.eventId()) + power_levels = state.power_levels; + if (topic.eventId() != state.topic.eventId()) + topic = state.topic; + + for (auto it = state.memberships.constBegin(); it != state.memberships.constEnd(); ++it) { + auto membershipState = it.value().content().membershipState(); + + if (it.key() == userAvatar_) { + needsNameCalculation = true; + needsAvatarCalculation = true; + } + + if (membershipState == events::Membership::Leave) + this->memberships.remove(it.key()); + else + this->memberships.insert(it.key(), it.value()); + } + + if (needsNameCalculation) + resolveName(); + + if (needsAvatarCalculation) + resolveAvatar(); } diff --git a/src/events/MemberEventContent.cc b/src/events/MemberEventContent.cc index 4c405f01..4dc8ad5f 100644 --- a/src/events/MemberEventContent.cc +++ b/src/events/MemberEventContent.cc @@ -34,15 +34,15 @@ void MemberEventContent::deserialize(const QJsonValue &data) auto value = object.value("membership").toString(); if (value == "ban") - membership_state_ = Membership::BanState; + membership_state_ = Membership::Ban; else if (value == "invite") - membership_state_ = Membership::InviteState; + membership_state_ = Membership::Invite; else if (value == "join") - membership_state_ = Membership::JoinState; + membership_state_ = Membership::Join; else if (value == "knock") - membership_state_ = Membership::KnockState; + membership_state_ = Membership::Knock; else if (value == "leave") - membership_state_ = Membership::LeaveState; + membership_state_ = Membership::Leave; else throw DeserializationException(QString("Unknown membership value: %1").arg(value).toUtf8().constData());