-
Notifications
You must be signed in to change notification settings - Fork 0
Ncto 155 make notifications system #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
43906a0
3850d31
d3d7ed1
2091eb5
90a2826
2276177
483d4e5
a4c93d2
c6cf3cd
14b2266
b179283
9019972
d25454d
df060f3
58d34b3
465c254
6f7ade7
b9328ea
31b971b
5c88326
a4ff490
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,15 @@ | ||||||
| services: | ||||||
|
||||||
| frontend: | ||||||
| image: oven/bun:latest | ||||||
|
||||||
| image: oven/bun:latest | |
| image: oven/bun:1.1.34 |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this, please declare a rule in the package.json file instead of writing commands to do the work so that we can also run it outside of Docker
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will change depending of your choice 👍
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,5 @@ | |
| /* eslint-disable */ | ||
| export type AuthResponseDto = { | ||
| access_token: string; | ||
| refresh_token: string; | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /* generated using openapi-typescript-codegen -- do not edit */ | ||
| /* istanbul ignore file */ | ||
| /* tslint:disable */ | ||
| /* eslint-disable */ | ||
| export type NotificationTestDto = { | ||
| /** | ||
| * The title of the test notification | ||
| */ | ||
| title: string; | ||
| /** | ||
| * The message content of the test notification | ||
| */ | ||
| message: string; | ||
| /** | ||
| * The type of the notification (friend request, message, etc.) | ||
| */ | ||
| type: string; | ||
| }; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* generated using openapi-typescript-codegen -- do not edit */ | ||
| /* istanbul ignore file */ | ||
| /* tslint:disable */ | ||
| /* eslint-disable */ | ||
| import type { NotificationTestDto } from '../models/NotificationTestDto'; | ||
| import type { CancelablePromise } from '../core/CancelablePromise'; | ||
| import { OpenAPI } from '../core/OpenAPI'; | ||
| import { request as __request } from '../core/request'; | ||
| export class NotificationsService { | ||
| /** | ||
| * Send a test notification to the current user | ||
| * @param requestBody | ||
| * @returns any Notification created and sent | ||
| * @throws ApiError | ||
| */ | ||
| public static notificationsControllerSendTestNotification( | ||
| requestBody: NotificationTestDto, | ||
| ): CancelablePromise<any> { | ||
| return __request(OpenAPI, { | ||
| method: 'POST', | ||
| url: '/notifications/test', | ||
| body: requestBody, | ||
| mediaType: 'application/json', | ||
| }); | ||
| } | ||
| /** | ||
| * set one notification as read | ||
| * @param id | ||
| * @returns any Notification marked as read | ||
| * @throws ApiError | ||
| */ | ||
| public static notificationsControllerMarkAsRead( | ||
| id: string, | ||
| ): CancelablePromise<any> { | ||
| return __request(OpenAPI, { | ||
| method: 'PATCH', | ||
| url: '/notifications/{id}/read', | ||
| path: { | ||
| 'id': id, | ||
| }, | ||
| }); | ||
| } | ||
| /** | ||
| * set notifications as read | ||
| * @returns any All notifications as read | ||
| * @throws ApiError | ||
| */ | ||
| public static notificationsControllerMarkAllAsRead(): CancelablePromise<any> { | ||
| return __request(OpenAPI, { | ||
| method: 'PATCH', | ||
| url: '/notifications/read-all', | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { Badge, IconButton, styled } from "@mui/material"; | ||
| import { JSX, useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from "react"; | ||
| import InfoBoxIcon from "@assets/infoBox.svg?react"; | ||
| import { useUser } from "@providers/UserProvider"; | ||
| import { LocalStorageManager } from "@utils/LocalStorageManager"; | ||
| import { NotificationMenu } from "./NotificationMenu"; | ||
| import { NotificationItem } from "./types"; | ||
| import { NotificationsService } from "@api/services/NotificationsService"; | ||
|
|
||
| const NOTIFICATION_SOCKET_PATH = "/socket/notifications"; | ||
| const MAX_NOTIFICATIONS = 50; | ||
|
|
||
| type NotificationWsMessage = | ||
| | { type: "notification"; payload: NotificationItem } | ||
| | { type: "notifications:init"; payload: NotificationItem[] }; | ||
|
|
||
| const NotificationButton = styled(IconButton)(({ theme }) => ({ | ||
| margin: theme.spacing(2), | ||
| color: theme.palette.text.primary, | ||
| })); | ||
|
|
||
| const InfoIcon = styled(InfoBoxIcon)(({ theme }) => ({ | ||
| width: 24, | ||
| height: 24, | ||
| color: theme.palette.text.primary, | ||
| })); | ||
|
|
||
| const mergeNotification = ( | ||
| previous: NotificationItem[], | ||
| notification: NotificationItem, | ||
| ): NotificationItem[] => { | ||
| const withoutCurrent = previous.filter((item) => item.id !== notification.id); | ||
| return [notification, ...withoutCurrent].slice(0, MAX_NOTIFICATIONS); | ||
| }; | ||
|
|
||
| export const NotificationBox = (): JSX.Element => { | ||
| const { user } = useUser(); | ||
| const userId = user?.id; | ||
| const token = LocalStorageManager.getToken(); | ||
| const [notifications, setNotifications] = useState<NotificationItem[]>([]); | ||
| const [showMenu, setShowMenu] = useState(false); | ||
| const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>(undefined); | ||
| const socketRef = useRef<WebSocket | null>(null); | ||
| const backendUrl = (import.meta.env.VITE_BACKEND_URL || "http://localhost:3000").trim(); | ||
|
||
|
|
||
| const unreadCount = useMemo( | ||
| () => notifications.reduce((count, notification) => count + (notification.read ? 0 : 1), 0), | ||
| [notifications], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (!userId || !token || !backendUrl) { | ||
| if (socketRef.current) { | ||
| socketRef.current.close(); | ||
| socketRef.current = null; | ||
| } | ||
| setNotifications([]); | ||
| return; | ||
| } | ||
|
|
||
| const wsBase = backendUrl.replace(/^http/i, "ws").replace(/\/$/, ""); | ||
| const socketUrl = `${wsBase}${NOTIFICATION_SOCKET_PATH}?token=${encodeURIComponent(token)}`; | ||
| const socket = new WebSocket(socketUrl); | ||
|
|
||
| socketRef.current = socket; | ||
|
|
||
Notgoyome marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| socket.onmessage = (event: MessageEvent<string>) => { | ||
| try { | ||
| const message = JSON.parse(event.data) as NotificationWsMessage; | ||
|
|
||
| if (message.type === "notifications:init") { | ||
| setNotifications(message.payload.slice(0, MAX_NOTIFICATIONS)); | ||
| return; | ||
| } | ||
|
|
||
| if (message.type === "notification") { | ||
| setNotifications((previous) => mergeNotification(previous, message.payload)); | ||
| } | ||
| } catch { | ||
| // eslint | ||
Notgoyome marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| }; | ||
|
|
||
| return () => { | ||
| socket.close(); | ||
| socketRef.current = null; | ||
| }; | ||
| }, [userId, token, backendUrl]); | ||
|
|
||
| const handleClick = useCallback((event: MouseEvent<HTMLElement>) => { | ||
| setAnchorEl(event.currentTarget); | ||
| setShowMenu((previous) => { | ||
| const next = !previous; | ||
|
|
||
| return next; | ||
| }); | ||
| }, []); | ||
|
|
||
| const handleClose = useCallback(() => setShowMenu(false), []); | ||
|
|
||
| const handleMarkAsRead = useCallback((notificationId: string) => { | ||
| setNotifications((current) => | ||
| current.map((notification) => | ||
| notification.id === notificationId ? { ...notification, read: true } : notification, | ||
| ), | ||
| ); | ||
| NotificationsService.notificationsControllerMarkAsRead(notificationId); | ||
Notgoyome marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, []); | ||
|
|
||
| return ( | ||
| <> | ||
| <NotificationButton onClick={handleClick} disabled={!userId}> | ||
| <Badge badgeContent={unreadCount} color="error"> | ||
| <InfoIcon /> | ||
| </Badge> | ||
| </NotificationButton> | ||
| {showMenu && ( | ||
| <NotificationMenu | ||
| anchorEl={anchorEl} | ||
| open={showMenu} | ||
| onClose={handleClose} | ||
| notifications={notifications} | ||
| onMarkAsRead={handleMarkAsRead} | ||
| /> | ||
| )} | ||
| </> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.