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