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;
|