$user->id, 'cursorable_id' => $entity->id, 'cursorable_type' => get_class($entity), ], [ 'read_at' => Carbon::now(), ] ); } /** * Get the read cursor timestamp for a user on a specific entity. */ public function getCursorReadAt(User $user, $entity): \Carbon\CarbonInterface { $cursor = ReadCursor::where([ 'user_id' => $user->id, 'cursorable_id' => $entity->id, 'cursorable_type' => get_class($entity), ])->first(); // If no cursor exists, default to epoch (all activities are unread) return $cursor ? $cursor->read_at : Carbon::parse('1970-01-01'); } /** * Retrieve all activities for a given entity. */ public function getActivitiesForEntity($entity): array { if ($entity instanceof Dynamic) { $chatId = $entity->chat->id; } elseif ($entity instanceof Ledger) { $chatId = $entity->dynamic->chat->id; } else { return []; } $messages = Message::where('chat_id', $chatId) ->with(['user', 'subject']) ->latest() ->get(); return $messages->map(function ($message) { $messageData = $message->toArray(); $messageData['url'] = $this->getUrlForMessage($message); return $messageData; })->all(); } /** * Get unread activities grouped by active entities (Dynamics, Ledgers) for the given user. */ public function getUnreadEntitiesGrouped(User $user): array { $groupedEntities = []; $participatingDynamics = $user->dynamics()->with('ledgers')->get(); $entities = $participatingDynamics->concat($participatingDynamics->flatMap(fn ($d) => $d->ledgers)); foreach ($entities as $entity) { $readAt = $this->getCursorReadAt($user, $entity); $activities = $this->getActivitiesForEntity($entity); $this->partitionActivities($activities, $readAt, $entity, get_class($entity), $this->getUrlForEntity($entity), $groupedEntities); } return $groupedEntities; } /** * Partition activities into read and unread, and construct the grouped entity metadata. */ private function partitionActivities(array $activities, \Carbon\CarbonInterface $readAt, $entity, string $type, string $url, array &$groupedEntities): void { $alreadyRead = []; $unread = []; foreach ($activities as $act) { if (Carbon::parse($act['created_at'])->gt($readAt)) { $unread[] = $act; } else { $alreadyRead[] = $act; } } if (!empty($unread)) { $context = array_slice($alreadyRead, 0, 2); $groupedEntities[] = [ 'id' => $entity->id, 'name' => $entity->name, 'type' => Str::afterLast($type, '\\'), 'url' => $url, 'unread_count' => count($unread), 'context_activities' => $context, 'new_activities' => $unread, ]; } } private function getUrlForEntity($entity): string { if ($entity instanceof Dynamic) { return route('dynamics.show', $entity->id); } if ($entity instanceof Ledger) { return route('dynamics.ledgers.show', [$entity->dynamic_id, $entity->id]); } return ''; } private function getUrlForMessage(Message $message): string { if ($message->subject) { return $this->getUrlForEntity($message->subject); } if ($message->chat->chatable) { return $this->getUrlForEntity($message->chat->chatable); } return ''; } }