diff --git a/api/src/Controllers/ChatController.php b/api/src/Controllers/ChatController.php index f20b070f..aae984cf 100644 --- a/api/src/Controllers/ChatController.php +++ b/api/src/Controllers/ChatController.php @@ -3,14 +3,13 @@ namespace Matcha\Api\Controllers; use Flight; -use Matcha\Api\Controllers\Notifications\NotificationType; +use Matcha\Api\Enums\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; +use Predis\Client; class ChatController { @@ -89,7 +88,7 @@ public function store(string $username): void $saved = $message->save(); - $redis = new \Predis\Client([ + $redis = new Client([ 'scheme' => 'tcp', 'host' => 'redis', 'port' => 6379, @@ -107,6 +106,11 @@ public function store(string $username): void 'created_at' => $saved->created_at, ], ])); + + Flight::notifier()->notify($user, [ + 'type' => NotificationType::MESSAGE, + 'message' => "Vous avez recus un message de " . ucfirst(Flight::user()->first_name), + ], private: false); } catch (\Exception $e) { error_log('Redis publish failed: ' . $e->getMessage()); } diff --git a/api/src/Controllers/LikeController.php b/api/src/Controllers/LikeController.php index 4d8c0bc7..ba88c5e8 100644 --- a/api/src/Controllers/LikeController.php +++ b/api/src/Controllers/LikeController.php @@ -3,27 +3,37 @@ namespace Matcha\Api\Controllers; use Flight; +use Matcha\Api\Enums\NotificationType; use Matcha\Api\Model\User; class LikeController { public function store(string $username) { - $target = User::find(['username' => $username]); + /** @var User $me */ $me = Flight::user(); + $target = User::find(['username' => $username]); if (is_null($target) || $target->isBlocking($me)) { Flight::json(['message' => 'User not found'], 404); return; } - Flight::user()->like($target); + $me->like($target); - Flight::notifier()->publish("user/" . $target->id . "/notifications", [ - 'type' => 'like', - 'message' => Flight::user()->username . " has liked your profile", - ], private: false); + if ($target->hasMatch($me->username)) { + $notif = [ + 'type' => NotificationType::MATCH, + 'message' => 'Vous avez match avec ' . $me->first_name . " ! Ecrivez lui un message !", + ]; + } else { + $notif = [ + 'type' => NotificationType::LIKE, + 'message' => ucfirst($me->first_name) . ' vous a like !', + ]; + } + Flight::notifier()->notify($target, $notif, private: false); Flight::json([], 201); } @@ -41,7 +51,18 @@ public function destroy(string $username) return; } - Flight::user()->unlike($user); + /** @var User $me */ + $me = Flight::user(); + + if ($user->hasMatch($me->username)) { + Flight::notifier()->notify($user, [ + 'type' => NotificationType::MESSAGE, + 'message' => "Finalement " . ucfirst($me->first_name) . " vous a unlike...", + ], private: false); + } + + $me->unlike($user); + Flight::json([], 203); } } 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/ViewController.php b/api/src/Controllers/ViewController.php index 179c6008..fd2fa1d3 100644 --- a/api/src/Controllers/ViewController.php +++ b/api/src/Controllers/ViewController.php @@ -3,13 +3,16 @@ namespace Matcha\Api\Controllers; use Flight; +use Matcha\Api\Enums\NotificationType; use Matcha\Api\Model\User; class ViewController { public function store(string $username) { - if (Flight::user()->username == $username) { + /** @var User $me */ + $me = Flight::user(); + if ($me->username == $username) { Flight::json([], 422); return; } @@ -26,7 +29,13 @@ public function store(string $username) return; } - Flight::user()->view($user); + $me->view($user); + + Flight::notifier()->notify($user, [ + 'type' => NotificationType::VIEW, + 'message' => ucfirst($me->first_name) . " a consulter votre profile." + ], private: false); + Flight::json([], 204); } } 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 b7f26206..ebc60e39 100644 --- a/api/src/routes.php +++ b/api/src/routes.php @@ -74,8 +74,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(() => {