summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-07-08 21:15:50 -0400
committerJoseph Donofry <joedonofry@gmail.com>2021-07-08 21:15:50 -0400
commit1d204ce94c8b678442cccde87a9d8a670b30fe18 (patch)
treed8ec34fd579307dc35f33fc6780fdc923cae090e /src/timeline
parentAdd nheko logo spinner to relevant places in UI (diff)
parentFix cmake template define issue (diff)
downloadnheko-1d204ce94c8b678442cccde87a9d8a670b30fe18.tar.xz
Merge remote-tracking branch 'origin/master' into nheko_loading_spinner
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/CommunitiesModel.cpp118
-rw-r--r--src/timeline/CommunitiesModel.h24
-rw-r--r--src/timeline/EventStore.cpp62
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/InputBar.cpp2
-rw-r--r--src/timeline/RoomlistModel.cpp364
-rw-r--r--src/timeline/RoomlistModel.h35
-rw-r--r--src/timeline/TimelineModel.cpp49
-rw-r--r--src/timeline/TimelineModel.h12
-rw-r--r--src/timeline/TimelineViewManager.cpp36
10 files changed, 644 insertions, 60 deletions
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp

index cedaacce..97bfa76d 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp
@@ -21,6 +21,8 @@ CommunitiesModel::roleNames() const {DisplayName, "displayName"}, {Tooltip, "tooltip"}, {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, }; } @@ -37,11 +39,28 @@ CommunitiesModel::data(const QModelIndex &index, int role) const 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); + } else if (index.row() - 1 < spaceOrder_.size()) { + auto id = spaceOrder_.at(index.row() - 1); + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString::fromStdString(spaces_.at(id).avatar_url); + case CommunitiesModel::Roles::DisplayName: + case CommunitiesModel::Roles::Tooltip: + return QString::fromStdString(spaces_.at(id).name); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("space:" + id); + case CommunitiesModel::Roles::Id: + return "space:" + id; + } + } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) { + auto tag = tags_.at(index.row() - 1 - spaceOrder_.size()); if (tag == "m.favourite") { switch (role) { case CommunitiesModel::Roles::AvatarUrl: @@ -54,7 +73,7 @@ CommunitiesModel::data(const QModelIndex &index, int role) const } else if (tag == "m.lowpriority") { switch (role) { case CommunitiesModel::Roles::AvatarUrl: - return QString(":/icons/icons/ui/star.png"); + return QString(":/icons/icons/ui/lowprio.png"); case CommunitiesModel::Roles::DisplayName: return tr("Low Priority"); case CommunitiesModel::Roles::Tooltip: @@ -74,15 +93,16 @@ CommunitiesModel::data(const QModelIndex &index, int role) const case CommunitiesModel::Roles::AvatarUrl: return QString(":/icons/icons/ui/tag.png"); case CommunitiesModel::Roles::DisplayName: - return tag.right(2); case CommunitiesModel::Roles::Tooltip: - return tag.right(2); + return tag.mid(2); } } switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); case CommunitiesModel::Roles::ChildrenHidden: - return UserSettings::instance()->hiddenTags().contains("tag:" + tag); + return true; case CommunitiesModel::Roles::Id: return "tag:" + tag; } @@ -93,22 +113,35 @@ CommunitiesModel::data(const QModelIndex &index, int role) const void CommunitiesModel::initializeSidebar() { + beginResetModel(); + tags_.clear(); + spaceOrder_.clear(); + spaces_.clear(); + 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); + std::vector<RoomInfo> tempSpaces; + auto infos = cache::roomInfo(); + for (auto it = infos.begin(); it != infos.end(); it++) { + if (it.value().is_space) { + spaceOrder_.push_back(it.key()); + spaces_[it.key()] = it.value(); + } else { + for (const auto &t : it.value().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 @@ -117,6 +150,7 @@ CommunitiesModel::clear() beginResetModel(); tags_.clear(); endResetModel(); + resetCurrentTagId(); emit tagsChanged(); } @@ -133,6 +167,25 @@ CommunitiesModel::sync(const mtx::responses::Rooms &rooms) mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { tagsUpdated = true; } + for (const auto &e : room.state.events) + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || + std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { + tagsUpdated = true; + } + for (const auto &e : room.timeline.events) + if (std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Child>>(e) || + std::holds_alternative< + mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) { + tagsUpdated = true; + } + } + for (const auto &[roomid, room] : rooms.leave) { + (void)room; + if (spaceOrder_.contains(QString::fromStdString(roomid))) + tagsUpdated = true; } if (tagsUpdated) @@ -143,16 +196,51 @@ void CommunitiesModel::setCurrentTagId(QString tagId) { if (tagId.startsWith("tag:")) { - auto tag = tagId.remove(0, 4); + auto tag = tagId.mid(4); for (const auto &t : tags_) { if (t == tag) { this->currentTagId_ = tagId; - emit currentTagIdChanged(); + emit currentTagIdChanged(currentTagId_); + return; + } + } + } else if (tagId.startsWith("space:")) { + auto tag = tagId.mid(6); + for (const auto &t : spaceOrder_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); return; } } } this->currentTagId_ = ""; - emit currentTagIdChanged(); + 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 + 1 + spaceOrder_.size()), + index(idx + 1 + spaceOrder_.size()), + {Hidden}); + } else if (tagId.startsWith("space:")) { + auto idx = spaceOrder_.indexOf(tagId.mid(6)); + if (idx != -1) + emit dataChanged(index(idx + 1), index(idx + 1), {Hidden}); + } + + emit hiddenTagsChanged(); } diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 3f6a2a4c..677581dc 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h
@@ -11,12 +11,15 @@ #include <mtx/responses/sync.hpp> +#include "CacheStructs.h" + 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) + Q_PROPERTY(QStringList tagsWithDefault READ tagsWithDefault NOTIFY tagsChanged) public: enum Roles @@ -25,6 +28,7 @@ public: DisplayName, Tooltip, ChildrenHidden, + Hidden, Id, }; @@ -33,7 +37,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { (void)parent; - return 1 + tags_.size(); + return 1 + tags_.size() + spaceOrder_.size(); } QVariant data(const QModelIndex &index, int role) const override; @@ -46,15 +50,29 @@ public slots: void resetCurrentTagId() { currentTagId_.clear(); - emit currentTagIdChanged(); + emit currentTagIdChanged(currentTagId_); } QStringList tags() const { return tags_; } + QStringList tagsWithDefault() const + { + QStringList tagsWD = tags_; + tagsWD.prepend("m.lowpriority"); + tagsWD.prepend("m.favourite"); + tagsWD.removeOne("m.server_notice"); + tagsWD.removeDuplicates(); + return tagsWD; + } + void toggleTagId(QString tagId); signals: - void currentTagIdChanged(); + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); void tagsChanged(); private: QStringList tags_; QString currentTagId_; + QStringList hiddentTagIds_; + QStringList spaceOrder_; + std::map<QString, RoomInfo> spaces_; }; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 4a9f0fff..9a91ff79 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -185,6 +185,48 @@ EventStore::EventStore(std::string room_id, QObject *) [this](std::string txn_id, std::string event_id) { nhlog::ui()->debug("sent {}", txn_id); + // Replace the event_id in pending edits/replies/redactions with the actual + // event_id of this event. This allows one to edit and reply to events that are + // currently pending. + + // FIXME (introduced by balsoft): this doesn't work for encrypted events, but + // allegedly it's hard to fix so I'll leave my first contribution at that + for (auto related_event_id : cache::client()->relatedEvents(room_id_, txn_id)) { + if (cache::client()->getEvent(room_id_, related_event_id)) { + auto related_event = + cache::client()->getEvent(room_id_, related_event_id).value(); + auto relations = mtx::accessors::relations(related_event.data); + + // Replace the blockquote in fallback reply + auto related_text = + std::get_if<mtx::events::RoomEvent<mtx::events::msg::Text>>( + &related_event.data); + if (related_text && relations.reply_to() == txn_id) { + size_t index = + related_text->content.formatted_body.find(txn_id); + if (index != std::string::npos) { + related_text->content.formatted_body.replace( + index, event_id.length(), event_id); + } + } + + for (mtx::common::Relation &rel : relations.relations) { + if (rel.event_id == txn_id) + rel.event_id = event_id; + } + + mtx::accessors::set_relations(related_event.data, relations); + + cache::client()->replaceEvent( + room_id_, related_event_id, related_event); + + auto idx = idToIndex(related_event_id); + + events_by_id_.remove({room_id_, related_event_id}); + events_.remove({room_id_, toInternalIdx(*idx)}); + } + } + http::client()->read_event( room_id_, event_id, [this, event_id](mtx::http::RequestErr err) { if (err) { @@ -193,6 +235,11 @@ EventStore::EventStore(std::string room_id, QObject *) } }); + auto idx = idToIndex(event_id); + + if (idx) + emit dataChanged(*idx, *idx); + cache::client()->removePendingStatus(room_id_, txn_id); this->current_txn = ""; this->current_txn_error_count = 0; @@ -628,6 +675,9 @@ EventStore::decryptEvent(const IdIndex &idx, index.room_id, index.session_id, e.sender); + // we may not want to request keys during initial sync and such + if (suppressKeyRequests) + break; // TODO: Check if this actually works and look in key backup auto copy = e; copy.room_id = room_id_; @@ -769,6 +819,18 @@ EventStore::decryptEvent(const IdIndex &idx, return asCacheEntry(std::move(decryptionResult.event.value())); } +void +EventStore::enableKeyRequests(bool suppressKeyRequests_) +{ + if (!suppressKeyRequests_) { + for (const auto &key : decryptedEvents_.keys()) + if (key.room == this->room_id_) + decryptedEvents_.remove(key); + suppressKeyRequests = false; + } else + suppressKeyRequests = true; +} + mtx::events::collections::TimelineEvents * EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index d9bb86cb..7c404102 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -115,6 +115,7 @@ public slots: void addPending(mtx::events::collections::TimelineEvents event); void receivedSessionKey(const std::string &session_id); void clearTimeline(); + void enableKeyRequests(bool suppressKeyRequests_); private: std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id); @@ -142,4 +143,5 @@ private: std::string current_txn; int current_txn_error_count = 0; bool noMoreMessages = false; + bool suppressKeyRequests = true; }; diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index c309daab..b0747a7c 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -296,7 +296,7 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify) firstLine = false; body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); } else { - body = QString("%1\n> %2\n").arg(body).arg(line); + body += QString("> %1\n").arg(line); } } diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 4dd44b30..87940948 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp
@@ -33,6 +33,27 @@ RoomlistModel::RoomlistModel(TimelineViewManager *parent) &RoomlistModel::totalUnreadMessageCountUpdated, ChatPage::instance(), &ChatPage::unreadMessages); + + connect( + this, + &RoomlistModel::fetchedPreview, + this, + [this](QString roomid, RoomInfo info) { + if (this->previewedRooms.contains(roomid)) { + this->previewedRooms.insert(roomid, std::move(info)); + auto idx = this->roomidToIndex(roomid); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + Roles::AvatarUrl, + Roles::IsSpace, + Roles::IsPreviewFetched, + Qt::DisplayRole, + }); + } + }, + Qt::QueuedConnection); } QHash<int, QByteArray> @@ -51,6 +72,7 @@ RoomlistModel::roleNames() const {IsInvite, "isInvite"}, {IsSpace, "isSpace"}, {Tags, "tags"}, + {ParentSpaces, "parentSpaces"}, }; } @@ -60,6 +82,16 @@ 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 (role == Roles::ParentSpaces) { + auto parents = cache::client()->getParentRoomIds(roomid.toStdString()); + QStringList list; + for (const auto &t : parents) + list.push_back(QString::fromStdString(t)); + return list; + } else if (role == Roles::RoomId) { + return roomid; + } + if (models.contains(roomid)) { auto room = models.value(roomid); switch (role) { @@ -67,8 +99,6 @@ RoomlistModel::data(const QModelIndex &index, int role) const 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: @@ -84,7 +114,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const case Roles::NotificationCount: return room->notificationCount(); case Roles::IsInvite: + return false; case Roles::IsSpace: + return room->isSpace(); + case Roles::IsPreview: return false; case Roles::Tags: { auto info = cache::singleRoomInfo(roomid.toStdString()); @@ -103,14 +136,12 @@ RoomlistModel::data(const QModelIndex &index, int role) const 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; + return QString(); case Roles::Time: - return room.msgInfo.descriptiveTime; + return QString(); case Roles::Timestamp: - return QVariant(static_cast<quint64>(room.msgInfo.timestamp)); + return QVariant(static_cast<quint64>(0)); case Roles::HasUnreadMessages: case Roles::HasLoudNotification: return false; @@ -120,13 +151,77 @@ RoomlistModel::data(const QModelIndex &index, int role) const return true; case Roles::IsSpace: return false; + case Roles::IsPreview: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } + } else if (previewedRooms.contains(roomid) && + previewedRooms.value(roomid).has_value()) { + auto room = previewedRooms.value(roomid).value(); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::LastMessage: + return tr("Previewing this room"); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return room.is_space; + case Roles::IsPreview: + return true; + case Roles::IsPreviewFetched: + return true; case Roles::Tags: return QStringList(); default: return {}; } } else { - return {}; + if (role == Roles::IsPreview) + return true; + else if (role == Roles::IsPreviewFetched) + return false; + + fetchPreview(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString(); + case Roles::RoomName: + return tr("No preview available"); + case Roles::LastMessage: + return QString(); + case Roles::Time: + return QString(); + case Roles::Timestamp: + return QVariant(static_cast<quint64>(0)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return false; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } } } else { return {}; @@ -230,26 +325,112 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) newRoom->updateLastMessage(); - bool wasInvite = invites.contains(room_id); - if (!suppressInsertNotification && !wasInvite) - beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + std::vector<QString> previewsToAdd; + if (newRoom->isSpace()) { + auto childs = cache::client()->getChildRoomIds(room_id.toStdString()); + for (const auto &c : childs) { + auto id = QString::fromStdString(c); + if (!(models.contains(id) || invites.contains(id) || + previewedRooms.contains(id))) { + previewsToAdd.push_back(std::move(id)); + } + } + } - models.insert(room_id, std::move(newRoom)); + bool wasInvite = invites.contains(room_id); + bool wasPreview = previewedRooms.contains(room_id); + if (!suppressInsertNotification && + ((!wasInvite && !wasPreview) || !previewedRooms.empty())) + // if the old room was already in the list, don't add it. Also add all + // previews at the same time. + beginInsertRows(QModelIndex(), + (int)roomids.size(), + (int)(roomids.size() + previewsToAdd.size() - + ((wasInvite || wasPreview) ? 0 : 1))); + 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 if (wasPreview) { + auto idx = roomidToIndex(room_id); + previewedRooms.remove(room_id); + emit dataChanged(index(idx), index(idx)); } else { roomids.push_back(room_id); } + for (auto p : previewsToAdd) { + previewedRooms.insert(p, std::nullopt); + roomids.push_back(std::move(p)); + } + if (!suppressInsertNotification && !wasInvite) endInsertRows(); } } void +RoomlistModel::fetchPreview(QString roomid_) const +{ + std::string roomid = roomid_.toStdString(); + http::client()->get_state_event<mtx::events::state::Create>( + roomid, + "", + [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) { + bool is_space = false; + if (!err) { + is_space = c.type == mtx::events::state::room_type::space; + } + + http::client()->get_state_event<mtx::events::state::Avatar>( + roomid, + "", + [this, roomid, is_space](const mtx::events::state::Avatar &a, + mtx::http::RequestErr) { + auto avatar_url = a.url; + + http::client()->get_state_event<mtx::events::state::Topic>( + roomid, + "", + [this, roomid, avatar_url, is_space]( + const mtx::events::state::Topic &t, mtx::http::RequestErr) { + auto topic = t.topic; + http::client()->get_state_event<mtx::events::state::Name>( + roomid, + "", + [this, roomid, topic, avatar_url, is_space]( + const mtx::events::state::Name &n, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "Failed to fetch name event to " + "create preview for {}", + roomid); + } + + // don't even add a preview, if we got not a single + // response + if (n.name.empty() && avatar_url.empty() && + topic.empty()) + return; + + RoomInfo info{}; + info.name = n.name; + info.is_space = is_space; + info.avatar_url = avatar_url; + info.topic = topic; + + const_cast<RoomlistModel *>(this)->fetchedPreview( + QString::fromStdString(roomid), info); + }); + }); + }); + }); +} + +void RoomlistModel::sync(const mtx::responses::Rooms &rooms) { for (const auto &[room_id, room] : rooms.join) { @@ -324,6 +505,7 @@ RoomlistModel::initializeRooms() models.clear(); roomids.clear(); invites.clear(); + currentRoom_ = nullptr; invites = cache::client()->invites(); for (const auto &id : invites.keys()) @@ -407,11 +589,15 @@ RoomlistModel::setCurrentRoom(QString roomid) namespace { enum NotificationImportance : short { - ImportanceDisabled = -1, + ImportanceDisabled = -3, + NoPreview = -2, + Preview = -1, AllEventsRead = 0, NewMessage = 1, NewMentions = 2, - Invite = 3 + Invite = 3, + SubSpace = 4, + CurrentSpace = 5, }; } @@ -421,7 +607,18 @@ 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()) { + if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) { + if (filterType == FilterBy::Space && + filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString()) + return CurrentSpace; + else + return SubSpace; + } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) { + if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool()) + return Preview; + else + return NoPreview; + } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { return Invite; } else if (!this->sortByImportance) { return ImportanceDisabled; @@ -433,6 +630,7 @@ FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const return AllEventsRead; } } + bool FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { @@ -486,6 +684,140 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare } 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 (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) + .toBool()) { + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } + + 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; + } + + if (!hiddenSpaces.empty()) { + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Tag) { + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview) + .toBool()) { + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool()) { + return false; + } + + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + if (!tags.contains(filterStr)) + return false; + + if (!hiddenTags.empty()) { + for (const auto &t : tags) + if (t != filterStr && hiddenTags.contains(t)) + return false; + } + + if (!hiddenSpaces.empty()) { + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Space) { + if (filterStr == sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId) + .toString()) + return true; + + auto parents = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces) + .toStringList(); + + if (!parents.contains(filterStr)) + return false; + + 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; + } + + if (!hiddenSpaces.empty()) { + for (const auto &t : parents) + if (hiddenSpaces.contains(t)) + return false; + } + + if (sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace) + .toBool() && + !parents.contains(filterStr)) { + return false; + } + + return true; + } else { + return true; + } +} + +void FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) { if (on) { @@ -532,7 +864,7 @@ FilteredRoomlistModel::previousRoom() if (r) { int idx = roomidToIndex(r->roomId()); idx--; - if (idx > 0) { + if (idx >= 0) { setCurrentRoom( data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 7ee0419f..2005c35e 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h
@@ -37,7 +37,10 @@ public: NotificationCount, IsInvite, IsSpace, + IsPreview, + IsPreviewFetched, Tags, + ParentSpaces, }; RoomlistModel(TimelineViewManager *parent = nullptr); @@ -86,15 +89,18 @@ private slots: signals: void totalUnreadMessageCountUpdated(int unreadMessages); void currentRoomChanged(); + void fetchedPreview(QString roomid, RoomInfo info); private: void addRoom(const QString &room_id, bool suppressInsertNotification = false); + void fetchPreview(QString roomid) const; TimelineViewManager *manager = nullptr; std::vector<QString> roomids; QHash<QString, RoomInfo> invites; QHash<QString, QSharedPointer<TimelineModel>> models; std::map<QString, bool> roomReadStatus; + QHash<QString, std::optional<RoomInfo>> previewedRooms; QSharedPointer<TimelineModel> currentRoom_; @@ -109,6 +115,7 @@ class FilteredRoomlistModel : public QSortFilterProxyModel 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) @@ -128,6 +135,24 @@ public slots: void nextRoom(); void previousRoom(); + void updateFilterTag(QString tagId) + { + if (tagId.startsWith("tag:")) { + filterType = FilterBy::Tag; + filterStr = tagId.mid(4); + } else if (tagId.startsWith("space:")) { + filterType = FilterBy::Space; + filterStr = tagId.mid(6); + } else { + filterType = FilterBy::Nothing; + filterStr.clear(); + } + + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); + signals: void currentRoomChanged(); @@ -135,4 +160,14 @@ 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 f29f929e..caa40353 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -320,6 +320,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj { lastMessage_.timestamp = 0; + if (auto create = + cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString())) + this->isSpace_ = create->content.type == mtx::events::state::room_type::space; + connect( this, &TimelineModel::redactionFailed, @@ -376,6 +380,27 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj this->updateFlowEventId(event_id); }); + // When a message is sent, check if the current edit/reply relates to that message, + // and update the event_id so that it points to the sent message and not the pending one. + connect(&events, + &EventStore::messageSent, + this, + [this](std::string txn_id, std::string event_id) { + if (edit_.toStdString() == txn_id) { + edit_ = QString::fromStdString(event_id); + emit editChanged(edit_); + } + if (reply_.toStdString() == txn_id) { + reply_ = QString::fromStdString(event_id); + emit replyChanged(reply_); + } + }); + + connect(manager_, + &TimelineViewManager::initialSyncChanged, + &events, + &EventStore::enableKeyRequests); + showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); } @@ -507,6 +532,10 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r const static QRegularExpression matchImgUri( "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)"); formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3"); + // Same regex but for single quotes around the src + const static QRegularExpression matchImgUri2( + "(<img [^>]*)src=\'mxc://([^\']*)\'([^>]*>)"); + formattedBody_.replace(matchImgUri2, "\\1 src=\"image://mxcImage/\\2\"\\3"); const static QRegularExpression matchEmoticonHeight( "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)"); formattedBody_.replace(matchEmoticonHeight, @@ -568,10 +597,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case IsEdited: return QVariant(relations(event).replaces().has_value()); case IsEditable: - return QVariant(!is_state_event(event) && - mtx::accessors::sender(event) == - http::client()->user_id().to_string() && - !event_id(event).empty() && event_id(event).front() == '$'); + return QVariant(!is_state_event(event) && mtx::accessors::sender(event) == + http::client()->user_id().to_string()); case IsEncrypted: { auto id = event_id(event); auto encrypted_event = events.get(id, "", false); @@ -757,6 +784,7 @@ TimelineModel::syncState(const mtx::responses::State &s) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } } @@ -813,6 +841,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) } else if (std::holds_alternative<StateEvent<state::Member>>(e)) { emit roomAvatarUrlChanged(); emit roomNameChanged(); + emit roomMemberCountChanged(); } } updateLastMessage(); @@ -1787,7 +1816,8 @@ TimelineModel::formatMemberEvent(QString id) } if (event->content.reason != "") { - rendered += tr(" Reason: %1").arg(QString::fromStdString(event->content.reason)); + rendered += + " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); } return rendered; @@ -1796,9 +1826,6 @@ TimelineModel::formatMemberEvent(QString id) void TimelineModel::setEdit(QString newEdit) { - if (edit_.startsWith('m')) - return; - if (newEdit.isEmpty()) { resetEdit(); return; @@ -1921,3 +1948,9 @@ TimelineModel::roomTopic() const return utils::replaceEmoji(utils::linkifyMessage( QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); } + +int +TimelineModel::roomMemberCount() const +{ + return (int)cache::client()->memberCount(room_id_.toStdString()); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3ebbe120..3392d474 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -161,6 +161,8 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged) Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged) Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged) + Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) + Q_PROPERTY(bool isSpace READ isSpace CONSTANT) Q_PROPERTY(InputBar *input READ input CONSTANT) Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) @@ -262,6 +264,8 @@ public: RelatedInfo relatedInfo(QString id); DescInfo lastMessage() const { return lastMessage_; } + bool isSpace() const { return isSpace_; } + int roomMemberCount() const; public slots: void setCurrentIndex(int index); @@ -348,6 +352,7 @@ signals: void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); + void roomMemberCountChanged(); void permissionsChanged(); void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); @@ -366,9 +371,6 @@ private: QString room_id_; - bool decryptDescription = true; - bool m_paginationInProgress = false; - QString currentId, currentReadId; QString reply_, edit_; QString textBeforeEdit, replyBeforeEdit; @@ -388,6 +390,10 @@ private: friend struct SendMessageVisitor; int notification_count = 0, highlight_count = 0; + + bool decryptDescription = true; + bool m_paginationInProgress = false; + bool isSpace_ = false; }; template<class T> diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index faf56b85..a45294d1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -107,8 +107,7 @@ QColor TimelineViewManager::userColor(QString id, QColor background) { if (!userColors.contains(id)) - userColors.insert( - id, QColor(utils::generateContrastingHexColor(id, background.name()))); + userColors.insert(id, QColor(utils::generateContrastingHexColor(id, background))); return userColors.value(id); } @@ -195,7 +194,17 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par }); qmlRegisterSingletonType<RoomlistModel>( "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { - return new FilteredRoomlistModel(self->rooms_); + 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 * { @@ -386,18 +395,17 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) imgDialog->showFullScreen(); auto room = rooms_->currentRoom(); - connect( - imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() { - // hide the overlay while presenting the save dialog for better - // cross platform support. - imgDialog->hide(); + 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 (!room->saveMedia(eventId)) { - imgDialog->show(); - } else { - imgDialog->close(); - } - }); + if (!room->saveMedia(eventId)) { + imgDialog->show(); + } else { + imgDialog->close(); + } + }); } void