From 0acbb414cb5b283fd25fd38cc6e6336f730d9d8a Mon Sep 17 00:00:00 2001 From: ltuffery <123221865+ltuffery@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:56:25 +0200 Subject: [PATCH 1/5] feat notification page --- api/src/Controllers/ChatController.php | 3 - api/src/Controllers/LikeController.php | 5 +- .../Notifications/NotificationsController.php | 56 +---- .../NotificationType.php | 4 +- api/src/Resources/NotificationsResource.php | 2 + api/src/Services/MercurePublisher.php | 28 ++- api/src/routes.php | 2 - frontend/src/App.vue | 12 +- frontend/src/api/notifications.ts | 28 ++- frontend/src/components/chat/PeopleList.vue | 43 ++-- frontend/src/components/layout/NavBar.vue | 4 +- .../notifications/NotificationIcon.vue | 18 ++ frontend/src/store/useNotificationStore.ts | 8 +- frontend/src/views/ChatView.vue | 2 +- frontend/src/views/NotificationsView.vue | 227 +++++++++++++----- 15 files changed, 279 insertions(+), 163 deletions(-) rename api/src/{Controllers/Notifications => Enums}/NotificationType.php (84%) create mode 100644 frontend/src/components/notifications/NotificationIcon.vue diff --git a/api/src/Controllers/ChatController.php b/api/src/Controllers/ChatController.php index f20b070f..8cd28581 100644 --- a/api/src/Controllers/ChatController.php +++ b/api/src/Controllers/ChatController.php @@ -3,14 +3,11 @@ namespace Matcha\Api\Controllers; use Flight; -use Matcha\Api\Controllers\Notifications\NotificationType; use Matcha\Api\Model\Message; -use Matcha\Api\Model\Notification; use Matcha\Api\Model\User; use Matcha\Api\Resources\MatchUserResource; use Matcha\Api\Resources\MessageResource; use Matcha\Api\Validator\Validator; -use PDO; class ChatController { diff --git a/api/src/Controllers/LikeController.php b/api/src/Controllers/LikeController.php index f91c5b7e..ec661d48 100644 --- a/api/src/Controllers/LikeController.php +++ b/api/src/Controllers/LikeController.php @@ -3,6 +3,7 @@ namespace Matcha\Api\Controllers; use Flight; +use Matcha\Api\Enums\NotificationType; use Matcha\Api\Model\User; class LikeController @@ -23,8 +24,8 @@ public function store(string $username) Flight::user()->like($user); - Flight::notifier()->publish("user/" . $user->id . "/notifications", [ - 'type' => 'like', + Flight::notifier()->notify($user, [ + 'type' => NotificationType::LIKE, 'message' => Flight::user()->username . " a aimé ton profil", ], private: false); diff --git a/api/src/Controllers/Notifications/NotificationsController.php b/api/src/Controllers/Notifications/NotificationsController.php index d9047001..8e3f130b 100644 --- a/api/src/Controllers/Notifications/NotificationsController.php +++ b/api/src/Controllers/Notifications/NotificationsController.php @@ -4,19 +4,10 @@ use Flight; use Matcha\Api\Model\Notification; -use Matcha\Api\Model\User; use Matcha\Api\Resources\NotificationsResource; -use Matcha\Api\Validator\Validator; class NotificationsController { - private array $contents = [ - NotificationType::MATCH => '{name} has matched you', - NotificationType::LIKE => '{name} has liked you', - NotificationType::UNLIKE => '{name} has unliked you', - NotificationType::VIEW => '{name} has viewed you', - NotificationType::MESSAGE => '{name} sent you a message', - ]; public function index(): void { @@ -29,43 +20,10 @@ public function index(): void ); } - public function store(string $username): void - { - Validator::required([ - 'type', - ]); - - $me = Flight::user(); - $request = Flight::request(); - - if ($request->data->type == NotificationType::UNLIKE - && !$me->hasMatch($username)) { - Flight::json([ - 'message' => 'Forbidden', - ], 403); - return; - } - - $user = User::find(['username' => $username]); - - if ($request->data->type == NotificationType::LIKE && $me->likedBy($user)) { - $request->data->type = NotificationType::MATCH; - } - - $notification = Notification::to( - $me, - $user, - $request->data->type, - str_replace('{name}', $user->first_name, $this->contents[strtoupper($request->data->type)]), - ); - - Flight::json(new NotificationsResource($notification), 201); - } - - public function viewed(int $id): void + public function viewed(string $id): void { $notification = Notification::find([ - 'id' => $id, + 'id' => intval($id), ]); if (is_null($notification) || $notification->user_id !== Flight::user()->id) { @@ -76,13 +34,19 @@ public function viewed(int $id): void } if ($notification->view) { - Flight::json([], 204); + Flight::json([ + 'success' => true, + 'message' => 'Notification updated successfully.', + ]); return; } $notification->view = true; $notification->save(); - Flight::json([], 204); + Flight::json([ + 'success' => true, + 'message' => 'Notification updated successfully.', + ]); } } diff --git a/api/src/Controllers/Notifications/NotificationType.php b/api/src/Enums/NotificationType.php similarity index 84% rename from api/src/Controllers/Notifications/NotificationType.php rename to api/src/Enums/NotificationType.php index 29460869..c0ebf84e 100644 --- a/api/src/Controllers/Notifications/NotificationType.php +++ b/api/src/Enums/NotificationType.php @@ -1,8 +1,8 @@ $this->model->type, 'data' => [ 'username' => $user->username, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, 'avatar' => $user->getAvatar(), 'content' => $this->model->content, 'view' => $this->model->view, diff --git a/api/src/Services/MercurePublisher.php b/api/src/Services/MercurePublisher.php index 4d9332fd..7697412d 100644 --- a/api/src/Services/MercurePublisher.php +++ b/api/src/Services/MercurePublisher.php @@ -3,6 +3,10 @@ namespace Matcha\Api\Services; use Firebase\JWT\JWT; +use Flight; +use Matcha\Api\Model\Notification; +use Matcha\Api\Model\User; +use Matcha\Api\Resources\NotificationsResource; class MercurePublisher { @@ -10,24 +14,38 @@ class MercurePublisher public function __construct( private string $jwtKey, - ) {} + ) + { + } - public function publish(string $topic, array $data, bool $private = false): void + public function publish(string $topic, array|object $data, bool $private = false): void { $jwt = JWT::encode(['mercure' => ['publish' => ['*']]], $this->jwtKey, 'HS256'); $body = [ 'topic' => $topic, - 'data' => json_encode($data), + 'data' => json_encode($data), ]; if ($private) { $body['private'] = 'on'; } file_get_contents(self::HUB_URL, false, stream_context_create(['http' => [ - 'method' => 'POST', - 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer $jwt", + 'method' => 'POST', + 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer $jwt", 'content' => http_build_query($body), ]])); } + + public function notify(User $user, array $data, bool $private = false): void + { + $me = Flight::user(); + $notification = Notification::to($me, $user, $data['type'], $data['message']); + + $this->publish( + "user/" . $user->id . "/notifications", + new NotificationsResource($notification), + $private + ); + } } \ No newline at end of file diff --git a/api/src/routes.php b/api/src/routes.php index 09935690..4678ed7b 100644 --- a/api/src/routes.php +++ b/api/src/routes.php @@ -71,8 +71,6 @@ Flight::route('POST /view', [ViewController::class, 'store']); - Flight::route('POST /notifications', [NotificationsController::class, 'store']); - Flight::route('POST /report', [ReportController::class, 'store']); }); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index caf76f8c..b9a39bd1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,9 +8,12 @@ import NavBar from '@/components/layout/NavBar.vue' import { useAuthStore } from '@/store/useAuthStore' import { storeToRefs } from 'pinia' import CompleteProfileDialog from '@/components/CompleteProfileDialog.vue' +import { useNotificationStore } from '@/store/useNotificationStore' +import type { Notifications } from '@/api/notifications' const mode = useColorMode() const authStore = useAuthStore() +const notifications = useNotificationStore() const { isAuthenticated, user } = storeToRefs(authStore) let eventSource: EventSource | null = null @@ -21,11 +24,13 @@ function openNotifications(userId: number | string) { `/api/.well-known/mercure?topic=${encodeURIComponent(`user/${userId}/notifications`)}`, ) eventSource.onmessage = event => { - const data = JSON.parse(event.data) - toast.info(data.type, { - description: data.message, + const notif: Notifications = JSON.parse(event.data) + toast.info(notif.type, { + description: notif.data.content, closeButton: true, }) + + notifications.notifications.push(notif) } } @@ -49,6 +54,7 @@ onMounted(async () => { if (!authStore.user && localStorage.getItem('jwt')) { await authStore.fetchMe() + await notifications.fetchNotifications() } }) diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index c1771748..77f634a1 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -1,8 +1,24 @@ import { ApiClient } from '@/api/client' +export interface Notifications { + id: number + type: string + data: { + username: string + first_name: string + last_name: string + avatar: string + content: string + view: boolean + created_at: string + } +} + export const notificationsApi = { async getMyNotifications() { - return ApiClient.get('/users/me/notifications').then(res => res.json()) + return ApiClient.get('/users/me/notifications').then(res => + res.json(), + ) }, async markAsViewed(notificationId: number) { @@ -10,14 +26,4 @@ export const notificationsApi = { `/users/me/notifications/${notificationId}/view`, ).then(res => res.json()) }, - - // Route POST /users/@username/notifications - /** - * @deprecated frontend cannot send notification a another user - */ - async sendToUser(username: string, data: { type: string; content: string }) { - return ApiClient.post(`/users/${username}/notifications`, data).then(res => - res.json(), - ) - }, } diff --git a/frontend/src/components/chat/PeopleList.vue b/frontend/src/components/chat/PeopleList.vue index 067c6673..c01cad6e 100644 --- a/frontend/src/components/chat/PeopleList.vue +++ b/frontend/src/components/chat/PeopleList.vue @@ -60,7 +60,7 @@ const emit = defineEmits<{ const route = useRoute() -const typingStore = useTypingStore(); +const typingStore = useTypingStore() const selectedUser = ref('') const search = ref('') @@ -89,7 +89,7 @@ onMounted(() => {