ledgerrz/app/Services/ActivityService.php
Daan Meijer 98dc8659ba
Some checks failed
linter / quality (push) Failing after 1m5s
tests / ci (8.3) (push) Failing after 48s
tests / ci (8.4) (push) Failing after 1m3s
tests / ci (8.5) (push) Failing after 1m2s
web push notifications
2026-06-21 23:17:33 +02:00

195 lines
5.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\Mutation;
use App\Models\Message;
use App\Models\ReadCursor;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use App\Notifications\NewActivityNotification;
class ActivityService
{
/**
* Update the read cursor for a user on a specific entity.
*/
public function updateCursor(User $user, $entity): void
{
ReadCursor::updateOrCreate(
[
'user_id' => $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');
}
public function createMessage($chat, $user, $content, $subject = null)
{
$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
{
$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', $dynamic->chat->id)
->with(['user', 'subject'])
->latest()
->get();
return $messages->map(function ($message) use ($participantsMap) {
$messageData = $message->toArray();
$messageData['url'] = $this->getUrlForMessage($message);
// Resolve <user:id> placeholders to actual names/display names
$messageData['content'] = preg_replace_callback('/<user:(\d+)>/', 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 getUnreadDynamicsGrouped(User $user): array
{
$groupedDynamics = [];
$participatingDynamics = $user->dynamics()->with('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, \Carbon\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]);
}
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 '';
}
}