$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): 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'); } public function createMessage($dynamic, $user, $content, $subject = null) { $chat = $dynamic->getOrCreateChat(); $message = $chat->messages()->create([ 'user_id' => $user ? $user->id : null, 'content' => $content, 'subject_id' => $subject ? $subject->id : null, 'subject_type' => $subject ? get_class($subject) : null, ]); $this->notify($message); return $message; } public function createMutation($ledger, $user, $type, $amount, $description, $status) { $mutation = $ledger->mutations()->create([ 'user_id' => $user->id, 'type' => $type, 'amount' => $amount, 'description' => $description, 'status' => $status, ]); return $mutation; } public function notify(Message $message) { $dynamic = $message->chat->chatable; if ($dynamic instanceof Dynamic) { $participants = $dynamic->participants; foreach ($participants as $participant) { if ($message->user_id !== $participant->id) { $participant->notify(new NewActivityNotification($message)); } } } } /** * Retrieve all activities for a given entity. */ public function getActivitiesForDynamic(Dynamic $dynamic): array { $chat = $dynamic->getOrCreateChat(); if (!$chat) { return []; } $participants = $dynamic->participants()->withPivot('display_name')->get(); $participantsMap = $participants->reduce(function ($acc, $p) { $acc[$p->id] = $p->pivot->display_name ?? $p->name; return $acc; }, []); $messages = Message::where('chat_id', $chat->id) ->with(['user', 'subject']) ->latest() ->get(); return $messages->map(function ($message) use ($participantsMap) { $messageData = $message->toArray(); $messageData['url'] = $this->getUrlForMessage($message); // Resolve placeholders to actual names/display names $messageData['content'] = preg_replace_callback('//', function ($matches) use ($participantsMap) { $userId = $matches[1]; return $participantsMap[$userId] ?? "User #{$userId}"; }, $message->content); return $messageData; })->all(); } /** * Get unread activities grouped by active entities (Dynamics, Ledgers) for the given user. */ public function getUnreadEntitiesGrouped(User $user): array { $groupedDynamics = []; $participatingDynamics = $user->dynamics()->with(['chat', 'ledgers'])->get(); foreach ($participatingDynamics as $dynamic) { $readAt = $this->getCursorReadAt($user, $dynamic); $activities = $this->getActivitiesForDynamic($dynamic); $this->partitionAndGroupActivities($activities, $readAt, $dynamic, $groupedDynamics); } return $groupedDynamics; } /** * Partition activities into read and unread, and construct the grouped entity metadata. */ private function partitionAndGroupActivities(array $activities, CarbonInterface $readAt, Dynamic $dynamic, array &$groupedDynamics): 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); $groupedDynamics[] = [ 'id' => $dynamic->id, 'name' => $dynamic->name, 'url' => route('dynamics.show', $dynamic->uuid), 'unread_count' => count($unread), 'context_activities' => $context, 'new_activities' => array_reverse($unread), ]; } } private function getUrlForEntity($entity): string { if ($entity instanceof Dynamic) { return route('dynamics.show', $entity->uuid); } if ($entity instanceof Ledger) { return route('dynamics.ledgers.show', [$entity->dynamic->uuid, $entity->uuid]); } if ($entity instanceof Message) { $subject = $entity->subject; return $this->getUrlForEntity($subject); } 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 ''; } }