From 682cd0d000377b228b4981d841a61a660ffb6b25 Mon Sep 17 00:00:00 2001 From: sudormant Date: Tue, 12 May 2026 22:36:22 +0200 Subject: [PATCH 01/10] change(conversations): set default shouldSkipLastActivity / skipLastActivity flag for event dispatcher from false to true. This should help with chat state changes (users removed / added, moderators promoted / demoted) from triggering a reordering in the room list in Nextcloud Talk. Signed-off-by: Christian Lorang --- lib/Chat/SystemMessage/Listener.php | 4 +++- lib/Events/ASystemMessageSentEvent.php | 2 +- lib/Events/AttendeesAddedEvent.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 79ea5d4e0c4..fafebc2c7b9 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -476,6 +476,7 @@ protected function fixMimeTypeOfVoiceMessage(ShareCreatedEvent|BeforeDuplicateSh } protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { + $event->shouldSkipLastMessageUpdate = true; foreach ($event->getAttendees() as $attendee) { $this->logger->debug($attendee->getActorType() . ' "' . $attendee->getActorId() . '" added to room "' . $event->getRoom()->getToken() . '"', ['app' => 'spreed-bfp']); if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { @@ -493,6 +494,7 @@ protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { } protected function attendeesRemovedEvent(AttendeesRemovedEvent $event): void { + $event->shouldSkipLastMessageUpdate = true; foreach ($event->getAttendees() as $attendee) { $this->logger->debug($attendee->getActorType() . ' "' . $attendee->getActorId() . '" removed from room "' . $event->getRoom()->getToken() . '"', ['app' => 'spreed-bfp']); if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { @@ -512,7 +514,7 @@ protected function sendSystemMessage( string $message, array $parameters = [], ?Participant $participant = null, - bool $shouldSkipLastMessageUpdate = false, + bool $shouldSkipLastMessageUpdate = true, bool $silent = false, bool $forceSystemAsActor = false, ?int $replyTo = null, diff --git a/lib/Events/ASystemMessageSentEvent.php b/lib/Events/ASystemMessageSentEvent.php index c8ab6420cce..709cf946204 100644 --- a/lib/Events/ASystemMessageSentEvent.php +++ b/lib/Events/ASystemMessageSentEvent.php @@ -19,7 +19,7 @@ public function __construct( ?Participant $participant = null, bool $silent = false, ?IComment $parent = null, - protected bool $skipLastActivityUpdate = false, + protected bool $skipLastActivityUpdate = true, ) { parent::__construct( $room, diff --git a/lib/Events/AttendeesAddedEvent.php b/lib/Events/AttendeesAddedEvent.php index 85cbf621dee..2df49fb7c46 100644 --- a/lib/Events/AttendeesAddedEvent.php +++ b/lib/Events/AttendeesAddedEvent.php @@ -21,7 +21,7 @@ class AttendeesAddedEvent extends AttendeesEvent { public function __construct( Room $room, array $attendees, - private readonly bool $skipLastMessageUpdate = false, + private readonly bool $skipLastMessageUpdate = true, ) { parent::__construct($room, $attendees); } From 6f7f68481b715dd846f071cbc1cd36bf45e01bef Mon Sep 17 00:00:00 2001 From: sudormant Date: Tue, 12 May 2026 22:56:23 +0200 Subject: [PATCH 02/10] feature(api): Add a new field and corresponding functionality for lastMetadaActivity for Rooms and Threads, similar to lastActivity. This also adds a database migration for the oc_talk_rooms and oc_talk_threads tables as well. Functions are introduced and prepared for later use. The use of the field lastActivity to signal when the last real message in a conversation appeared remains unchanged to keep the API stable. The intended use of this feature is to better distinguish between real messages (lastActivity) to notify and bump conversations in the thread list to the top, and other status / metadata related messages (lastMetadataActivity) like room state and participant list changes that shall get synced and be updated in the clients, but not trigger an activity bump of its conversations in the thread list. Signed-off-by: Christian Lorang --- lib/Capabilities.php | 1 + lib/Controller/RoomController.php | 4 ++ lib/Events/ARoomSyncedEvent.php | 1 + lib/Manager.php | 7 ++ .../Version24000Date20260510193300.php | 65 +++++++++++++++++++ lib/Model/SelectHelper.php | 3 + lib/Model/Thread.php | 8 +++ lib/ResponseDefinitions.php | 7 +- lib/Room.php | 8 +++ lib/Service/RoomFormatter.php | 9 +++ 10 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 lib/Migration/Version24000Date20260510193300.php diff --git a/lib/Capabilities.php b/lib/Capabilities.php index a3f3cbbf2e5..5b423e78595 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -42,6 +42,7 @@ class Capabilities implements IPublicCapability { 'multi-room-users', 'favorites', 'last-room-activity', + 'last-metadata-activity', 'no-ping', 'system-messages', 'delete-messages', diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 7370b207987..7acfc5b5a15 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -274,6 +274,10 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, i // Include rooms which had activity return true; } + if ($room->getLastMetadataActivity() && $room->getLastMetadataActivity()->getTimestamp() >= $modifiedSince) { + // Include rooms which had metadata activity + return true; + } // Include rooms where only attendee level things changed, // e.g. favorite, read-marker update, notification setting diff --git a/lib/Events/ARoomSyncedEvent.php b/lib/Events/ARoomSyncedEvent.php index 97cd3a6b56e..cb4844e86af 100644 --- a/lib/Events/ARoomSyncedEvent.php +++ b/lib/Events/ARoomSyncedEvent.php @@ -10,4 +10,5 @@ abstract class ARoomSyncedEvent extends ARoomEvent { public const PROPERTY_LAST_ACTIVITY = 'lastActivity'; + public const PROPERTY_LAST_METADATA_ACTIVITY = 'lastMetadataActivity'; } diff --git a/lib/Manager.php b/lib/Manager.php index 9b33f9f3e27..884a263990c 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -104,6 +104,7 @@ public function createRoomObjectFromData(array $data): Room { 'call_flag' => 0, 'active_since' => null, 'last_activity' => null, + 'last_metadata_activity' => null, 'last_message' => 0, 'comment_id' => null, 'lobby_timer' => null, @@ -135,6 +136,10 @@ public function createRoomObject(array $row): Room { if (!empty($row['last_activity'])) { $lastActivity = $this->timeFactory->getDateTime($row['last_activity']); } + $lastMetadataActivity = null; + if (!empty($row['last_metadata_activity'])) { + $lastMetadataActivity = $this->timeFactory->getDateTime($row['last_metadata_activity']); + } $lobbyTimer = null; if (!empty($row['lobby_timer'])) { @@ -171,6 +176,7 @@ public function createRoomObject(array $row): Room { (int)$row['call_flag'], $activeSince, $lastActivity, + $lastMetadataActivity, (int)$row['last_message'], $lastMessage, $lobbyTimer, @@ -321,6 +327,7 @@ public function getInactiveRooms(\DateTime $inactiveSince): array { $helper->selectRoomsTable($query); $query->from('talk_rooms', 'r') ->andWhere($query->expr()->lte('r.last_activity', $query->createNamedParameter($inactiveSince, IQueryBuilder::PARAM_DATETIME_MUTABLE))) + ->andWhere($query->expr()->lte('r.last_metadata_activity', $query->createNamedParameter($inactiveSince, IQueryBuilder::PARAM_DATETIME_MUTABLE))) ->andWhere($query->expr()->neq('r.read_only', $query->createNamedParameter(Room::READ_ONLY, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->in('r.type', $query->createNamedParameter([Room::TYPE_PUBLIC, Room::TYPE_GROUP], IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($query->expr()->emptyString('remoteServer')) diff --git a/lib/Migration/Version24000Date20260510193300.php b/lib/Migration/Version24000Date20260510193300.php new file mode 100644 index 00000000000..df0e4d981c7 --- /dev/null +++ b/lib/Migration/Version24000Date20260510193300.php @@ -0,0 +1,65 @@ +getTable('talk_rooms'); + + if (!$table->hasColumn('last_metadata_activity')) { + $table->addColumn('last_metadata_activity', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); + + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + #[Override] + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) : void { + $update = $this->connection->getQueryBuilder(); + $update->update('talk_rooms') + ->set('last_metadata_activity', 'last_activity'); + $update->executeStatement(); + } + +} diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index f248e43d5eb..ceb708f747a 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -30,6 +30,7 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi $alias . 'default_permissions', $alias . 'call_flag', $alias . 'last_activity', + $alias . 'last_metadata_activity', $alias . 'last_message', $alias . 'lobby_timer', $alias . 'object_type', @@ -61,6 +62,7 @@ public function selectThreadsTable(IQueryBuilder $query, string $alias = 'th', b ->selectAlias($alias . 'last_message_id', 'th_last_message_id') ->selectAlias($alias . 'num_replies', 'th_num_replies') ->selectAlias($alias . 'last_activity', 'th_last_activity') + ->selectAlias($alias . 'last_metadata_activity', 'th_last_metadata_activity') ->selectAlias($alias . 'name', 'th_name') ->selectAlias($alias . 'id', 'th_id'); return; @@ -71,6 +73,7 @@ public function selectThreadsTable(IQueryBuilder $query, string $alias = 'th', b $alias . 'last_message_id', $alias . 'num_replies', $alias . 'last_activity', + $alias . 'last_metadata_activity', $alias . 'name', ])->selectAlias($alias . 'id', 'th_id'); diff --git a/lib/Model/Thread.php b/lib/Model/Thread.php index b2822c85308..ea7f644a7fa 100644 --- a/lib/Model/Thread.php +++ b/lib/Model/Thread.php @@ -22,7 +22,9 @@ * @method void setNumReplies(int $numReplies) * @method int getNumReplies() * @method void setLastActivity(\DateTime $lastActivity) + * @method void setLastMetadataActivity(\DateTime $lastMetadataActivity) * @method \DateTime|null getLastActivity() + * @method \DateTime|null getLastMetadataActivity() * @method void setName(string $name) * * @psalm-import-type TalkThread from ResponseDefinitions @@ -34,6 +36,7 @@ class Thread extends Entity { protected int $lastMessageId = 0; protected int $numReplies = 0; protected ?\DateTime $lastActivity = null; + protected ?\DateTime $lastMetadataActivity = null; protected string $name = ''; public function __construct() { @@ -41,6 +44,7 @@ public function __construct() { $this->addType('lastMessageId', Types::BIGINT); $this->addType('numReplies', Types::BIGINT); $this->addType('lastActivity', Types::DATETIME); + $this->addType('lastMetadataActivity', Types::DATETIME); $this->addType('name', Types::STRING); } @@ -51,6 +55,7 @@ public static function createFromRow(array $row): Thread { $thread->setLastMessageId((int)$row['last_message_id']); $thread->setNumReplies((int)$row['num_replies']); $thread->setLastActivity(new \DateTime($row['last_activity'])); + $thread->setLastMetadataActivity(new \DateTime($row['last_metadata_activity'])); $thread->setName($row['name']); return $thread; } @@ -68,6 +73,7 @@ public static function fromJson(string $json): Thread { $thread->setLastMessageId((int)$row['last_message_id']); $thread->setNumReplies((int)$row['num_replies']); $thread->setLastActivity(new \DateTime('@' . $row['last_activity'])); + $thread->setLastMetadataActivity(new \DateTime('@' . $row['last_metadata_activity'])); $thread->setName($row['name']); return $thread; } @@ -83,6 +89,7 @@ public function toJson(): string { 'last_message_id' => $this->getLastMessageId(), 'num_replies' => $this->getNumReplies(), 'last_activity' => $this->getLastActivity()?->getTimestamp() ?? 0, + 'last_metadata_activity' => $this->getLastMetadataActivity()?->getTimestamp() ?? 0, 'name' => $this->getName(), ], flags: JSON_THROW_ON_ERROR); } @@ -107,6 +114,7 @@ public function toArray(Room $room): array { 'lastMessageId' => max(0, $this->getLastMessageId()), 'numReplies' => max(0, $this->getNumReplies()), 'lastActivity' => max(0, $this->getLastActivity()?->getTimestamp() ?? 0), + 'lastMetadataActivity' => max(0, $this->getLastMetadataActivity()?->getTimestamp() ?? 0), 'title' => $this->getName(), ]; } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 5c8ed5e407b..f4f99cef108 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -503,8 +503,10 @@ * isCustomAvatar: bool, * // Flag if the conversation is favorited by the user * isFavorite: bool, - * // Timestamp of the last activity in the conversation, in seconds and UTC time zone + * // Timestamp of the last message activity in the conversation, in seconds and UTC time zone * lastActivity: int, + * // Timestamp of the last activity (metadata, not messages) in the conversation, in seconds and UTC time zone + * lastMetadataActivity: int * // ID of the last message read by every user that has read privacy set to public in a room. When the user themself has it set to private the value is `0` (only available with `chat-read-status` capability) * lastCommonReadMessage: int, * // Last message in a conversation if available, otherwise empty. **Note:** Even when given the message will not contain the `parent` or `reactionsSelf` attribute due to performance reasons @@ -725,8 +727,9 @@ * title: string, * // ID of the last message in the thread * lastMessageId: non-negative-int, - * // UNIX timestamp of the last activity in the thread + * // UNIX timestamp of the last message activity in the thread * lastActivity: non-negative-int, + * // UNIX timestamp of the last metadata, not message, activity in the thread * // Number of replies in the thread * numReplies: non-negative-int, * } diff --git a/lib/Room.php b/lib/Room.php index 348c9d993e7..ab55198bc73 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -118,6 +118,7 @@ public function __construct( private int $callFlag, private ?\DateTime $activeSince, private ?\DateTime $lastActivity, + private ?\DateTime $lastMetadataActivity, private int $lastMessageId, private ?IComment $lastMessage, private ?\DateTime $lobbyTimer, @@ -314,6 +315,13 @@ public function getLastActivity(): ?\DateTime { public function setLastActivity(\DateTime $now): void { $this->lastActivity = $now; } + public function getLastMetadataActivity(): ?\DateTime { + return $this->lastMetadataActivity; + } + + public function setLastMetadataActivity(\DateTime $now): void { + $this->lastMetadataActivity = $now; + } public function getLastMessageId(): int { return $this->lastMessageId; diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 45ce1dd296f..e9fa43bc6ea 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -117,6 +117,7 @@ public function formatRoomV4( 'callRecording' => Room::RECORDING_NONE, 'canStartCall' => false, 'lastActivity' => 0, + 'lastMetadataActivity' => 0, 'lastReadMessage' => 0, 'unreadMessages' => 0, 'unreadMention' => false, @@ -172,6 +173,12 @@ public function formatRoomV4( } else { $lastActivity = 0; } + $lastMetadataActivity = $room->getLastMetadataActivity(); + if ($lastMetadataActivity instanceof \DateTimeInterface) { + $lastMetadataActivity = $lastMetadataActivity->getTimestamp(); + } else { + $lastMetadataActivity = 0; + } $this->roomService->validateLobbyTimer($room); @@ -195,6 +202,7 @@ public function formatRoomV4( 'readOnly' => $room->getReadOnly(), 'hasCall' => $room->getActiveSince() instanceof \DateTimeInterface, 'lastActivity' => $lastActivity, + 'lastMetadataActivity' => $lastMetadataActivity, 'callFlag' => $room->getCallFlag(), 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, @@ -227,6 +235,7 @@ public function formatRoomV4( 'callRecording' => $room->getCallRecording(), 'recordingConsent' => $this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL ? $room->getRecordingConsent() : $this->talkConfig->recordingConsentRequired(), 'lastActivity' => $lastActivity, + 'lastMetadataActivity' => $lastMetadataActivity, 'callFlag' => $room->getCallFlag(), 'isFavorite' => $attendee->isFavorite(), 'notificationLevel' => $attendee->getNotificationLevel(), From d028ad8d9091379da34daa9fe734137f7e1aebcc Mon Sep 17 00:00:00 2001 From: sudormant Date: Tue, 12 May 2026 22:59:51 +0200 Subject: [PATCH 03/10] documentation: clarify that sorting by lastActivity in the thread list will still go by last message activity, after introducing lastMetadataActivity, and not sort by last metadata changes in rooms like participant list updates. Signed-off-by: Christian Lorang --- lib/Controller/ThreadController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index 7eba0c644b5..f366e261b51 100644 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -121,7 +121,7 @@ public function getSubscribedThreads(int $limit = 100, int $offset = 0): DataRes } } - // Sort by last activity again + // Sort by last message activity again usort($threads, static function (Thread $a, Thread $b): int { if ($b->getLastActivity() === $a->getLastActivity()) { return $b->getId() <=> $a->getId(); From 739857cf00260cb95b65fbccaf027c19b464fb7c Mon Sep 17 00:00:00 2001 From: sudormant Date: Tue, 12 May 2026 23:04:14 +0200 Subject: [PATCH 04/10] change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages. Signed-off-by: Christian Lorang --- lib/Chat/ChatManager.php | 8 +++---- .../Version24000Date20260510193300.php | 23 +++++++++++++++---- lib/Service/BreakoutRoomService.php | 2 +- lib/Service/RoomService.php | 23 +++++++++++++++++-- lib/Service/ThreadService.php | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index baeccfb65a7..9b609fbb7ec 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -154,7 +154,7 @@ public function addSystemMessage( bool $sendNotifications, ?string $referenceId = null, ?IComment $replyTo = null, - bool $shouldSkipLastMessageUpdate = false, + bool $shouldSkipLastMessageUpdate = true, bool $silent = false, int $threadId = 0, ): IComment { @@ -473,15 +473,15 @@ public function sendMessage( if (!$fromScheduledMessage && $participant instanceof Participant) { $this->participantService->updateLastReadMessage($participant, $messageId); } - // Update last_message if ($comment->getActorType() !== Attendee::ACTOR_BOTS || $comment->getActorId() === Attendee::ACTOR_ID_CHANGELOG || str_starts_with((string)$comment->getActorId(), Attendee::ACTOR_BOT_PREFIX)) { - $this->roomService->setLastMessage($chat, $comment); + $this->roomService->setLastMessageInfo($chat, (int)$comment->getId(), $comment->getCreationDateTime()); + $this->roomService->setLastMetadataActivity($chat, $comment->getCreationDateTime()); $this->unreadCountCache->clear($chat->getId() . '-'); } else { - $this->roomService->setLastActivity($chat, $comment->getCreationDateTime()); + $this->roomService->setLastMetadataActivity($chat, $comment->getCreationDateTime); } $alreadyNotifiedUsers = []; diff --git a/lib/Migration/Version24000Date20260510193300.php b/lib/Migration/Version24000Date20260510193300.php index df0e4d981c7..4c5515a0500 100644 --- a/lib/Migration/Version24000Date20260510193300.php +++ b/lib/Migration/Version24000Date20260510193300.php @@ -42,8 +42,17 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('last_metadata_activity', Types::DATETIME, [ 'notnull' => false, ]); - $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); + $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); + + } + + $table = $schema->getTable('talk_threads'); + if (!$table->hasColumn('last_metadata_activity')) { + $table->addColumn('last_metadata_activity', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); } return $schema; @@ -56,10 +65,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt */ #[Override] public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) : void { - $update = $this->connection->getQueryBuilder(); - $update->update('talk_rooms') - ->set('last_metadata_activity', 'last_activity'); - $update->executeStatement(); + $update = $this->connection->getQueryBuilder(); + $update->update('talk_rooms') + ->set('last_metadata_activity', 'last_activity'); + $update->executeStatement(); + $update = $this->connection->getQueryBuilder(); + $update->update('talk_threads') + ->set('last_metadata_activity', 'last_activity'); + $update->executeStatement(); } } diff --git a/lib/Service/BreakoutRoomService.php b/lib/Service/BreakoutRoomService.php index 552323ad835..4dd596083f5 100644 --- a/lib/Service/BreakoutRoomService.php +++ b/lib/Service/BreakoutRoomService.php @@ -388,7 +388,7 @@ protected function setAssistanceRequest(Room $breakoutRoom, int $status): void { } $this->roomService->setBreakoutRoomStatus($breakoutRoom, $status); - $this->roomService->setLastActivity($breakoutRoom, $this->timeFactory->getDateTime()); + $this->roomService->setLastMetadataActivity($breakoutRoom, $this->timeFactory->getDateTime()); } /** diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index a53a5979310..341754b3d44 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -464,12 +464,12 @@ public function setRecordingConsent(Room $room, int $recordingConsent, bool $all $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') ->set('recording_consent', $update->createNamedParameter($recordingConsent, IQueryBuilder::PARAM_INT)) - ->set('last_activity', $update->createNamedParameter($now, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->set('last_metadata_activity', $update->createNamedParameter($now, IQueryBuilder::PARAM_DATETIME_MUTABLE)) ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); $update->executeStatement(); $room->setRecordingConsent($recordingConsent); - $room->setLastActivity($now); + $room->setLastMetaDataActivity($now); $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT, $recordingConsent, $oldRecordingConsent); $this->dispatcher->dispatchTyped($event); @@ -1233,11 +1233,13 @@ public function setLastMessage(Room $room, IComment $message): void { $update->update('talk_rooms') ->set('last_message', $update->createNamedParameter((int)$message->getId())) ->set('last_activity', $update->createNamedParameter($message->getCreationDateTime(), 'datetime')) + ->set('last_metadata_activity', $update->createNamedParameter($message->getCreationDateTime(), 'datetime')) ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); $update->executeStatement(); $room->setLastMessage($message); $room->setLastActivity($message->getCreationDateTime()); + $room->setLastMetadataActivity($message->getCreationDateTime()); } public function setLastMessageInfo(Room $room, int $messageId, \DateTime $dateTime): void { @@ -1245,11 +1247,13 @@ public function setLastMessageInfo(Room $room, int $messageId, \DateTime $dateTi $update->update('talk_rooms') ->set('last_message', $update->createNamedParameter($messageId)) ->set('last_activity', $update->createNamedParameter($dateTime, 'datetime')) + ->set('last_metadata_activity', $update->createNamedParameter($dateTime, 'datetime')) ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); $update->executeStatement(); $room->setLastMessageId($messageId); $room->setLastActivity($dateTime); + $room->setLastMetadataActivity($dateTime); } /** @@ -1283,6 +1287,16 @@ public function setLastActivity(Room $room, \DateTime $now): void { $room->setLastActivity($now); } + public function setLastMetadataActivity(Room $room, \DateTime $now): void { + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set('last_metadata_activity', $update->createNamedParameter($now, 'datetime')) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + $room->setLastMetadataActivity($now); + } + /** * @psalm-param TalkRoom $host */ @@ -1350,6 +1364,11 @@ public function syncPropertiesFromHostRoom(Room $local, array $host): void { $this->setLastActivity($local, $lastActivity); $changed[] = ARoomSyncedEvent::PROPERTY_LAST_ACTIVITY; } + if (isset($host['lastMetadataActivity']) && $host['lastMetadataActivity'] !== 0 && $host['lastMetadataActivity'] !== ((int)$local->getLastMetadataActivity()?->getTimestamp())) { + $lastMetadataActivity = $this->timeFactory->getDateTime('@' . $host['lastMetadataActivity']); + $this->setLastMetadataActivity($local, $lastMetadataActivity); + $changed[] = ARoomSyncEvent::PROPERTY_LAST_METADATA_ACTIVITY; + } if (isset($host['lobbyState'], $host['lobbyTimer']) && ($host['lobbyState'] !== $local->getLobbyState() || $host['lobbyTimer'] !== ((int)$local->getLobbyTimer()?->getTimestamp()))) { $hostTimer = $host['lobbyTimer'] === 0 ? null : $this->timeFactory->getDateTime('@' . $host['lobbyTimer']); try { diff --git a/lib/Service/ThreadService.php b/lib/Service/ThreadService.php index bc76cab9022..1e73cab27a4 100644 --- a/lib/Service/ThreadService.php +++ b/lib/Service/ThreadService.php @@ -45,7 +45,7 @@ public function createThread(Room $room, int $threadId, string $title): Thread { $thread->setId($threadId); $thread->setName($title); $thread->setRoomId($room->getId()); - $thread->setLastActivity($this->timeFactory->getDateTime()); + $thread->setLastMetadataActivity($this->timeFactory->getDateTime()); $thread = $this->threadMapper->insert($thread); $this->cache->set(self::CACHE_PREFIX . $room->getId() . '/' . $threadId, $thread->toJson(), 60 * 15); From c08c7aa953fa1be8e68139b8cef55c963c49e53d Mon Sep 17 00:00:00 2001 From: sudormant Date: Tue, 12 May 2026 23:08:52 +0200 Subject: [PATCH 05/10] fix(conversations): this fixes the behavior described in #11882: system messages like user added or removed, moderator promoted or demoted trigger an activity bump of the conversation in the list and move it to the top. This fix keeps activity bumps for real chat messages but won't bump the activity anymore for the aforementioned system messages. This fix also incorporates use of the new lastMetadataActivity field to update this information instead of lastActivity accordingly and can be expanded later on to handle other kind of messages differently. No changes client side are needed. Signed-off-by: Christian Lorang --- .../Version24000Date20260510193300.php | 31 +++++++++++++++++++ lib/Service/ParticipantService.php | 18 +++++++++-- lib/Signaling/Listener.php | 2 +- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/Migration/Version24000Date20260510193300.php b/lib/Migration/Version24000Date20260510193300.php index 4c5515a0500..127ee60d952 100644 --- a/lib/Migration/Version24000Date20260510193300.php +++ b/lib/Migration/Version24000Date20260510193300.php @@ -42,12 +42,30 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('last_metadata_activity', Types::DATETIME, [ 'notnull' => false, ]); +<<<<<<< HEAD +<<<<<<< HEAD $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); } $table = $schema->getTable('talk_threads'); + if (!$table->hasColumn('last_metadata_activity')) { + $table->addColumn('last_metadata_activity', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); +======= + $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); +======= + $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); +>>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) + +>>>>>>> 2ed52550f0 (feature(api): Add a new field and corresponding functionality for lastMetadaActivity for Rooms and Threads, similar to lastActivity. This also adds a database migration for the oc_talk_rooms and oc_talk_threads tables as well. Functions are introduced and prepared for later use. The use of the field lastActivity to signal when the last real message in a conversation appeared remains unchanged to keep the API stable. The intended use of this feature is to better distinguish between real messages (lastActivity) to notify and bump conversations in the thread list to the top, and other status / metadata related messages (lastMetadataActivity) like room state and participant list changes that shall get synced and be updated in the clients, but not trigger an activity bump of its conversations in the thread list.) + } + + $table = $schema->getTable('talk_threads'); + if (!$table->hasColumn('last_metadata_activity')) { $table->addColumn('last_metadata_activity', Types::DATETIME, [ 'notnull' => false, @@ -65,6 +83,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt */ #[Override] public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) : void { +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) $update = $this->connection->getQueryBuilder(); $update->update('talk_rooms') ->set('last_metadata_activity', 'last_activity'); @@ -73,6 +95,15 @@ public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $update->update('talk_threads') ->set('last_metadata_activity', 'last_activity'); $update->executeStatement(); +<<<<<<< HEAD +======= + $update = $this->connection->getQueryBuilder(); + $update->update('talk_rooms') + ->set('last_metadata_activity', 'last_activity'); + $update->executeStatement(); +>>>>>>> 2ed52550f0 (feature(api): Add a new field and corresponding functionality for lastMetadaActivity for Rooms and Threads, similar to lastActivity. This also adds a database migration for the oc_talk_rooms and oc_talk_threads tables as well. Functions are introduced and prepared for later use. The use of the field lastActivity to signal when the last real message in a conversation appeared remains unchanged to keep the API stable. The intended use of this feature is to better distinguish between real messages (lastActivity) to notify and bump conversations in the thread list to the top, and other status / metadata related messages (lastMetadataActivity) like room state and participant list changes that shall get synced and be updated in the clients, but not trigger an activity bump of its conversations in the thread list.) +======= +>>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) } } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index b5c6bb52d06..a6e0cce6e53 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -738,9 +738,16 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null $this->dispatcher->dispatchTyped($event); $lastMessage = $event->getLastMessage(); - if ($lastMessage instanceof IComment) { + // TODO: is there any better getter / way to access message type? + $lastMessageType = json_decode($lastMessage->getMessage(), true)['message'] ?? ''; + if ($lastMessage instanceof IComment || !in_array($lastMessageType, ['user_added', 'user_removed', 'moderator_promoted', 'moderator_demoted'], true)) { + // do not update the room message to the status message, so the conversion / thread will not be bumped by activity to the top + // left to do is to update the last message in the preview in the room list, without bumping it up. + } elseif ($lastMessage instanceof IComment) { $this->updateRoomLastMessage($room, $lastMessage); } + $now = $this->timeFactory->getDateTime(); + $room->setLastMetadataActivity($now); return $attendees; } @@ -748,14 +755,16 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null protected function updateRoomLastMessage(Room $room, IComment $message): void { /** @var RoomService $roomService */ $roomService = Server::get(RoomService::class); - $roomService->setLastMessage($room, $message); + $roomService->setLastMessageInfo($room, (int)$message->getId(), $message->getCreationDateTime()); + $now = $this->timeFactory->getDateTime(); + $room->setLastMetadataActivity($now); $lastMessageCache = $this->cacheFactory->createDistributed(CachePrefix::CHAT_LAST_MESSAGE_ID); $lastMessageCache->remove($room->getToken()); $unreadCountCache = $this->cacheFactory->createDistributed(CachePrefix::CHAT_UNREAD_COUNT); $unreadCountCache->clear($room->getId() . '-'); - $event = new SystemMessagesMultipleSentEvent($room, $message); + $event = new SystemMessagesMultipleSentEvent($room, $message, skipLastActivityUpdate: true); $this->dispatcher->dispatchTyped($event); } @@ -1255,6 +1264,9 @@ public function removeUser(Room $room, IUser $user, string $reason): void { return; } + $now = $this->timeFactory->getDateTime(); + $room->setLastMetadataActivity($now); + $attendee = $participant->getAttendee(); $sessions = $this->sessionService->getAllSessionsForAttendee($attendee); diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 898522217b3..c6b7a82e4f6 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -557,7 +557,7 @@ protected function notifySystemMessageSent(ASystemMessageSentEvent $event): void $messageType = $messageDecoded['message'] ?? ''; if ($event->shouldSkipLastActivityUpdate() === true - && !in_array($messageType, ['message_deleted', 'message_edited', 'thread_created', 'thread_renamed'], true) + && !in_array($messageType, ['message_deleted', 'message_edited', 'thread_created', 'thread_renamed', 'user_added', 'user_removed', 'moderator_promoted', 'moderator_demoted'], true) ) { return; } From f0cb33a0ac3fbc2c22f6d865df75271b6ee799dd Mon Sep 17 00:00:00 2001 From: Christian Lorang Date: Sun, 17 May 2026 17:14:19 +0200 Subject: [PATCH 06/10] chore: fix git merge gone wrong Signed-off-by: Christian Lorang --- .../Version24000Date20260510193300.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/lib/Migration/Version24000Date20260510193300.php b/lib/Migration/Version24000Date20260510193300.php index 127ee60d952..4c5515a0500 100644 --- a/lib/Migration/Version24000Date20260510193300.php +++ b/lib/Migration/Version24000Date20260510193300.php @@ -42,30 +42,12 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('last_metadata_activity', Types::DATETIME, [ 'notnull' => false, ]); -<<<<<<< HEAD -<<<<<<< HEAD $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); } $table = $schema->getTable('talk_threads'); - if (!$table->hasColumn('last_metadata_activity')) { - $table->addColumn('last_metadata_activity', Types::DATETIME, [ - 'notnull' => false, - ]); - $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); -======= - $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); -======= - $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); ->>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) - ->>>>>>> 2ed52550f0 (feature(api): Add a new field and corresponding functionality for lastMetadaActivity for Rooms and Threads, similar to lastActivity. This also adds a database migration for the oc_talk_rooms and oc_talk_threads tables as well. Functions are introduced and prepared for later use. The use of the field lastActivity to signal when the last real message in a conversation appeared remains unchanged to keep the API stable. The intended use of this feature is to better distinguish between real messages (lastActivity) to notify and bump conversations in the thread list to the top, and other status / metadata related messages (lastMetadataActivity) like room state and participant list changes that shall get synced and be updated in the clients, but not trigger an activity bump of its conversations in the thread list.) - } - - $table = $schema->getTable('talk_threads'); - if (!$table->hasColumn('last_metadata_activity')) { $table->addColumn('last_metadata_activity', Types::DATETIME, [ 'notnull' => false, @@ -83,10 +65,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt */ #[Override] public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) : void { -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) $update = $this->connection->getQueryBuilder(); $update->update('talk_rooms') ->set('last_metadata_activity', 'last_activity'); @@ -95,15 +73,6 @@ public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $update->update('talk_threads') ->set('last_metadata_activity', 'last_activity'); $update->executeStatement(); -<<<<<<< HEAD -======= - $update = $this->connection->getQueryBuilder(); - $update->update('talk_rooms') - ->set('last_metadata_activity', 'last_activity'); - $update->executeStatement(); ->>>>>>> 2ed52550f0 (feature(api): Add a new field and corresponding functionality for lastMetadaActivity for Rooms and Threads, similar to lastActivity. This also adds a database migration for the oc_talk_rooms and oc_talk_threads tables as well. Functions are introduced and prepared for later use. The use of the field lastActivity to signal when the last real message in a conversation appeared remains unchanged to keep the API stable. The intended use of this feature is to better distinguish between real messages (lastActivity) to notify and bump conversations in the thread list to the top, and other status / metadata related messages (lastMetadataActivity) like room state and participant list changes that shall get synced and be updated in the clients, but not trigger an activity bump of its conversations in the thread list.) -======= ->>>>>>> a798ce9366 (change(conversations): change behavior in all necessary places for Rooms and Threads to use / set lastMetadataActivity instaed of lastActivity where appropriate, i.e. where system messages are signalled and not real chat messages.) } } From 50d3fe3d9305e4cb196107d2973412bd0944d4b5 Mon Sep 17 00:00:00 2001 From: Christian Lorang Date: Sun, 17 May 2026 17:30:49 +0200 Subject: [PATCH 07/10] refactor(conversations): limit no acitvity bumping by metadata changes only to rooms, remove same handling from threads Signed-off-by: Christian Lorang --- lib/Migration/Version24000Date20260510193300.php | 13 ------------- lib/Model/SelectHelper.php | 3 --- lib/Model/Thread.php | 8 -------- lib/Service/ThreadService.php | 1 - 4 files changed, 25 deletions(-) diff --git a/lib/Migration/Version24000Date20260510193300.php b/lib/Migration/Version24000Date20260510193300.php index 4c5515a0500..19a999dcc2a 100644 --- a/lib/Migration/Version24000Date20260510193300.php +++ b/lib/Migration/Version24000Date20260510193300.php @@ -45,15 +45,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['last_metadata_activity'], 'talkroom_lastmetadataactive'); } - - $table = $schema->getTable('talk_threads'); - - if (!$table->hasColumn('last_metadata_activity')) { - $table->addColumn('last_metadata_activity', Types::DATETIME, [ - 'notnull' => false, - ]); - $table->addIndex(['last_metadata_activity'], 'talkthread_lastmetadataactive'); - } return $schema; } @@ -69,10 +60,6 @@ public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $update->update('talk_rooms') ->set('last_metadata_activity', 'last_activity'); $update->executeStatement(); - $update = $this->connection->getQueryBuilder(); - $update->update('talk_threads') - ->set('last_metadata_activity', 'last_activity'); - $update->executeStatement(); } } diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index ceb708f747a..f248e43d5eb 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -30,7 +30,6 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi $alias . 'default_permissions', $alias . 'call_flag', $alias . 'last_activity', - $alias . 'last_metadata_activity', $alias . 'last_message', $alias . 'lobby_timer', $alias . 'object_type', @@ -62,7 +61,6 @@ public function selectThreadsTable(IQueryBuilder $query, string $alias = 'th', b ->selectAlias($alias . 'last_message_id', 'th_last_message_id') ->selectAlias($alias . 'num_replies', 'th_num_replies') ->selectAlias($alias . 'last_activity', 'th_last_activity') - ->selectAlias($alias . 'last_metadata_activity', 'th_last_metadata_activity') ->selectAlias($alias . 'name', 'th_name') ->selectAlias($alias . 'id', 'th_id'); return; @@ -73,7 +71,6 @@ public function selectThreadsTable(IQueryBuilder $query, string $alias = 'th', b $alias . 'last_message_id', $alias . 'num_replies', $alias . 'last_activity', - $alias . 'last_metadata_activity', $alias . 'name', ])->selectAlias($alias . 'id', 'th_id'); diff --git a/lib/Model/Thread.php b/lib/Model/Thread.php index ea7f644a7fa..b2822c85308 100644 --- a/lib/Model/Thread.php +++ b/lib/Model/Thread.php @@ -22,9 +22,7 @@ * @method void setNumReplies(int $numReplies) * @method int getNumReplies() * @method void setLastActivity(\DateTime $lastActivity) - * @method void setLastMetadataActivity(\DateTime $lastMetadataActivity) * @method \DateTime|null getLastActivity() - * @method \DateTime|null getLastMetadataActivity() * @method void setName(string $name) * * @psalm-import-type TalkThread from ResponseDefinitions @@ -36,7 +34,6 @@ class Thread extends Entity { protected int $lastMessageId = 0; protected int $numReplies = 0; protected ?\DateTime $lastActivity = null; - protected ?\DateTime $lastMetadataActivity = null; protected string $name = ''; public function __construct() { @@ -44,7 +41,6 @@ public function __construct() { $this->addType('lastMessageId', Types::BIGINT); $this->addType('numReplies', Types::BIGINT); $this->addType('lastActivity', Types::DATETIME); - $this->addType('lastMetadataActivity', Types::DATETIME); $this->addType('name', Types::STRING); } @@ -55,7 +51,6 @@ public static function createFromRow(array $row): Thread { $thread->setLastMessageId((int)$row['last_message_id']); $thread->setNumReplies((int)$row['num_replies']); $thread->setLastActivity(new \DateTime($row['last_activity'])); - $thread->setLastMetadataActivity(new \DateTime($row['last_metadata_activity'])); $thread->setName($row['name']); return $thread; } @@ -73,7 +68,6 @@ public static function fromJson(string $json): Thread { $thread->setLastMessageId((int)$row['last_message_id']); $thread->setNumReplies((int)$row['num_replies']); $thread->setLastActivity(new \DateTime('@' . $row['last_activity'])); - $thread->setLastMetadataActivity(new \DateTime('@' . $row['last_metadata_activity'])); $thread->setName($row['name']); return $thread; } @@ -89,7 +83,6 @@ public function toJson(): string { 'last_message_id' => $this->getLastMessageId(), 'num_replies' => $this->getNumReplies(), 'last_activity' => $this->getLastActivity()?->getTimestamp() ?? 0, - 'last_metadata_activity' => $this->getLastMetadataActivity()?->getTimestamp() ?? 0, 'name' => $this->getName(), ], flags: JSON_THROW_ON_ERROR); } @@ -114,7 +107,6 @@ public function toArray(Room $room): array { 'lastMessageId' => max(0, $this->getLastMessageId()), 'numReplies' => max(0, $this->getNumReplies()), 'lastActivity' => max(0, $this->getLastActivity()?->getTimestamp() ?? 0), - 'lastMetadataActivity' => max(0, $this->getLastMetadataActivity()?->getTimestamp() ?? 0), 'title' => $this->getName(), ]; } diff --git a/lib/Service/ThreadService.php b/lib/Service/ThreadService.php index 1e73cab27a4..17e83d0d7de 100644 --- a/lib/Service/ThreadService.php +++ b/lib/Service/ThreadService.php @@ -45,7 +45,6 @@ public function createThread(Room $room, int $threadId, string $title): Thread { $thread->setId($threadId); $thread->setName($title); $thread->setRoomId($room->getId()); - $thread->setLastMetadataActivity($this->timeFactory->getDateTime()); $thread = $this->threadMapper->insert($thread); $this->cache->set(self::CACHE_PREFIX . $room->getId() . '/' . $threadId, $thread->toJson(), 60 * 15); From 3ad92d6891dd396ae6488c718a52d8d0cfa626a2 Mon Sep 17 00:00:00 2001 From: Christian Lorang Date: Sun, 17 May 2026 20:56:34 +0200 Subject: [PATCH 08/10] fix(conversations): add getter / setter for shouldSkipLastActivity/MessageUpdate to be compatible with another pull request making this field private Signed-off-by: Christian Lorang --- lib/Chat/SystemMessage/Listener.php | 4 ++-- lib/Events/ASystemMessageSentEvent.php | 10 +++++++++- lib/Events/AttendeesAddedEvent.php | 6 +++++- lib/Events/AttendeesRemovedEvent.php | 10 ++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index fafebc2c7b9..d12b0b946e2 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -476,7 +476,7 @@ protected function fixMimeTypeOfVoiceMessage(ShareCreatedEvent|BeforeDuplicateSh } protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { - $event->shouldSkipLastMessageUpdate = true; + $event->setShouldSkipLastActivityUpdate(true); foreach ($event->getAttendees() as $attendee) { $this->logger->debug($attendee->getActorType() . ' "' . $attendee->getActorId() . '" added to room "' . $event->getRoom()->getToken() . '"', ['app' => 'spreed-bfp']); if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { @@ -494,7 +494,7 @@ protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { } protected function attendeesRemovedEvent(AttendeesRemovedEvent $event): void { - $event->shouldSkipLastMessageUpdate = true; + $event->setShouldSkipLastActivityUpdate(true); foreach ($event->getAttendees() as $attendee) { $this->logger->debug($attendee->getActorType() . ' "' . $attendee->getActorId() . '" removed from room "' . $event->getRoom()->getToken() . '"', ['app' => 'spreed-bfp']); if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { diff --git a/lib/Events/ASystemMessageSentEvent.php b/lib/Events/ASystemMessageSentEvent.php index 709cf946204..5136d42cada 100644 --- a/lib/Events/ASystemMessageSentEvent.php +++ b/lib/Events/ASystemMessageSentEvent.php @@ -19,7 +19,7 @@ public function __construct( ?Participant $participant = null, bool $silent = false, ?IComment $parent = null, - protected bool $skipLastActivityUpdate = true, + private bool $skipLastActivityUpdate = true, ) { parent::__construct( $room, @@ -44,4 +44,12 @@ public function __construct( public function shouldSkipLastActivityUpdate(): bool { return $this->skipLastActivityUpdate; } + + /** + * public setter for shouldSkipLastAcitvityUpdate + * @param bool $pShouldSkipLastActivity + */ + public function setShouldSkipLastActivityUpdate(bool $pShouldSkipLastActivity) { + $this->$skipLastActivityUpdate = $pShouldSkipLastactivity; + } } diff --git a/lib/Events/AttendeesAddedEvent.php b/lib/Events/AttendeesAddedEvent.php index 2df49fb7c46..cca1c8bb48d 100644 --- a/lib/Events/AttendeesAddedEvent.php +++ b/lib/Events/AttendeesAddedEvent.php @@ -21,7 +21,7 @@ class AttendeesAddedEvent extends AttendeesEvent { public function __construct( Room $room, array $attendees, - private readonly bool $skipLastMessageUpdate = true, + private bool $skipLastMessageUpdate = true, ) { parent::__construct($room, $attendees); } @@ -30,6 +30,10 @@ public function shouldSkipLastMessageUpdate(): bool { return $this->skipLastMessageUpdate; } + public function setShouldSkipLastActivityUpdate(bool $pShouldSkipLastActivityUpdate) { + $this->shouldSkipLastMessageUpdate = $pShouldSkipLastActivityUpdate; + } + public function setLastMessage(IComment $lastMessage): void { $this->lastMessage = $lastMessage; } diff --git a/lib/Events/AttendeesRemovedEvent.php b/lib/Events/AttendeesRemovedEvent.php index b36bbc159b1..5eb490b66ac 100644 --- a/lib/Events/AttendeesRemovedEvent.php +++ b/lib/Events/AttendeesRemovedEvent.php @@ -9,4 +9,14 @@ namespace OCA\Talk\Events; class AttendeesRemovedEvent extends AttendeesEvent { + + private bool $shouldSkipLastMessageUpdate = true; + + public function shouldSkipLastMessageUpdate() : bool { + return $this->shouldSkipLastMessageUpdate; + } + + public function setShouldSkipLastActivityUpdate(bool $pShouldSkipLastActivityUpdate) { + $this->shouldSkipLastMessageUpdate = $pShouldSkipLastActivityUpdate; + } } From d8c57dc0a79f8ec00aeacc542ba32574790376c9 Mon Sep 17 00:00:00 2001 From: Christian Lorang Date: Sun, 17 May 2026 21:00:00 +0200 Subject: [PATCH 09/10] fix(conversations): fix handling events that don't have a last message (like room creation) when deciding whether to bump activity Signed-off-by: Christian Lorang --- lib/Service/ParticipantService.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index a6e0cce6e53..0e639d96a4f 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -737,14 +737,17 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null $event = new AttendeesAddedEvent($room, $attendees, true); $this->dispatcher->dispatchTyped($event); - $lastMessage = $event->getLastMessage(); - // TODO: is there any better getter / way to access message type? - $lastMessageType = json_decode($lastMessage->getMessage(), true)['message'] ?? ''; - if ($lastMessage instanceof IComment || !in_array($lastMessageType, ['user_added', 'user_removed', 'moderator_promoted', 'moderator_demoted'], true)) { - // do not update the room message to the status message, so the conversion / thread will not be bumped by activity to the top - // left to do is to update the last message in the preview in the room list, without bumping it up. - } elseif ($lastMessage instanceof IComment) { - $this->updateRoomLastMessage($room, $lastMessage); + // getLastMessage doesn't exist for all event types, e.g. room creation + if (($event->getLastMessage() ?? null) !== null) { + $lastMessage = $event->getLastMessage(); + // TODO: is there any better getter / way to access message type? + $lastMessageType = json_decode($lastMessage->getMessage(), true)['message'] ?? ''; + if ($lastMessage instanceof IComment || !in_array($lastMessageType, ['user_added', 'user_removed', 'moderator_promoted', 'moderator_demoted'], true)) { + // do not update the room message to the status message, so the conversion / thread will not be bumped by activity to the top + // left to do is to update the last message in the preview in the room list, without bumping it up. + } elseif ($lastMessage instanceof IComment) { + $this->updateRoomLastMessage($room, $lastMessage); + } } $now = $this->timeFactory->getDateTime(); $room->setLastMetadataActivity($now); From 6c80235aabaf99b04fad9e891da2db2101754447 Mon Sep 17 00:00:00 2001 From: sudormant Date: Sun, 17 May 2026 21:04:56 +0200 Subject: [PATCH 10/10] Update lib/ResponseDefinitions.php Co-authored-by: Joas Schilling <213943+nickvergessen@users.noreply.github.com> Signed-off-by: sudormant --- lib/ResponseDefinitions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index f4f99cef108..a994625f148 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -730,6 +730,7 @@ * // UNIX timestamp of the last message activity in the thread * lastActivity: non-negative-int, * // UNIX timestamp of the last metadata, not message, activity in the thread + * lastMetadataActivity: int * // Number of replies in the thread * numReplies: non-negative-int, * }