aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--room.cpp148
-rw-r--r--room.h46
2 files changed, 164 insertions, 30 deletions
diff --git a/room.cpp b/room.cpp
index c2a1ce70..2d32c430 100644
--- a/room.cpp
+++ b/room.cpp
@@ -28,7 +28,6 @@
#include "connection.h"
#include "state.h"
#include "user.h"
-#include "events/event.h"
#include "events/roommessageevent.h"
#include "events/roomnameevent.h"
#include "events/roomaliasesevent.h"
@@ -49,7 +48,7 @@ class Room::Private
Private(Connection* c, const QString& id_)
: q(nullptr), connection(c), id(id_), joinState(JoinState::Join)
- , roomMessagesJob(nullptr)
+ , unreadMessages(false), roomMessagesJob(nullptr)
{ }
Room* q;
@@ -68,6 +67,7 @@ class Room::Private
QString displayname;
QString topic;
JoinState joinState;
+ bool unreadMessages;
int highlightCount;
int notificationCount;
members_map_t membersMap;
@@ -90,6 +90,7 @@ class Room::Private
void getPreviousContent();
+ bool isEventNotable(const Event* e) const;
private:
QString calculateDisplayname() const;
QString roomNameFromMemberNames(const QList<User*>& userlist) const;
@@ -167,34 +168,77 @@ void Room::setLastReadEvent(User* user, QString eventId)
{
d->lastReadEvent.insert(user, eventId);
emit lastReadEventChanged(user);
+ if (user == d->connection->user())
+ emit readMarkerPromoted();
}
-bool Room::promoteReadMarker(User* user, QString eventId)
+Room::Timeline::const_iterator Room::promoteReadMarker(User* u, QString eventId)
{
- // Check that the new read event is not before the previously set - only
- // allow the read marker to move down the timeline, not up.
- QString prevLastReadId = lastReadEvent(user);
+ QString prevLastReadId = lastReadEvent(u);
+ int stillUnreadMessagesCount = 0;
+ auto it = d->messageEvents.end();
+ Event* targetEvent = nullptr;
// Older Qt doesn't provide rbegin()/rend() for Qt containers
- for (auto it = messageEvents().end(); it != messageEvents().begin();)
+ while (it != d->messageEvents.begin())
{
--it;
+ // Check that the new read event is not before the previously set - only
+ // allow the read marker to move down the timeline, not up.
if (prevLastReadId == (*it)->id())
- return false;
+ break;
+
+ // Found the message to mark as read; if there are messages from
+ // that user right below this one, automatically promote the marker
+ // to them instead of this one; still return this one to save
+ // markMessagesAsRead() from going through local messages over again.
if (eventId == (*it)->id())
{
- setLastReadEvent(user, eventId);
- return true;
+ setLastReadEvent(u, (targetEvent ? targetEvent : *it)->id());
+ break;
+ }
+
+ // If we are on a message from that user (or a series thereof),
+ // remember it (or the end of the sequence) so that we could use it
+ // in case when the event to promote the marker to is immediately
+ // above the ones from that user.
+ if ((*it)->senderId() == u->id())
+ {
+ if (!targetEvent)
+ targetEvent = *it;
+ }
+ else
+ targetEvent = nullptr;
+
+ // Detect events "notable" for the local user so that we can properly
+ // set unreadMessages
+ if (u == connection()->user())
+ stillUnreadMessagesCount += d->isEventNotable(*it);
+ }
+
+ if( u == connection()->user() )
+ {
+ if (d->unreadMessages && stillUnreadMessagesCount == 0)
+ {
+ d->unreadMessages = false;
+ qDebug() << "Room" << displayName() << ": no more unread messages";
+ emit unreadMessagesChanged(this);
}
+ if (stillUnreadMessagesCount > 0)
+ qDebug() << "Room" << displayName()
+ << ": still" << stillUnreadMessagesCount << "unread message(s)";
}
- return false;
+ return it;
}
-void Room::markMessagesAsRead(Timeline::const_iterator last)
+void Room::markMessagesAsRead(QString uptoEventId)
{
- QString prevLastReadId = lastReadEvent(connection()->user());
- if ( !promoteReadMarker(connection()->user(), (*last)->id()) )
+ if (d->messageEvents.empty())
return;
+ User* localUser = connection()->user();
+ QString prevLastReadId = lastReadEvent(localUser);
+ auto last = promoteReadMarker(localUser, uptoEventId);
+
// We shouldn't send read receipts for messages from the local user - so
// shift back (if necessary) to the nearest message not from the local user
// or the so far last read message, whichever comes first.
@@ -202,7 +246,7 @@ void Room::markMessagesAsRead(Timeline::const_iterator last)
{
if ((*last)->senderId() != connection()->userId())
{
- d->connection->postReceipt(this, (*last));
+ d->connection->postReceipt(this, *last);
break;
}
if (last == messageEvents().begin())
@@ -213,14 +257,24 @@ void Room::markMessagesAsRead(Timeline::const_iterator last)
void Room::markMessagesAsRead()
{
if (!messageEvents().empty())
- markMessagesAsRead(messageEvents().end() - 1);
+ markMessagesAsRead(messageEvents().back()->id());
+}
+
+bool Room::hasUnreadMessages()
+{
+ return d->unreadMessages;
}
-QString Room::lastReadEvent(User* user)
+QString Room::lastReadEvent(User* user) const
{
return d->lastReadEvent.value(user);
}
+QString Room::readMarkerEventId() const
+{
+ return lastReadEvent(d->connection->user());
+}
+
int Room::notificationCount() const
{
return d->notificationCount;
@@ -441,10 +495,50 @@ void Room::addNewMessageEvents(const 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)
{
d->messageEvents.reserve(d->messageEvents.size() + events.size());
- std::copy(events.begin(), events.end(), std::back_inserter(d->messageEvents));
+
+ Timeline::size_type newUnreadMessages = 0;
+
+ // The first message in the batch defines whose read marker we can
+ // automatically promote any further. 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->messageEvents.empty() ||
+ lastReadEvent(firstWriter) == d->messageEvents.back()->id();
+ Event* firstWriterSeriesEnd = canAutoPromote ? events.front() : nullptr;
+
+ for (auto e: events)
+ {
+ d->messageEvents.push_back(e);
+
+ newUnreadMessages += d->isEventNotable(e);
+ if (firstWriterSeriesEnd)
+ {
+ if (e->senderId() != firstWriter->id())
+ firstWriterSeriesEnd = e;
+ else
+ {
+ setLastReadEvent(firstWriter, firstWriterSeriesEnd->id());
+ firstWriterSeriesEnd = nullptr;
+ }
+ }
+ }
+
+ if( !d->unreadMessages && newUnreadMessages > 0)
+ {
+ d->unreadMessages = true;
+ emit unreadMessagesChanged(this);
+ qDebug() << "Room" << displayName() << ": unread messages";
+ }
}
void Room::addHistoricalMessageEvents(const Events& events)
@@ -535,10 +629,8 @@ void Room::processEphemeralEvent(Event* event)
{
const auto receipts = receiptEvent->receiptsForEvent(eventId);
for( const Receipt& r: receipts )
- {
if (auto m = d->member(r.userId))
promoteReadMarker(m, eventId);
- }
}
}
}
@@ -628,3 +720,19 @@ void Room::Private::updateDisplayname()
if (old_name != displayname)
emit q->displaynameChanged(q);
}
+
+MemberSorter Room::memberSorter() const
+{
+ return MemberSorter(this);
+}
+
+bool MemberSorter::operator()(User *u1, User *u2) const
+{
+ auto n1 = room->roomMembername(u1);
+ auto n2 = room->roomMembername(u2);
+ if (n1[0] == '@')
+ n1.remove(0, 1);
+ if (n2[0] == '@')
+ n2.remove(0, 1);
+ return n1 < n2;
+}
diff --git a/room.h b/room.h
index 637c93f2..7cfd4c00 100644
--- a/room.h
+++ b/room.h
@@ -31,10 +31,12 @@ namespace QMatrixClient
class Event;
class Connection;
class User;
+ class MemberSorter;
class Room: public QObject
{
Q_OBJECT
+ Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerPromoted)
public:
using Timeline = Owning<Events>;
@@ -68,27 +70,31 @@ namespace QMatrixClient
Q_INVOKABLE void updateData(SyncRoomData& data );
Q_INVOKABLE void setJoinState( JoinState state );
- Q_INVOKABLE QString lastReadEvent(User* user);
+ Q_INVOKABLE QString lastReadEvent(User* user) const;
+ QString readMarkerEventId() const;
/**
- * @brief Mark the message at the iterator as read
+ * @brief Mark the event with uptoEventId as read
*
- * Marks the message at the iterator as read; also posts a read
- * receipt to the server either for this message or, if it's from
- * the local user, for the nearest non-local message before.
+ * Finds in the timeline and marks as read the event with
+ * the specified id; also posts a read receipt to the server either
+ * for this message or, if it's from the local user, for
+ * the nearest non-local message before.
*/
- Q_INVOKABLE void markMessagesAsRead(Timeline::const_iterator last);
+ Q_INVOKABLE void markMessagesAsRead(QString uptoEventId);
/**
- * @brief Mark the most recent message in the timeline as read
- *
- * This effectively marks everything in the room as read.
+ * @brief Mark the whole room timeline as read
*/
Q_INVOKABLE void markMessagesAsRead();
+ Q_INVOKABLE bool hasUnreadMessages();
+
Q_INVOKABLE int notificationCount() const;
Q_INVOKABLE void resetNotificationCount();
Q_INVOKABLE int highlightCount() const;
Q_INVOKABLE void resetHighlightCount();
+ MemberSorter memberSorter() const;
+
public slots:
void getPreviousContent();
void userRenamed(User* user, QString oldName);
@@ -115,6 +121,8 @@ namespace QMatrixClient
void highlightCountChanged(Room* room);
void notificationCountChanged(Room* room);
void lastReadEventChanged(User* user);
+ void readMarkerPromoted();
+ void unreadMessagesChanged(Room* room);
protected:
Connection* connection() const;
@@ -123,7 +131,7 @@ namespace QMatrixClient
virtual void processStateEvents(const Events& events);
virtual void processEphemeralEvent(Event* event);
- bool promoteReadMarker(User* user, QString eventId);
+ Timeline::const_iterator promoteReadMarker(User* u, QString eventId);
private:
class Private;
@@ -134,4 +142,22 @@ namespace QMatrixClient
void setLastReadEvent(User* user, QString eventId);
};
+
+ class MemberSorter
+ {
+ public:
+ MemberSorter(const Room* r) : room(r) { }
+
+ bool operator()(User* u1, User* u2) const;
+
+ template <typename ContT>
+ typename ContT::size_type lowerBoundIndex(const ContT& c,
+ typename ContT::value_type v) const
+ {
+ return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin();
+ }
+
+ private:
+ const Room* room;
+ };
}