From 22ea325ef03cdc15f2c36b1e0c82c84dec01cfb5 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 3 Mar 2017 12:29:53 +0900 Subject: Use special indices instead of iterators for persistent pointers into timeline + no more discarding read markers to events that haven't arrived yet When using deque::const_reverse_iterator for read markers and eventsIndex, I didn't realise that insertions into std::deque invalidate iterators (though preserve references and pointers). Therefore, a small TimelineItem class has been introduced that stores an event together with a persistent index that is generated upon insertion into the timeline (timeline.back()+1 for newer events, timeline.front()-1 for older events). Using such indices, we can still reach an event by it's index in constant time, while avoiding a problem with invalidating iterators. While rewriting the code, another problem has been detected with read markers to events that haven't yet arrived to the timeline (in particular, older events). The old code simply discarded such read markers. The new code stores such read markers anyway, so that when that event arrives, it could be matched against the stored last-read-event id. --- room.cpp | 150 +++++++++++++++++++++++++++++++++++++-------------------------- room.h | 29 +++++++++--- 2 files changed, 111 insertions(+), 68 deletions(-) diff --git a/room.cpp b/room.cpp index 3d5ede4d..2f477682 100644 --- a/room.cpp +++ b/room.cpp @@ -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 members_map_t; typedef Timeline::const_reverse_iterator rev_iter_t; + typedef std::pair 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 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 promoteReadMarker(User* u, QString eventId); + void setLastReadEvent(User* u, QString eventId); - private: - QHash eventsIndex; + rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); + private: QString calculateDisplayname() const; QString roomNameFromMemberNames(const QList& 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::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(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); + } } } } diff --git a/room.h b/room.h index ff76b25a..b2e56502 100644 --- a/room.h +++ b/room.h @@ -18,6 +18,9 @@ #pragma once +#include +#include + #include #include #include @@ -26,8 +29,6 @@ #include "jobs/syncjob.h" #include "joinstate.h" -#include - 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 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 >; + using Timeline = std::deque; 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 -- cgit v1.2.3 From 4dddf3e49c55caa7af05f88a04f35b43a172796d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 14 Mar 2017 16:14:44 +0900 Subject: Room: exposed findInTimeline and related things from Room::Private This will be used from Quaternion for a better algorithm dealing with read markers --- room.cpp | 136 +++++++++++++++++++++++++++++++++++++++------------------------ room.h | 29 ++++++++++++-- 2 files changed, 111 insertions(+), 54 deletions(-) diff --git a/room.cpp b/room.cpp index 2f477682..1cedab1e 100644 --- a/room.cpp +++ b/room.cpp @@ -42,19 +42,11 @@ 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 members_map_t; - typedef Timeline::const_reverse_iterator rev_iter_t; typedef std::pair rev_iter_pair_t; Private(Connection* c, const QString& id_) @@ -103,47 +95,29 @@ class Room::Private void getPreviousContent(int limit = 10); - bool isEventNotable(const Event* e) const; + bool isEventNotable(const Event* e) const + { + return e->senderId() != connection->userId() && + e->type() == EventType::RoomMessage; + } void appendEvent(Event* e) { insertEvent(e, timeline.end(), - timeline.empty() ? 0 : timeline.back().index() + 1); + timeline.empty() ? 0 : q->maxTimelineIndex() + 1); } void prependEvent(Event* e) { insertEvent(e, timeline.begin(), - timeline.empty() ? 0 : timeline.front().index() - 1); + timeline.empty() ? 0 : q->minTimelineIndex() - 1); } /** * Removes events from the passed container that are already in the timeline */ - void dropDuplicateEvents(Events& events) const - { - // Collect all duplicate events at the end of the container - auto dupsBegin = - std::stable_partition(events.begin(), events.end(), - [&] (Event* e) { return !eventsIndex.contains(e->id()); }); - // Dispose of those dups - std::for_each(dupsBegin, events.end(), [] (Event* e) { delete e; }); - 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(); - } + void dropDuplicateEvents(Events& events) const; - rev_iter_t readMarker(const User* user) const - { - return findInTimeline(lastReadEventIds[user]); - } void setLastReadEvent(User* u, QString eventId); - rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker); private: @@ -231,12 +205,12 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) } Room::Private::rev_iter_pair_t -Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker) +Room::Private::promoteReadMarker(User* u, Room::rev_iter_t newMarker) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); - const auto prevMarker = readMarker(u); + const auto prevMarker = q->readMarker(u); if (prevMarker <= newMarker) // Remember, we deal with reverse iterators return { prevMarker, prevMarker }; @@ -273,7 +247,9 @@ void Room::markMessagesAsRead(QString uptoEventId) { User* localUser = connection()->user(); Private::rev_iter_pair_t markers = - d->promoteReadMarker(localUser, d->findInTimeline(uptoEventId)); + d->promoteReadMarker(localUser, findInTimeline(uptoEventId)); + if (markers.first != markers.second) + qDebug() << "Marked messages as read until" << *readMarker(); // 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 @@ -283,7 +259,7 @@ void Room::markMessagesAsRead(QString uptoEventId) if ((*markers.second)->senderId() != localUser->id()) { connection()->callApi(this->id(), - (*markers.second)->id()); + (*markers.second)->id()); break; } } @@ -294,6 +270,52 @@ bool Room::hasUnreadMessages() return d->unreadMessages; } +Room::rev_iter_t Room::timelineEdge() const +{ + return d->timeline.crend(); +} + +TimelineItem::index_t Room::minTimelineIndex() const +{ + return d->timeline.empty() ? 0 : d->timeline.front().index(); +} + +TimelineItem::index_t Room::maxTimelineIndex() const +{ + return d->timeline.empty() ? 0 : d->timeline.back().index(); +} + +bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const +{ + return !d->timeline.empty() && + timelineIndex >= minTimelineIndex() && + timelineIndex <= maxTimelineIndex(); +} + +Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const +{ + return timelineEdge() - + (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); +} + +Room::rev_iter_t Room::findInTimeline(QString evtId) const +{ + if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) + return findInTimeline(d->eventsIndex.value(evtId)); + return timelineEdge(); +} + +Room::rev_iter_t Room::readMarker(const User* user) const +{ + Q_ASSERT(user); + return findInTimeline(d->lastReadEventIds.value(user)); +} + +Room::rev_iter_t Room::readMarker() const +{ + return readMarker(connection()->user()); +} + QString Room::readMarkerEventId() const { return d->lastReadEventIds.value(d->connection->user()); @@ -377,14 +399,18 @@ void Room::Private::insertEvent(Event* e, Timeline::iterator where, 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")); - Q_ASSERT_X(!eventsIndex.contains(e->id()), __FUNCTION__, - makeErrorStr(e, - "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"); + if (eventsIndex.contains(e->id())) + { + qWarning() << "Event" << e->id() << "is already in the timeline."; + qWarning() << "Either dropDuplicateEvents() wasn't called or duplicate " + "events within the same batch arrived from the server."; + return; + } timeline.emplace(where, e, index); eventsIndex.insert(e->id(), index); - Q_ASSERT(findInTimeline(e->id())->event() == e); + Q_ASSERT(q->findInTimeline(e->id())->event() == e); } void Room::Private::addMember(User *u) @@ -536,6 +562,17 @@ void Room::Private::getPreviousContent(int limit) } } +void Room::Private::dropDuplicateEvents(Events& events) const +{ + // Collect all duplicate events at the end of the container + auto dupsBegin = + std::stable_partition(events.begin(), events.end(), + [&] (Event* e) { return !eventsIndex.contains(e->id()); }); + // Dispose of those dups + std::for_each(dupsBegin, events.end(), [] (Event* e) { delete e; }); + events.erase(dupsBegin, events.end()); +} + Connection* Room::connection() const { return d->connection; @@ -551,12 +588,6 @@ void Room::addNewMessageEvents(Events events) emit addedMessages(); } -bool Room::Private::isEventNotable(const Event* e) const -{ - return e->senderId() != connection->userId() && - e->type() == EventType::RoomMessage; -} - void Room::doAddNewMessageEvents(const Events& events) { Q_ASSERT(!events.isEmpty()); @@ -577,7 +608,9 @@ void Room::doAddNewMessageEvents(const Events& events) // 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())); + d->promoteReadMarker(firstWriter, findInTimeline(events.front()->id())); + qDebug() << "Auto-promoted read marker for" << firstWriter->id() + << "to" << *readMarker(firstWriter); if( !d->unreadMessages && newUnreadMessages > 0) { @@ -688,7 +721,7 @@ void Room::processEphemeralEvent(Event* event) } if (d->eventsIndex.contains(eventId)) { - const auto newMarker = d->findInTimeline(eventId); + const auto newMarker = findInTimeline(eventId); for( const Receipt& r: receipts ) if (auto m = d->member(r.userId)) d->promoteReadMarker(m, newMarker); @@ -702,7 +735,7 @@ void Room::processEphemeralEvent(Event* event) // 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()) + if (readMarker(m) == timelineEdge()) d->setLastReadEvent(m, eventId); } } @@ -810,3 +843,4 @@ bool MemberSorter::operator()(User *u1, User *u2) const n2.remove(0, 1); return n1 < n2; } + diff --git a/room.h b/room.h index b2e56502..582b89c4 100644 --- a/room.h +++ b/room.h @@ -39,7 +39,9 @@ namespace QMatrixClient class TimelineItem { public: - using index_t = int; // For compatibility with Qt containers + // For compatibility with Qt containers, even though we use + // a std:: container now + using index_t = int; TimelineItem(Event* e, index_t number) : evt(e), idx(number) { } @@ -51,6 +53,12 @@ namespace QMatrixClient std::unique_ptr evt; index_t idx; }; + inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) + { + QDebugStateSaver dss(d); + d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; + return d; + } class Room: public QObject { @@ -58,12 +66,12 @@ namespace QMatrixClient Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) public: using Timeline = std::deque; + using rev_iter_t = Timeline::const_reverse_iterator; Room(Connection* connection, QString id); virtual ~Room(); Q_INVOKABLE QString id() const; - Q_INVOKABLE const Timeline& messageEvents() const; Q_INVOKABLE QString name() const; Q_INVOKABLE QStringList aliases() const; Q_INVOKABLE QString canonicalAlias() const; @@ -89,6 +97,21 @@ namespace QMatrixClient Q_INVOKABLE void updateData(SyncRoomData& data ); Q_INVOKABLE void setJoinState( JoinState state ); + const Timeline& messageEvents() const; + /** + * A convenience method returning the read marker to the before-oldest + * message + */ + rev_iter_t timelineEdge() const; + Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; + Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; + Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; + + rev_iter_t findInTimeline(TimelineItem::index_t index) const; + rev_iter_t findInTimeline(QString evtId) const; + + rev_iter_t readMarker(const User* user) const; + rev_iter_t readMarker() const; QString readMarkerEventId() const; /** * @brief Mark the event with uptoEventId as read @@ -98,7 +121,7 @@ namespace QMatrixClient * for this message or, if it's from the local user, for * the nearest non-local message before. uptoEventId must be non-empty. */ - Q_INVOKABLE void markMessagesAsRead(QString uptoEventId); + void markMessagesAsRead(QString uptoEventId); Q_INVOKABLE bool hasUnreadMessages(); -- cgit v1.2.3