summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-06-13 01:44:25 +0000
committerGitHub <noreply@github.com>2021-06-13 01:44:25 +0000
commit5b4566d3f959e13b34d34a4156a0646b26c7fd08 (patch)
tree11d1b42e1d9f92977c48670605bb8a2e26ddd43c /src/timeline
parentTranslated using Weblate (Esperanto) (diff)
parentFix button spacing (diff)
downloadnheko-5b4566d3f959e13b34d34a4156a0646b26c7fd08.tar.xz
Merge pull request #605 from Nheko-Reborn/qml-roomlist
Qml roomlist and stuff
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/CommunitiesModel.cpp188
-rw-r--r--src/timeline/CommunitiesModel.h64
-rw-r--r--src/timeline/EventStore.cpp14
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/InputBar.cpp36
-rw-r--r--src/timeline/InputBar.h1
-rw-r--r--src/timeline/RoomlistModel.cpp590
-rw-r--r--src/timeline/RoomlistModel.h164
-rw-r--r--src/timeline/TimelineModel.cpp45
-rw-r--r--src/timeline/TimelineModel.h15
-rw-r--r--src/timeline/TimelineViewManager.cpp326
-rw-r--r--src/timeline/TimelineViewManager.h68
12 files changed, 1210 insertions, 303 deletions
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
new file mode 100644

index 00000000..96a450ea --- /dev/null +++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CommunitiesModel.h" + +#include <set> + +#include "Cache.h" +#include "UserSettingsPage.h" + +CommunitiesModel::CommunitiesModel(QObject *parent) + : QAbstractListModel(parent) +{} + +QHash<int, QByteArray> +CommunitiesModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {DisplayName, "displayName"}, + {Tooltip, "tooltip"}, + {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, + }; +} + +QVariant +CommunitiesModel::data(const QModelIndex &index, int role) const +{ + if (index.row() == 0) { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/world.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("All rooms"); + case CommunitiesModel::Roles::Tooltip: + return tr("Shows all rooms without filtering."); + case CommunitiesModel::Roles::ChildrenHidden: + return false; + case CommunitiesModel::Roles::Hidden: + return false; + case CommunitiesModel::Roles::Id: + return ""; + } + } else if (index.row() - 1 < tags_.size()) { + auto tag = tags_.at(index.row() - 1); + if (tag == "m.favourite") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Favourites"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms you have favourited."); + } + } else if (tag == "m.lowpriority") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Low Priority"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms with low priority."); + } + } else if (tag == "m.server_notice") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Server Notices"); + case CommunitiesModel::Roles::Tooltip: + return tr("Messages from your server or administrator."); + } + } else { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tag.mid(2); + case CommunitiesModel::Roles::Tooltip: + return tag.mid(2); + } + } + + switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Id: + return "tag:" + tag; + } + } + return QVariant(); +} + +void +CommunitiesModel::initializeSidebar() +{ + std::set<std::string> ts; + for (const auto &e : cache::roomInfo()) { + for (const auto &t : e.tags) { + if (t.find("u.") == 0 || t.find("m." == 0)) { + ts.insert(t); + } + } + } + + beginResetModel(); + tags_.clear(); + for (const auto &t : ts) + tags_.push_back(QString::fromStdString(t)); + + hiddentTagIds_ = UserSettings::instance()->hiddenTags(); + endResetModel(); + + emit tagsChanged(); + emit hiddenTagsChanged(); +} + +void +CommunitiesModel::clear() +{ + beginResetModel(); + tags_.clear(); + endResetModel(); + resetCurrentTagId(); + + emit tagsChanged(); +} + +void +CommunitiesModel::sync(const mtx::responses::Rooms &rooms) +{ + bool tagsUpdated = false; + + for (const auto &[roomid, room] : rooms.join) { + (void)roomid; + for (const auto &e : room.account_data.events) + if (std::holds_alternative< + mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { + tagsUpdated = true; + } + } + + if (tagsUpdated) + initializeSidebar(); +} + +void +CommunitiesModel::setCurrentTagId(QString tagId) +{ + if (tagId.startsWith("tag:")) { + auto tag = tagId.mid(4); + for (const auto &t : tags_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } + } + } + + this->currentTagId_ = ""; + emit currentTagIdChanged(currentTagId_); +} + +void +CommunitiesModel::toggleTagId(QString tagId) +{ + if (hiddentTagIds_.contains(tagId)) { + hiddentTagIds_.removeOne(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } else { + hiddentTagIds_.push_back(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } + + if (tagId.startsWith("tag:")) { + auto idx = tags_.indexOf(tagId.mid(4)); + if (idx != -1) + emit dataChanged(index(idx), index(idx), {Hidden}); + } + + emit hiddenTagsChanged(); +} diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h new file mode 100644
index 00000000..c98b5955 --- /dev/null +++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractListModel> +#include <QHash> +#include <QString> +#include <QStringList> + +#include <mtx/responses/sync.hpp> + +class CommunitiesModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY + currentTagIdChanged RESET resetCurrentTagId) + Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) + +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + Tooltip, + ChildrenHidden, + Hidden, + Id, + }; + + CommunitiesModel(QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return 1 + tags_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void initializeSidebar(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + QString currentTagId() const { return currentTagId_; } + void setCurrentTagId(QString tagId); + void resetCurrentTagId() + { + currentTagId_.clear(); + emit currentTagIdChanged(currentTagId_); + } + QStringList tags() const { return tags_; } + void toggleTagId(QString tagId); + +signals: + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); + void tagsChanged(); + +private: + QStringList tags_; + QString currentTagId_; + QStringList hiddentTagIds_; +}; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 883d384c..4a9f0fff 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -770,7 +770,7 @@ EventStore::decryptEvent(const IdIndex &idx, } mtx::events::collections::TimelineEvents * -EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits) +EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); @@ -778,7 +778,7 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, if (id.empty()) return nullptr; - IdIndex index{room_id_, std::string(id)}; + IdIndex index{room_id_, std::move(id)}; if (resolve_edits) { auto edits_ = edits(index.id); if (!edits_.empty()) { @@ -796,14 +796,12 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, http::client()->get_event( room_id_, index.id, - [this, - relatedTo = std::string(related_to.data(), related_to.size()), - id = index.id](const mtx::events::collections::TimelineEvents &timeline, - mtx::http::RequestErr err) { + [this, relatedTo = std::string(related_to), id = index.id]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { if (err) { nhlog::net()->error( - "Failed to retrieve event with id {}, which " - "was " + "Failed to retrieve event with id {}, which was " "requested to show the replyTo for event {}", relatedTo, id); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index c7a7588b..d9bb86cb 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -70,7 +70,7 @@ public: // optionally returns the event or nullptr and fetches it, after which it emits a // relatedFetched event - mtx::events::collections::TimelineEvents *get(std::string_view id, + mtx::events::collections::TimelineEvents *get(std::string id, std::string_view related_to, bool decrypt = true, bool resolve_edits = true); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..c309daab 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -20,6 +20,7 @@ #include "Cache.h" #include "ChatPage.h" #include "CompletionProxyModel.h" +#include "Config.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -508,8 +509,7 @@ InputBar::command(QString command, QString args) } else if (command == "react") { auto eventId = room->reply(); if (!eventId.isEmpty()) - ChatPage::instance()->timelineManager()->queueReactionMessage( - eventId, args.trimmed()); + reaction(eventId, args.trimmed()); } else if (command == "join") { ChatPage::instance()->joinRoom(args); } else if (command == "part" || command == "leave") { @@ -715,3 +715,35 @@ InputBar::stopTyping() } }); } + +void +InputBar::reaction(const QString &reactedEvent, const QString &reactionKey) +{ + auto reactions = room->reactions(reactedEvent.toStdString()); + + QString selfReactedEvent; + for (const auto &reaction : reactions) { + if (reactionKey == reaction.key_) { + selfReactedEvent = reaction.selfReactedEvent_; + break; + } + } + + if (selfReactedEvent.startsWith("m")) + return; + + // If selfReactedEvent is empty, that means we haven't previously reacted + if (selfReactedEvent.isEmpty()) { + mtx::events::msg::Reaction reaction; + mtx::common::Relation rel; + rel.rel_type = mtx::common::RelationType::Annotation; + rel.event_id = reactedEvent.toStdString(); + rel.key = reactionKey.toStdString(); + reaction.relations.relations.push_back(rel); + + room->sendMessageEvent(reaction, mtx::events::EventType::Reaction); + // Otherwise, we have previously reacted and the reaction should be redacted + } else { + room->redactEvent(selfReactedEvent); + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots: void message(QString body, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); + void reaction(const QString &reactedEvent, const QString &reactionKey); private slots: void startTyping(); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp new file mode 100644
index 00000000..0f980c6c --- /dev/null +++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,590 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomlistModel.h" + +#include "Cache_p.h" +#include "ChatPage.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "TimelineModel.h" +#include "TimelineViewManager.h" +#include "UserSettingsPage.h" + +RoomlistModel::RoomlistModel(TimelineViewManager *parent) + : QAbstractListModel(parent) + , manager(parent) +{ + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); + QHash<QString, QSharedPointer<TimelineModel>>::iterator i; + for (i = models.begin(); i != models.end(); ++i) { + auto ptr = i.value(); + + if (!ptr.isNull()) { + ptr->setDecryptDescription(decrypt); + ptr->updateLastMessage(); + } + } + }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); +} + +QHash<int, QByteArray> +RoomlistModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {RoomName, "roomName"}, + {RoomId, "roomId"}, + {LastMessage, "lastMessage"}, + {Time, "time"}, + {Timestamp, "timestamp"}, + {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, + {NotificationCount, "notificationCount"}, + {IsInvite, "isInvite"}, + {IsSpace, "isSpace"}, + {Tags, "tags"}, + }; +} + +QVariant +RoomlistModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { + auto roomid = roomids.at(index.row()); + + if (models.contains(roomid)) { + auto room = models.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->plainRoomName(); + case Roles::RoomId: + return room->roomId(); + case Roles::LastMessage: + return room->lastMessage().body; + case Roles::Time: + return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant( + static_cast<quint64>(room->lastMessage().timestamp)); + case Roles::HasUnreadMessages: + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); + case Roles::NotificationCount: + return room->notificationCount(); + case Roles::IsInvite: + case Roles::IsSpace: + return false; + case Roles::Tags: { + auto info = cache::singleRoomInfo(roomid.toStdString()); + QStringList list; + for (const auto &t : info.tags) + list.push_back(QString::fromStdString(t)); + return list; + } + default: + return {}; + } + } else if (invites.contains(roomid)) { + auto room = invites.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::RoomId: + return roomid; + case Roles::LastMessage: + return room.msgInfo.body; + case Roles::Time: + return room.msgInfo.descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast<quint64>(room.msgInfo.timestamp)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return true; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } + } else { + return {}; + } + } else { + return {}; + } +} + +void +RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_) +{ + std::vector<int> roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); + } + + this->roomReadStatus[roomid] = roomUnread; + } + + for (auto idx : roomsToUpdate) { + emit dataChanged(index(idx), + index(idx), + { + Roles::HasUnreadMessages, + }); + } +} +void +RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) +{ + if (!models.contains(room_id)) { + // ensure we get read status updates and are only connected once + connect(cache::client(), + &Cache::roomReadStatus, + this, + &RoomlistModel::updateReadStatus, + Qt::UniqueConnection); + + QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); + newRoom->setDecryptDescription( + ChatPage::instance()->userSettings()->decryptSidebar()); + + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + manager->imageProvider(), + &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + manager, + &TimelineViewManager::forwardMessageToRoom); + connect( + newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::LastMessage, + Roles::Timestamp, + Roles::NotificationCount, + Qt::DisplayRole, + }); + }); + connect( + newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::AvatarUrl, + }); + }); + connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + }); + }); + connect( + newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::NotificationCount, + Qt::DisplayRole, + }); + + int total_unread_msgs = 0; + + for (const auto &room : models) { + if (!room.isNull()) + total_unread_msgs += room->notificationCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); + }); + + newRoom->updateLastMessage(); + + bool wasInvite = invites.contains(room_id); + if (!suppressInsertNotification && !wasInvite) + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + + models.insert(room_id, std::move(newRoom)); + + if (wasInvite) { + auto idx = roomidToIndex(room_id); + invites.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else { + roomids.push_back(room_id); + } + + if (!suppressInsertNotification && !wasInvite) + endInsertRows(); + } +} + +void +RoomlistModel::sync(const mtx::responses::Rooms &rooms) +{ + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->sync(room); + // room_model->addEvents(room.timeline); + connect(room_model.data(), + &TimelineModel::newCallEvent, + manager->callManager(), + &CallManager::syncEvent, + Qt::UniqueConnection); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + for (const auto &ev : room.ephemeral.events) { + if (auto t = std::get_if< + mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( + &ev)) { + std::vector<QString> typing; + typing.reserve(t->content.user_ids.size()); + for (const auto &user : t->content.user_ids) { + if (user != http::client()->user_id().to_string()) + typing.push_back( + QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } + } + } + + for (const auto &[room_id, room] : rooms.leave) { + (void)room; + auto idx = this->roomidToIndex(QString::fromStdString(room_id)); + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + if (models.contains(QString::fromStdString(room_id))) + models.remove(QString::fromStdString(room_id)); + else if (invites.contains(QString::fromStdString(room_id))) + invites.remove(QString::fromStdString(room_id)); + endRemoveRows(); + } + } + + for (const auto &[room_id, room] : rooms.invite) { + (void)room; + auto qroomid = QString::fromStdString(room_id); + + auto invite = cache::client()->invite(room_id); + if (!invite) + continue; + + if (invites.contains(qroomid)) { + invites[qroomid] = *invite; + auto idx = roomidToIndex(qroomid); + emit dataChanged(index(idx), index(idx)); + } else { + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + invites.insert(qroomid, *invite); + roomids.push_back(std::move(qroomid)); + endInsertRows(); + } + } +} + +void +RoomlistModel::initializeRooms() +{ + beginResetModel(); + models.clear(); + roomids.clear(); + invites.clear(); + currentRoom_ = nullptr; + + invites = cache::client()->invites(); + for (const auto &id : invites.keys()) + roomids.push_back(id); + + for (const auto &id : cache::client()->roomIds()) + addRoom(id, true); + + endResetModel(); +} + +void +RoomlistModel::clear() +{ + beginResetModel(); + models.clear(); + invites.clear(); + roomids.clear(); + currentRoom_ = nullptr; + emit currentRoomChanged(); + endResetModel(); +} + +void +RoomlistModel::acceptInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->joinRoom(roomid); + } + } +} +void +RoomlistModel::declineInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} +void +RoomlistModel::leave(QString roomid) +{ + if (models.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + models.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} + +void +RoomlistModel::setCurrentRoom(QString roomid) +{ + nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); + if (models.contains(roomid)) { + currentRoom_ = models.value(roomid); + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } +} + +namespace { +enum NotificationImportance : short +{ + ImportanceDisabled = -1, + AllEventsRead = 0, + NewMessage = 1, + NewMentions = 2, + Invite = 3 +}; +} + +short int +FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const +{ + // Returns the degree of importance of the unread messages in the room. + // If sorting by importance is disabled in settings, this only ever + // returns ImportanceDisabled or Invite + if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + return Invite; + } else if (!this->sortByImportance) { + return ImportanceDisabled; + } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { + return NewMentions; + } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { + return NewMessage; + } else { + return AllEventsRead; + } +} +bool +FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); + QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); + + // Sort by "importance" (i.e. invites before mentions before + // notifs before new events before old events), then secondly + // by recency. + + // Checking importance first + const auto a_importance = calculateImportance(left_idx); + const auto b_importance = calculateImportance(right_idx); + if (a_importance != b_importance) { + return a_importance > b_importance; + } + + // Now sort by recency + // Zero if empty, otherwise the time that the event occured + uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); + uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); + + if (a_recency != b_recency) + return a_recency > b_recency; + else + return left.row() < right.row(); +} + +FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent) + : QSortFilterProxyModel(parent) + , roomlistmodel(model) +{ + this->sortByImportance = UserSettings::instance()->sortByImportance(); + setSourceModel(model); + setDynamicSortFilter(true); + + QObject::connect(UserSettings::instance().get(), + &UserSettings::roomSortingChanged, + this, + [this](bool sortByImportance_) { + this->sortByImportance = sortByImportance_; + invalidate(); + }); + + connect(roomlistmodel, + &RoomlistModel::currentRoomChanged, + this, + &FilteredRoomlistModel::currentRoomChanged); + + sort(0); +} + +void +FilteredRoomlistModel::updateHiddenTagsAndSpaces() +{ + hiddenTags.clear(); + hiddenSpaces.clear(); + for (const auto &t : UserSettings::instance()->hiddenTags()) { + if (t.startsWith("tag:")) + hiddenTags.push_back(t.mid(4)); + else if (t.startsWith("space:")) + hiddenSpaces.push_back(t.mid(6)); + } + + invalidateFilter(); +} + +bool +FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const +{ + if (filterType == FilterBy::Nothing) { + if (!hiddenTags.empty()) { + auto tags = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Tag) { + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + if (!tags.contains(filterStr)) + return false; + else if (!hiddenTags.empty()) { + for (const auto &t : tags) + if (t != filterStr && hiddenTags.contains(t)) + return false; + } + return true; + } else { + return true; + } +} + +void +FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) +{ + if (on) { + http::client()->put_tag( + roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to add tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } else { + http::client()->delete_tag( + roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to delete tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } +} + +void +FilteredRoomlistModel::nextRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx++; + if (idx < rowCount()) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} + +void +FilteredRoomlistModel::previousRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx--; + if (idx > 0) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h new file mode 100644
index 00000000..b0244886 --- /dev/null +++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <CacheStructs.h> +#include <QAbstractListModel> +#include <QHash> +#include <QSharedPointer> +#include <QSortFilterProxyModel> +#include <QString> +#include <set> + +#include <mtx/responses/sync.hpp> + +#include "TimelineModel.h" + +class TimelineViewManager; + +class RoomlistModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomName, + RoomId, + LastMessage, + Time, + Timestamp, + HasUnreadMessages, + HasLoudNotification, + NotificationCount, + IsInvite, + IsSpace, + Tags, + }; + + RoomlistModel(TimelineViewManager *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + QSharedPointer<TimelineModel> getRoomById(QString id) const + { + if (models.contains(id)) + return models.value(id); + else + return {}; + } + +public slots: + void initializeRooms(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + int roomidToIndex(QString roomid) + { + for (int i = 0; i < (int)roomids.size(); i++) { + if (roomids[i] == roomid) + return i; + } + + return -1; + } + void acceptInvite(QString roomid); + void declineInvite(QString roomid); + void leave(QString roomid); + TimelineModel *currentRoom() const { return currentRoom_.get(); } + void setCurrentRoom(QString roomid); + void resetCurrentRoom() + { + currentRoom_ = nullptr; + emit currentRoomChanged(); + } + +private slots: + void updateReadStatus(const std::map<QString, bool> roomReadStatus_); + +signals: + void totalUnreadMessageCountUpdated(int unreadMessages); + void currentRoomChanged(); + +private: + void addRoom(const QString &room_id, bool suppressInsertNotification = false); + + TimelineViewManager *manager = nullptr; + std::vector<QString> roomids; + QHash<QString, RoomInfo> invites; + QHash<QString, QSharedPointer<TimelineModel>> models; + std::map<QString, bool> roomReadStatus; + + QSharedPointer<TimelineModel> currentRoom_; + + friend class FilteredRoomlistModel; +}; + +class FilteredRoomlistModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; + +public slots: + int roomidToIndex(QString roomid) + { + return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) + .row(); + } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } + void leave(QString roomid) { roomlistmodel->leave(roomid); } + void toggleTag(QString roomid, QString tag, bool on); + + TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } + void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } + void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); } + + void nextRoom(); + void previousRoom(); + + void updateFilterTag(QString tagId) + { + if (tagId.startsWith("tag:")) { + filterType = FilterBy::Tag; + filterStr = tagId.mid(4); + } else { + filterType = FilterBy::Nothing; + filterStr.clear(); + } + + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); + +signals: + void currentRoomChanged(); + +private: + short int calculateImportance(const QModelIndex &idx) const; + RoomlistModel *roomlistmodel; + bool sortByImportance = true; + + enum class FilterBy + { + Tag, + Space, + Nothing, + }; + QString filterStr = ""; + FilterBy filterType = FilterBy::Nothing; + QStringList hiddenTags, hiddenSpaces; +}; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8df17457..f29f929e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , room_id_(room_id) , manager_(manager) { + lastMessage_.timestamp = 0; + connect( this, &TimelineModel::redactionFailed, @@ -572,7 +574,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r !event_id(event).empty() && event_id(event).front() == '$'); case IsEncrypted: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); return encrypted_event && std::holds_alternative< mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -581,7 +583,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case Trustlevel: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); if (encrypted_event) { if (auto encrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -724,6 +726,20 @@ TimelineModel::fetchMore(const QModelIndex &) } void +TimelineModel::sync(const mtx::responses::JoinedRoom &room) +{ + this->syncState(room.state); + this->addEvents(room.timeline); + + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } +} + +void TimelineModel::syncState(const mtx::responses::State &s) { using namespace mtx::events; @@ -866,14 +882,17 @@ TimelineModel::updateLastMessage() if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { auto time = mtx::accessors::origin_server_ts(*event); uint64_t ts = time.toMSecsSinceEpoch(); - emit manager_->updateRoomsLastMessage( - room_id_, + auto description = DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), QString::fromStdString(http::client()->user_id().to_string()), tr("You joined this room."), utils::descriptiveTime(time), ts, - time}); + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) @@ -884,7 +903,10 @@ TimelineModel::updateLastMessage() QString::fromStdString(http::client()->user_id().to_string()), cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } } @@ -1867,6 +1889,17 @@ TimelineModel::roomName() const } QString +TimelineModel::plainRoomName() const +{ + auto info = cache::getRoomInfo({room_id_.toStdString()}); + + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); +} + +QString TimelineModel::roomAvatarUrl() const { auto info = cache::getRoomInfo({room_id_.toStdString()}); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 92fccd2d..3ebbe120 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -14,6 +14,7 @@ #include <mtxclient/http/errors.hpp> #include "CacheCryptoStructs.h" +#include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" #include "Permissions.h" @@ -253,12 +254,15 @@ public: } void updateLastMessage(); + void sync(const mtx::responses::JoinedRoom &room); void addEvents(const mtx::responses::Timeline &events); void syncState(const mtx::responses::State &state); template<class T> void sendMessageEvent(const T &content, mtx::events::EventType eventType); RelatedInfo relatedInfo(QString id); + DescInfo lastMessage() const { return lastMessage_; } + public slots: void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } @@ -303,12 +307,16 @@ public slots: } QString roomName() const; + QString plainRoomName() const; QString roomTopic() const; InputBar *input() { return &input_; } Permissions *permissions() { return &permissions_; } QString roomAvatarUrl() const; QString roomId() const { return room_id_; } + bool hasMentions() { return highlight_count > 0; } + int notificationCount() { return notification_count; } + QString scrollTarget() const; private slots: @@ -328,6 +336,9 @@ signals: void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); + void lastMessageChanged(); + void notificationsChanged(); + void openRoomSettingsDialog(RoomSettings *settings); void newMessageToSend(mtx::events::collections::TimelineEvents event); @@ -372,7 +383,11 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; + DescInfo lastMessage_{}; + friend struct SendMessageVisitor; + + int notification_count = 0, highlight_count = 0; }; template<class T> diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 0785e3e1..a45294d1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -4,7 +4,6 @@ #include "TimelineViewManager.h" -#include <QDesktopServices> #include <QDropEvent> #include <QMetaType> #include <QPalette> @@ -32,6 +31,7 @@ #include "emoji/Provider.h" #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" +#include "ui/NhekoGlobalObject.h" Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector<DeviceInfo>) @@ -87,21 +87,6 @@ removeReplyFallback(mtx::events::Event<T> &e) } void -TimelineViewManager::updateEncryptedDescriptions() -{ - auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); - QHash<QString, QSharedPointer<TimelineModel>>::iterator i; - for (i = models.begin(); i != models.end(); ++i) { - auto ptr = i.value(); - - if (!ptr.isNull()) { - ptr->setDecryptDescription(decrypt); - ptr->updateLastMessage(); - } - } -} - -void TimelineViewManager::updateColorPalette() { userColors.clear(); @@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const } TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) - : imgProvider(new MxcImageProvider()) + : QObject(parent) + , imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) , blurhashProvider(new BlurhashProvider()) , callManager_(callManager) + , rooms_(new RoomlistModel(this)) + , communities_(new CommunitiesModel(this)) { qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); @@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); return ptr; }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = new FilteredRoomlistModel(self->rooms_); + + connect(self->communities_, + &CommunitiesModel::currentTagIdChanged, + ptr, + &FilteredRoomlistModel::updateFilterTag); + connect(self->communities_, + &CommunitiesModel::hiddenTagsChanged, + ptr, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); + return ptr; + }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = self->communities_; + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); qmlRegisterSingletonType<UserSettings>( "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { auto ptr = ChatPage::instance()->userSettings().data(); @@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Clipboard(); }); + qmlRegisterSingletonType<Nheko>( + "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Nheko(); + }); qRegisterMetaType<mtx::events::collections::TimelineEvents>(); qRegisterMetaType<std::vector<DeviceInfo>>(); @@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Error: Only enums"); #ifdef USE_QUICK_VIEW - view = new QQuickView(); + view = new QQuickView(parent); container = QWidget::createWindowContainer(view, parent); #else view = new QQuickWidget(parent); @@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("blurhash", blurhashProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + view->setSource(QUrl("qrc:///qml/Root.qml")); connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); - connect(parent, - &ChatPage::decryptSidebarChanged, - this, - &TimelineViewManager::updateEncryptedDescriptions); connect( dynamic_cast<ChatPage *>(parent), &ChatPage::receivedRoomDeviceVerificationRequest, @@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem() } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res) { - for (const auto &[room_id, room] : rooms.join) { - // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(room_id)); - const auto &room_model = models.value(QString::fromStdString(room_id)); - if (!isInitialSync_) - connect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); - room_model->syncState(room.state); - room_model->addEvents(room.timeline); - if (!isInitialSync_) - disconnect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); + this->rooms_->sync(rooms_res); + this->communities_->sync(rooms_res); - if (ChatPage::instance()->userSettings()->typingNotifications()) { - for (const auto &ev : room.ephemeral.events) { - if (auto t = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( - &ev)) { - std::vector<QString> typing; - typing.reserve(t->content.user_ids.size()); - for (const auto &user : t->content.user_ids) { - if (user != http::client()->user_id().to_string()) - typing.push_back( - QString::fromStdString(user)); - } - room_model->updateTypingUsers(typing); - } - } - } - } - - this->isInitialSync_ = false; - emit initialSyncChanged(false); -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) { - QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); - newRoom->setDecryptDescription( - ChatPage::instance()->userSettings()->decryptSidebar()); - - connect(newRoom.data(), - &TimelineModel::newEncryptedImage, - imgProvider, - &MxcImageProvider::addEncryptionInfo); - connect(newRoom.data(), - &TimelineModel::forwardToRoom, - this, - &TimelineViewManager::forwardMessageToRoom); - models.insert(room_id, std::move(newRoom)); + if (isInitialSync_) { + this->isInitialSync_ = false; + emit initialSyncChanged(false); } } void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - container->setFocus(); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::highlightRoom(const QString &room_id) -{ - ChatPage::instance()->highlightRoom(room_id); -} - -void TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) { - auto room = models.find(room_id); - if (room != models.end()) { - if (timeline_ != room.value().data()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); + if (auto room = rooms_->getRoomById(room_id)) { + if (rooms_->currentRoom() != room) { + rooms_->setCurrentRoom(room_id); container->setFocus(); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } - timeline_->showEvent(event_id); + room->showEvent(event_id); } } @@ -457,12 +393,14 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) auto imgDialog = new dialogs::ImageOverlay(pixmap); imgDialog->showFullScreen(); - connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() { + + auto room = rooms_->currentRoom(); + connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { // hide the overlay while presenting the save dialog for better // cross platform support. imgDialog->hide(); - if (!timeline_->saveMedia(eventId)) { + if (!room->saveMedia(eventId)) { imgDialog->show(); } else { imgDialog->close(); @@ -471,70 +409,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) } void -TimelineViewManager::openLink(QString link) const -{ - QUrl url(link); - if (url.scheme() == "https" && url.host() == "matrix.to") { - // handle matrix.to links internally - QString p = url.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray uri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("#")) { - QByteArray uri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("!")) { - QByteArray uri = "matrix:roomid/" + - QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } - } - } else { - QDesktopServices::openUrl(url); - } -} - -void TimelineViewManager::openInviteUsersDialog() { MainWindow::instance()->openInviteUsersDialog( [this](const QStringList &invitees) { emit inviteUsers(invitees); }); } void -TimelineViewManager::openMemberListDialog() const +TimelineViewManager::openMemberListDialog(QString roomid) const { - MainWindow::instance()->openMemberListDialog(timeline_->roomId()); + MainWindow::instance()->openMemberListDialog(roomid); } void -TimelineViewManager::openLeaveRoomDialog() const +TimelineViewManager::openLeaveRoomDialog(QString roomid) const { - MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId()); + MainWindow::instance()->openLeaveRoomDialog(roomid); } void @@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid) if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != room_members.end()) { - auto model = models.value(QString::fromStdString(room_id)); - auto flow = DeviceVerificationFlow::InitiateUserVerification( - this, model.data(), userid); - connect(model.data(), - &TimelineModel::updateFlowEventId, - this, - [this, flow](std::string eventId) { - dvList[QString::fromStdString(eventId)] = flow; - }); - emit newDeviceVerificationRequest(flow.data()); - return; + if (auto model = + rooms_->getRoomById(QString::fromStdString(room_id))) { + auto flow = + DeviceVerificationFlow::InitiateUserVerification( + this, model.data(), userid); + connect(model.data(), + &TimelineModel::updateFlowEventId, + this, + [this, flow](std::string eventId) { + dvList[QString::fromStdString(eventId)] = + flow; + }); + emit newDeviceVerificationRequest(flow.data()); + return; + } } } } @@ -593,26 +485,24 @@ void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids) { - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); + if (auto room = rooms_->getRoomById(room_id)) { + room->markEventsAsRead(event_ids); } } void TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) { - auto room = models.find(QString::fromStdString(room_id)); - if (room != models.end()) { - room.value()->receivedSessionKey(session_id); + if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) { + room->receivedSessionKey(session_id); } } void -TimelineViewManager::initWithMessages(const std::vector<QString> &roomIds) +TimelineViewManager::initializeRoomlist() { - for (const auto &roomId : roomIds) - addRoom(roomId); + rooms_->initializeRooms(); + communities_->initializeSidebar(); } void @@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody) { - auto room = models.find(roomid); - if (room != models.end()) { - room.value()->setReply(repliedToEvent); - room.value()->input()->message(replyBody); + if (auto room = rooms_->getRoomById(roomid)) { + room->setReply(repliedToEvent); + room->input()->message(replyBody); } } void -TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey) -{ - if (!timeline_) - return; - - auto reactions = timeline_->reactions(reactedEvent.toStdString()); - - QString selfReactedEvent; - for (const auto &reaction : reactions) { - if (reactionKey == reaction.key_) { - selfReactedEvent = reaction.selfReactedEvent_; - break; - } - } - - if (selfReactedEvent.startsWith("m")) - return; - - // If selfReactedEvent is empty, that means we haven't previously reacted - if (selfReactedEvent.isEmpty()) { - mtx::events::msg::Reaction reaction; - mtx::common::Relation rel; - rel.rel_type = mtx::common::RelationType::Annotation; - rel.event_id = reactedEvent.toStdString(); - rel.key = reactionKey.toStdString(); - reaction.relations.relations.push_back(rel); - - timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); - // Otherwise, we have previously reacted and the reaction should be redacted - } else { - timeline_->redactEvent(selfReactedEvent); - } -} -void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) { - models.value(roomid)->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &callCandidates) { - models.value(roomid)->sendMessageEvent(callCandidates, - mtx::events::EventType::CallCandidates); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &callAnswer) { - models.value(roomid)->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &callHangUp) { - models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } void @@ -738,7 +596,7 @@ void TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId) { - auto room = models.find(roomId); + auto room = rooms_->getRoomById(roomId); auto content = mtx::accessors::url(*e); std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); @@ -781,12 +639,15 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven ev.content.url = url; } - auto room = models.find(roomId); - removeReplyFallback(ev); - ev.content.relations.relations.clear(); - room.value()->sendMessageEvent( - ev.content, - mtx::events::EventType::RoomMessage); + if (auto room = rooms_->getRoomById(roomId)) { + removeReplyFallback(ev); + ev.content.relations.relations + .clear(); + room->sendMessageEvent( + ev.content, + mtx::events::EventType:: + RoomMessage); + } } }, *e); @@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven mtx::events::EventType::RoomMessage) { e.content.relations.relations.clear(); removeReplyFallback(e); - room.value()->sendMessageEvent(e.content, - mtx::events::EventType::RoomMessage); + room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage); } }, *e); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b23a61db..556bcf4c 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,8 @@ #include "WebRTCSession.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "timeline/CommunitiesModel.h" +#include "timeline/RoomlistModel.h" class MxcImageProvider; class BlurhashProvider; @@ -35,12 +37,8 @@ class TimelineViewManager : public QObject Q_OBJECT Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - Q_PROPERTY( bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) Q_PROPERTY( - bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged) - Q_PROPERTY( bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged) public: @@ -48,48 +46,38 @@ public: QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - void clearAll() - { - timeline_ = nullptr; - emit activeTimelineChanged(nullptr); - models.clear(); - } + MxcImageProvider *imageProvider() { return imgProvider; } + CallManager *callManager() { return callManager_; } + + void clearAll() { rooms_->clear(); } - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - bool isNarrowView() const { return isNarrowView_; } bool isWindowFocused() const { return isWindowFocused_; } Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; - Q_INVOKABLE void openLink(QString link) const; - Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); - Q_INVOKABLE void openMemberListDialog() const; - Q_INVOKABLE void openLeaveRoomDialog() const; + Q_INVOKABLE void openMemberListDialog(QString roomid) const; + Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); void verifyUser(QString userid); void verifyDevice(QString userid, QString deviceid); signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); void replyingEventChanged(QString replyingEvent); void replyClosed(); void newDeviceVerificationRequest(DeviceVerificationFlow *flow); void inviteUsers(QStringList users); - void showRoomList(); - void narrowViewChanged(); void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); @@ -98,58 +86,32 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void initWithMessages(const std::vector<QString> &roomIds); + void initializeRoomlist(); void chatFocusChanged(bool focused) { isWindowFocused_ = focused; emit focusChanged(); } - void setHistoryView(const QString &room_id); - void highlightRoom(const QString &room_id); void showEvent(const QString &room_id, const QString &event_id); void focusTimeline(); - TimelineModel *getHistoryView(const QString &room_id) - { - auto room = models.find(room_id); - if (room != models.end()) - return room.value().data(); - else - return nullptr; - } void updateColorPalette(); void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody); - void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); - void updateEncryptedDescriptions(); void setVideoCallItem(); - void enableBackButton() - { - if (isNarrowView_) - return; - isNarrowView_ = true; - emit narrowViewChanged(); - } - void disableBackButton() - { - if (!isNarrowView_) - return; - isNarrowView_ = false; - emit narrowViewChanged(); - } - - void backToRooms() { emit showRoomList(); } QObject *completerFor(QString completerName, QString roomId = ""); void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); + RoomlistModel *rooms() { return rooms_; } + private slots: void openImageOverlayInternal(QString eventId, QImage img); @@ -165,14 +127,14 @@ private: ColorImageProvider *colorImgProvider; BlurhashProvider *blurhashProvider; - QHash<QString, QSharedPointer<TimelineModel>> models; - TimelineModel *timeline_ = nullptr; CallManager *callManager_ = nullptr; bool isInitialSync_ = true; - bool isNarrowView_ = false; bool isWindowFocused_ = false; + RoomlistModel *rooms_ = nullptr; + CommunitiesModel *communities_ = nullptr; + QHash<QString, QColor> userColors; QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList;