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/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/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 79ea5d4e0c4..d12b0b946e2 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->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) { @@ -493,6 +494,7 @@ protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { } protected function attendeesRemovedEvent(AttendeesRemovedEvent $event): void { + $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) { @@ -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/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/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(); 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/Events/ASystemMessageSentEvent.php b/lib/Events/ASystemMessageSentEvent.php index c8ab6420cce..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 = false, + 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 85cbf621dee..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 = false, + 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; + } } 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..19a999dcc2a --- /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'], 'talkroom_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/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 5c8ed5e407b..a994625f148 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,10 @@ * 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 + * lastMetadataActivity: int * // 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/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/ParticipantService.php b/lib/Service/ParticipantService.php index b5c6bb52d06..0e639d96a4f 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -737,10 +737,20 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null $event = new AttendeesAddedEvent($room, $attendees, true); $this->dispatcher->dispatchTyped($event); - $lastMessage = $event->getLastMessage(); - if ($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); return $attendees; } @@ -748,14 +758,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 +1267,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/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(), 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..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->setLastActivity($this->timeFactory->getDateTime()); $thread = $this->threadMapper->insert($thread); $this->cache->set(self::CACHE_PREFIX . $room->getId() . '/' . $threadId, $thread->toJson(), 60 * 15); 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; }