diff options
-rw-r--r-- | room.cpp | 150 | ||||
-rw-r--r-- | room.h | 29 |
2 files changed, 111 insertions, 68 deletions
@@ -42,12 +42,20 @@ using namespace QMatrixClient; +inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) +{ + QDebugStateSaver dss(d); + d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; + return d; +} + class Room::Private { public: /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */ typedef QMultiHash<QString, User*> members_map_t; typedef Timeline::const_reverse_iterator rev_iter_t; + typedef std::pair<rev_iter_t, rev_iter_t> rev_iter_pair_t; Private(Connection* c, const QString& id_) : q(nullptr), connection(c), id(id_), joinState(JoinState::Join) @@ -64,6 +72,7 @@ class Room::Private Connection* connection; Timeline timeline; + QHash<QString, TimelineItem::index_t> eventsIndex; QString id; QStringList aliases; QString canonicalAlias; @@ -98,11 +107,13 @@ class Room::Private void appendEvent(Event* e) { - insertEvent(e, timeline.end()); + insertEvent(e, timeline.end(), + timeline.empty() ? 0 : timeline.back().index() + 1); } void prependEvent(Event* e) { - insertEvent(e, timeline.begin()); + insertEvent(e, timeline.begin(), + timeline.empty() ? 0 : timeline.front().index() - 1); } /** @@ -119,22 +130,31 @@ class Room::Private events.erase(dupsBegin, events.end()); } + rev_iter_t findInTimeline(QString evtId) const + { + if (!timeline.empty() && eventsIndex.contains(evtId)) + return timeline.crbegin() + + (timeline.back().index() - eventsIndex.value(evtId)); + return timeline.crend(); + } + rev_iter_t readMarker(const User* user) const { - return eventsIndex.value(lastReadEventIds[user], timeline.crend()); + return findInTimeline(lastReadEventIds[user]); } - std::pair<rev_iter_t, rev_iter_t> promoteReadMarker(User* u, QString eventId); + void setLastReadEvent(User* u, QString eventId); - private: - QHash<QString, rev_iter_t> eventsIndex; + rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); + private: QString calculateDisplayname() const; QString roomNameFromMemberNames(const QList<User*>& userlist) const; void insertMemberIntoMap(User* u); void removeMemberFromMap(QString username, User* u); - void insertEvent(Event* e, Timeline::iterator where); + void insertEvent(Event* e, Timeline::iterator where, + TimelineItem::index_t index); }; Room::Room(Connection* connection, QString id) @@ -202,52 +222,45 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -void Room::setLastReadEvent(User* user, Event* event) +void Room::Private::setLastReadEvent(User* u, QString eventId) { - d->lastReadEventIds.insert(user, event->id()); - emit lastReadEventChanged(user); - if (user == d->connection->user()) - emit readMarkerPromoted(); + lastReadEventIds.insert(u, eventId); + emit q->lastReadEventChanged(u); + if (u == connection->user()) + emit q->readMarkerMoved(); } -std::pair<Room::Private::rev_iter_t, Room::Private::rev_iter_t> -Room::Private::promoteReadMarker(User* u, QString eventId) +Room::Private::rev_iter_pair_t +Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker) { - Q_ASSERT_X (!eventId.isEmpty(), __FUNCTION__, - "Attempt to promote a read marker to an event with no id"); + Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); + Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); - // Find the event by its id and position the marker at it. If the event - // is not found (most likely, because it's not fetched from the server), - // or if it's older than the current marker position, keep the marker intact. - const auto newMarker = eventsIndex.value(eventId, timeline.crend()); const auto prevMarker = readMarker(u); if (prevMarker <= newMarker) // Remember, we deal with reverse iterators return { prevMarker, prevMarker }; - using namespace std; + Q_ASSERT(newMarker < timeline.crend()); // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). auto eagerMarker = find_if(newMarker.base(), timeline.cend(), - [=](Event* e) { return e->senderId() != u->id(); }); - if (eagerMarker > timeline.begin()) - q->setLastReadEvent(u, *(eagerMarker - 1)); + [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); + setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (u == connection->user() && unreadMessages) { auto stillUnreadMessagesCount = count_if(eagerMarker, timeline.cend(), - [=](Event* e) { return isEventNotable(e); }); + [=](const TimelineItem& ti) { return isEventNotable(ti.event()); }); if (stillUnreadMessagesCount == 0) { unreadMessages = false; - qDebug() << "Room" << q->displayName() - << "has no more unread messages"; + qDebug() << "Room" << displayname << "has no more unread messages"; emit q->unreadMessagesChanged(q); - } - else - qDebug() << "Room" << q->displayName() << "still has" + } else + qDebug() << "Room" << displayname << "still has" << stillUnreadMessagesCount << "unread message(s)"; } @@ -258,14 +271,16 @@ Room::Private::promoteReadMarker(User* u, QString eventId) void Room::markMessagesAsRead(QString uptoEventId) { - auto markers = d->promoteReadMarker(connection()->user(), uptoEventId); + User* localUser = connection()->user(); + Private::rev_iter_pair_t markers = + d->promoteReadMarker(localUser, d->findInTimeline(uptoEventId)); // We shouldn't send read receipts for the local user's own messages - so // search earlier messages for the latest message not from the local user // until the previous last-read message, whichever comes first. for (; markers.second < markers.first; ++markers.second) { - if ((*markers.second)->senderId() != connection()->userId()) + if ((*markers.second)->senderId() != localUser->id()) { connection()->callApi<PostReceiptJob>(this->id(), (*markers.second)->id()); @@ -350,24 +365,26 @@ void Room::Private::removeMemberFromMap(QString username, User* u) updateDisplayname(); } -inline QByteArray makeErrorStr(Event* e, const char* msg) +inline QByteArray makeErrorStr(const Event* e, const char* msg) { return QString("%1; event dump follows:\n%2") .arg(msg, e->originalJson()).toUtf8(); } -void Room::Private::insertEvent(Event* e, Room::Timeline::iterator where) +void Room::Private::insertEvent(Event* e, Timeline::iterator where, + TimelineItem::index_t index) { Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); Q_ASSERT_X(!e->id().isEmpty(), __FUNCTION__, - makeErrorStr(e, - "Event with empty id cannot be in the timeline")); + makeErrorStr(e, "Event with empty id cannot be in the timeline")); Q_ASSERT_X(!eventsIndex.contains(e->id()), __FUNCTION__, makeErrorStr(e, - "Event is already in the timeline (use dropDuplicateEvents())")); - Q_ASSERT(where >= timeline.begin() && where <= timeline.end()); - eventsIndex.insert(e->id(), rev_iter_t(timeline.insert(where, e) + 1)); - Q_ASSERT((*eventsIndex.value(e->id()))->id() == e->id()); + "Event is already in the timeline (use dropDuplicateEvents())")); + Q_ASSERT_X(where == timeline.end() || where == timeline.begin(), __FUNCTION__, + "Events can only be appended or prepended to the timeline"); + timeline.emplace(where, e, index); + eventsIndex.insert(e->id(), index); + Q_ASSERT(findInTimeline(e->id())->event() == e); } void Room::Private::addMember(User *u) @@ -543,29 +560,24 @@ bool Room::Private::isEventNotable(const Event* e) const void Room::doAddNewMessageEvents(const Events& events) { Q_ASSERT(!events.isEmpty()); - Timeline::size_type newUnreadMessages = 0; - - // The first event in the batch defines whose read marker we can - // automatically promote any further (if this author has read the whole - // timeline by then). Others will need explicit read receipts - // from the server (or, for the local user, markMessagesAsRead() invocation) - // to promote their read markers over the new message events. - User* firstWriter = connection()->user(events.front()->senderId()); - bool canAutoPromote = d->readMarker(firstWriter) == d->timeline.crbegin(); + Timeline::size_type newUnreadMessages = 0; for (auto e: events) { d->appendEvent(e); newUnreadMessages += d->isEventNotable(e); } - qDebug() << "Added" << events.size() + qDebug() << "Room" << displayName() << "received" << events.size() << "(with" << newUnreadMessages << "notable)" - << "new events to room" << displayName(); - if (canAutoPromote) - { - qDebug() << "Auto-promoting" << firstWriter->id() << "over own events"; - d->promoteReadMarker(firstWriter, events.front()->id()); - } + << "new events; the last event is now" << d->timeline.back(); + + // The first event in the batch defines whose read marker can possibly be + // promoted any further over the same author's events newly arrived. + // Others will need explicit read receipts from the server (or, for + // the local user, markMessagesAsRead() invocation) to promote their + // read markers over the new message events. + User* firstWriter = connection()->user(events.front()->senderId()); + d->promoteReadMarker(firstWriter, d->findInTimeline(events.front()->id())); if( !d->unreadMessages && newUnreadMessages > 0) { @@ -591,8 +603,8 @@ void Room::doAddHistoricalMessageEvents(const Events& events) // Historical messages arrive in newest-to-oldest order for (auto e: events) d->prependEvent(e); - qDebug() << "Added" << events.size() << "historical events to room" - << displayName(); + qDebug() << "Room" << displayName() << "received" << events.size() + << "past events; the oldest event is now" << d->timeline.front(); } void Room::processStateEvents(const Events& events) @@ -674,9 +686,25 @@ void Room::processEphemeralEvent(Event* event) else qd << receipts.size() << "users"; } - for( const Receipt& r: receipts ) - if (auto m = d->member(r.userId)) - d->promoteReadMarker(m, eventId); + if (d->eventsIndex.contains(eventId)) + { + const auto newMarker = d->findInTimeline(eventId); + for( const Receipt& r: receipts ) + if (auto m = d->member(r.userId)) + d->promoteReadMarker(m, newMarker); + } else + { + qDebug() << "Event" << eventId + << "not found; saving read markers anyway"; + // If the event is not found (most likely, because it's too old + // and hasn't been fetched from the server yet), but there is + // a previous marker for a user, keep the previous marker. + // Otherwise, blindly store the event id for this user. + for( const Receipt& r: receipts ) + if (auto m = d->member(r.userId)) + if (d->readMarker(m) == d->timeline.crend()) + d->setLastReadEvent(m, eventId); + } } } } @@ -18,6 +18,9 @@ #pragma once +#include <memory> +#include <deque> + #include <QtCore/QList> #include <QtCore/QStringList> #include <QtCore/QObject> @@ -26,8 +29,6 @@ #include "jobs/syncjob.h" #include "joinstate.h" -#include <deque> - namespace QMatrixClient { class Event; @@ -35,12 +36,28 @@ namespace QMatrixClient class User; class MemberSorter; + class TimelineItem + { + public: + using index_t = int; // For compatibility with Qt containers + + TimelineItem(Event* e, index_t number) : evt(e), idx(number) { } + + Event* event() const { return evt.get(); } + Event* operator->() const { return event(); } //< Synonym for event() + index_t index() const { return idx; } + + private: + std::unique_ptr<Event> evt; + index_t idx; + }; + class Room: public QObject { Q_OBJECT - Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerPromoted) + Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) public: - using Timeline = Owning< std::deque<Event*> >; + using Timeline = std::deque<TimelineItem>; Room(Connection* connection, QString id); virtual ~Room(); @@ -119,7 +136,7 @@ namespace QMatrixClient void highlightCountChanged(Room* room); void notificationCountChanged(Room* room); void lastReadEventChanged(User* user); - void readMarkerPromoted(); + void readMarkerMoved(); void unreadMessagesChanged(Room* room); protected: @@ -135,8 +152,6 @@ namespace QMatrixClient void addNewMessageEvents(Events events); void addHistoricalMessageEvents(Events events); - - void setLastReadEvent(User* user, Event* event); }; class MemberSorter |