feat: add guest notification center
This commit is contained in:
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\GuestNotificationAudience;
|
||||
use App\Enums\GuestNotificationDeliveryStatus;
|
||||
use App\Enums\GuestNotificationState;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Models\EventMediaAsset;
|
||||
use App\Models\GuestNotification;
|
||||
use App\Models\Photo;
|
||||
use App\Models\PhotoShareLink;
|
||||
use App\Services\Analytics\JoinTokenAnalyticsRecorder;
|
||||
use App\Services\EventJoinTokenService;
|
||||
use App\Services\EventTasksCacheService;
|
||||
use App\Services\GuestNotificationService;
|
||||
use App\Services\Packages\PackageLimitEvaluator;
|
||||
use App\Services\Packages\PackageUsageTracker;
|
||||
use App\Services\Storage\EventStorageManager;
|
||||
@@ -41,6 +46,7 @@ class EventPublicController extends BaseController
|
||||
private readonly PackageLimitEvaluator $packageLimitEvaluator,
|
||||
private readonly PackageUsageTracker $packageUsageTracker,
|
||||
private readonly EventTasksCacheService $eventTasksCache,
|
||||
private readonly GuestNotificationService $guestNotificationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -1596,6 +1602,183 @@ class EventPublicController extends BaseController
|
||||
return array_values(array_unique(array_filter($candidates)));
|
||||
}
|
||||
|
||||
public function notifications(Request $request, string $token)
|
||||
{
|
||||
$result = $this->resolvePublishedEvent($request, $token, ['id', 'tenant_id']);
|
||||
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
$guestIdentifier = $this->resolveNotificationIdentifier($request);
|
||||
$limit = max(1, min(50, (int) $request->integer('limit', 35)));
|
||||
|
||||
$baseQuery = GuestNotification::query()
|
||||
->where('event_id', $event->id)
|
||||
->active()
|
||||
->notExpired()
|
||||
->visibleToGuest($guestIdentifier);
|
||||
|
||||
$notifications = (clone $baseQuery)
|
||||
->with(['receipts' => fn ($query) => $query->where('guest_identifier', $guestIdentifier)])
|
||||
->orderByDesc('priority')
|
||||
->orderByDesc('id')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$unreadCount = (clone $baseQuery)
|
||||
->where(function ($query) use ($guestIdentifier) {
|
||||
$query->whereDoesntHave('receipts', fn ($receipt) => $receipt->where('guest_identifier', $guestIdentifier))
|
||||
->orWhereHas('receipts', fn ($receipt) => $receipt
|
||||
->where('guest_identifier', $guestIdentifier)
|
||||
->where('status', GuestNotificationDeliveryStatus::NEW->value));
|
||||
})
|
||||
->count();
|
||||
|
||||
$data = $notifications->map(fn (GuestNotification $notification) => $this->formatGuestNotification($notification, $guestIdentifier));
|
||||
|
||||
$etag = sha1(json_encode([
|
||||
$event->id,
|
||||
$guestIdentifier,
|
||||
$unreadCount,
|
||||
$notifications->first()?->updated_at?->toAtomString(),
|
||||
]));
|
||||
|
||||
$clientEtags = array_map(fn ($tag) => trim($tag, '"'), $request->getETags());
|
||||
if (in_array($etag, $clientEtags, true)) {
|
||||
return response('', 304)
|
||||
->header('ETag', $etag)
|
||||
->header('Cache-Control', 'no-store')
|
||||
->header('Vary', 'X-Device-Id, Accept-Language');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'unread_count' => $unreadCount,
|
||||
'poll_after_seconds' => 90,
|
||||
],
|
||||
])->header('ETag', $etag)
|
||||
->header('Cache-Control', 'no-store')
|
||||
->header('Vary', 'X-Device-Id, Accept-Language');
|
||||
}
|
||||
|
||||
public function markNotificationRead(Request $request, string $token, GuestNotification $notification)
|
||||
{
|
||||
return $this->handleNotificationAction($request, $token, $notification, 'read');
|
||||
}
|
||||
|
||||
public function dismissNotification(Request $request, string $token, GuestNotification $notification)
|
||||
{
|
||||
return $this->handleNotificationAction($request, $token, $notification, 'dismiss');
|
||||
}
|
||||
|
||||
private function handleNotificationAction(Request $request, string $token, GuestNotification $notification, string $action)
|
||||
{
|
||||
$result = $this->resolvePublishedEvent($request, $token, ['id']);
|
||||
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$event] = $result;
|
||||
|
||||
if ((int) $notification->event_id !== (int) $event->id) {
|
||||
return ApiError::response(
|
||||
'notification_not_found',
|
||||
'Notification not found',
|
||||
'Diese Benachrichtigung gehört nicht zu diesem Event.',
|
||||
Response::HTTP_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
$guestIdentifier = $this->resolveNotificationIdentifier($request);
|
||||
|
||||
if (! $this->notificationVisibleToGuest($notification, $guestIdentifier)) {
|
||||
return ApiError::response(
|
||||
'notification_forbidden',
|
||||
'Notification unavailable',
|
||||
'Diese Nachricht steht nicht zur Verfügung.',
|
||||
Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
$receipt = $action === 'read'
|
||||
? $this->guestNotificationService->markAsRead($notification, $guestIdentifier)
|
||||
: $this->guestNotificationService->dismiss($notification, $guestIdentifier);
|
||||
|
||||
return response()->json([
|
||||
'id' => $notification->id,
|
||||
'status' => $receipt->status->value,
|
||||
'read_at' => $receipt->read_at?->toAtomString(),
|
||||
'dismissed_at' => $receipt->dismissed_at?->toAtomString(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function notificationVisibleToGuest(GuestNotification $notification, string $guestIdentifier): bool
|
||||
{
|
||||
if ($notification->status !== GuestNotificationState::ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($notification->hasExpired()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($notification->audience_scope === GuestNotificationAudience::ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $notification->audience_scope === GuestNotificationAudience::GUEST
|
||||
&& $notification->target_identifier === $guestIdentifier;
|
||||
}
|
||||
|
||||
private function formatGuestNotification(GuestNotification $notification, string $guestIdentifier): array
|
||||
{
|
||||
$receipt = $notification->receipts->firstWhere('guest_identifier', $guestIdentifier);
|
||||
$status = $receipt?->status ?? GuestNotificationDeliveryStatus::NEW;
|
||||
$payload = $notification->payload ?? [];
|
||||
|
||||
$cta = null;
|
||||
if (is_array($payload) && isset($payload['cta']) && is_array($payload['cta'])) {
|
||||
$ctaPayload = $payload['cta'];
|
||||
if (! empty($ctaPayload['label']) && ! empty($ctaPayload['href'])) {
|
||||
$cta = [
|
||||
'label' => (string) $ctaPayload['label'],
|
||||
'href' => (string) $ctaPayload['href'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $notification->id,
|
||||
'type' => $notification->type->value,
|
||||
'title' => $notification->title,
|
||||
'body' => $notification->body,
|
||||
'status' => $status->value,
|
||||
'created_at' => $notification->created_at?->toAtomString(),
|
||||
'read_at' => $receipt?->read_at?->toAtomString(),
|
||||
'dismissed_at' => $receipt?->dismissed_at?->toAtomString(),
|
||||
'cta' => $cta,
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveNotificationIdentifier(Request $request): string
|
||||
{
|
||||
$identifier = $this->determineGuestIdentifier($request);
|
||||
|
||||
if ($identifier) {
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
$deviceId = (string) $request->headers->get('X-Device-Id', '');
|
||||
$deviceId = substr(preg_replace('/[^A-Za-z0-9 _\-]/', '', $deviceId) ?? '', 0, 120);
|
||||
|
||||
return $deviceId !== '' ? $deviceId : 'anonymous';
|
||||
}
|
||||
|
||||
public function stats(Request $request, string $token)
|
||||
{
|
||||
$result = $this->resolvePublishedEvent($request, $token, ['id']);
|
||||
|
||||
Reference in New Issue
Block a user