Skip to content

Commit

Permalink
[frontend] Handling notification (#243)
Browse files Browse the repository at this point in the history
* Add handling of room enter/leave notifications
* Separate logic and ui by cutting logic into custom hooks
* Add handling of mute/updateRole notifications
  • Loading branch information
lim396 authored Feb 6, 2024
1 parent 6e9764e commit 88fe248
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 257 deletions.
46 changes: 44 additions & 2 deletions frontend/app/lib/client-socket-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,61 @@ import { ToastAction } from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
import { chatSocket } from "@/socket";
import Link from "next/link";
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import { useAuthContext } from "./client-auth";
import {
DenyEvent,
EnterRoomEvent,
InviteEvent,
LeaveRoomEvent,
MatchEvent,
MessageEvent,
PublicUserEntity,
} from "./dtos";
import { chatSocket as socket } from "@/socket";
import { useRouter } from "next/navigation";
import { usePathname } from "next/navigation";

export default function SocketProvider() {
const { toast } = useToast();
const { currentUser } = useAuthContext();
const pathName = usePathname();
const router = useRouter();

const handleEnterRoomEvent = useCallback(
(data: EnterRoomEvent) => {
if (pathName === "/room/" + data.roomId.toString()) {
router.refresh();
} else if (
(pathName.startsWith("/room/") || pathName === "/room") &&
currentUser?.id === data.userId
) {
router.refresh();
}
},
[currentUser, pathName, router],
);

const handleLeaveRoomEvent = useCallback(
(data: LeaveRoomEvent) => {
if (
pathName === "/room/" + data.roomId.toString() &&
data.userId === currentUser?.id
) {
router.push("/room");
router.refresh();
} else if (pathName === "/room/" + data.roomId.toString()) {
router.refresh();
} else if (
(pathName.startsWith("/room/") || pathName === "/room") &&
currentUser?.id === data.userId
) {
router.refresh();
}
},
[currentUser, pathName, router],
);

const MatchPong = (data: MatchEvent) => {
router.push(`/pong/${data.roomId}?mode=player`);
};
Expand Down Expand Up @@ -99,6 +137,10 @@ export default function SocketProvider() {
const handler = (event: string, data: any) => {
if (event === "message") {
showMessageToast(data);
} else if (event === "enter-room") {
handleEnterRoomEvent(data);
} else if (event === "leave") {
handleLeaveRoomEvent(data);
} else if (event === "invite-pong") {
showInvitePongToast(data);
} else if (event === "invite-cancel-pong") {
Expand All @@ -118,6 +160,6 @@ export default function SocketProvider() {
chatSocket.offAny(handler);
chatSocket.disconnect();
};
}, [currentUser, toast]);
}, [currentUser, handleEnterRoomEvent, handleLeaveRoomEvent, toast]);
return <></>;
}
10 changes: 10 additions & 0 deletions frontend/app/lib/dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export type JwtPayload = {
isTwoFactorAuthenticated: boolean;
};

export type EnterRoomEvent = {
roomId: number;
userId: number;
};

export type LeaveRoomEvent = {
roomId: number;
userId: number;
};

export type MessageEvent = {
user: PublicUserEntity;
content: string;
Expand Down
55 changes: 55 additions & 0 deletions frontend/app/lib/hooks/useBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { blockUser, unblockUser } from "@/app/lib/actions";
import type { PublicUserEntity } from "@/app/lib/dtos";
import { toast } from "@/components/ui/use-toast";
import { useCallback, useState } from "react";

const showBlockErrorToast = () => {
toast({
title: "Error",
description: "failed to block user",
});
};

const showUnblockErrorToast = () => {
toast({
title: "Error",
description: "failed to unblock user",
});
};

export function useBlock(userId: number, blockingUsers: PublicUserEntity[]) {
const [blockPending, setBlockPending] = useState(false);
const [isBlocked, setIsBlocked] = useState(
blockingUsers.some((u) => u.id === userId),
);
const block = useCallback(async () => {
setBlockPending(true);
const res = await blockUser(userId);
if (res === "Success") {
setIsBlocked(true);
setBlockPending(false);
} else {
showBlockErrorToast();
setBlockPending(false);
}
}, [userId]);
const unblock = useCallback(async () => {
setBlockPending(true);
const res = await unblockUser(userId);
if (res === "Success") {
setIsBlocked(false);
setBlockPending(false);
} else {
showUnblockErrorToast();
setBlockPending(false);
}
}, [userId]);
return {
blockPending,
isBlocked,
block,
unblock,
};
}
29 changes: 29 additions & 0 deletions frontend/app/lib/hooks/useInviteToGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { useCallback, useState } from "react";
import { chatSocket as socket } from "@/socket";

export const useInviteToGame = (userId: number) => {
const [isInvitingToGame, setIsInvitingToGame] = useState(false);
const [invitePending, setInvitePending] = useState(false);

const inviteToGame = useCallback(async () => {
setInvitePending(true);
await socket.emit("invite-pong", { userId: userId });
setIsInvitingToGame(true);
setInvitePending(false);
}, [userId]);
const cancelInviteToGame = useCallback(async () => {
setInvitePending(true);
await socket.emit("invite-cancel-pong", { userId: userId });
setIsInvitingToGame(false);
setInvitePending(false);
}, [userId]);

return {
invitePending,
isInvitingToGame,
inviteToGame,
cancelInviteToGame,
};
};
32 changes: 32 additions & 0 deletions frontend/app/lib/hooks/useKick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { kickUserOnRoom } from "@/app/lib/actions";
import type { UserOnRoomEntity } from "@/app/lib/dtos";
import { toast } from "@/components/ui/use-toast";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { chatSocket as socket } from "@/socket";

const showKickErrorToast = () => {
toast({
title: "Error",
description: "failed to kick user",
});
};

export function useKick(roomId: number, userId: number) {
const router = useRouter();
const [kickPending, setKickPending] = useState(false);

const kick = useCallback(async () => {
setKickPending(true);
const res = await kickUserOnRoom(roomId, userId);
if (res === "Success") {
setKickPending(false);
} else {
showKickErrorToast();
setKickPending(false);
}
}, [roomId, userId]);
return { kickPending, kick };
}
84 changes: 84 additions & 0 deletions frontend/app/lib/hooks/useMute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { muteUser, unmuteUser } from "@/app/lib/actions";
import type { PublicUserEntity } from "@/app/lib/dtos";
import { toast } from "@/components/ui/use-toast";
import { useCallback, useEffect, useState } from "react";
import { chatSocket as socket } from "@/socket";

interface MuteEvent {
userId: number;
roomId: number;
}

const showMuteErrorToast = () => {
toast({
title: "Error",
description: "failed to mute user",
});
};

const showUnmuteErrorToast = () => {
toast({
title: "Error",
description: "failed to unmute user",
});
};

export function useMute(
roomId: number,
userId: number,
mutedUsers: PublicUserEntity[],
) {
const [mutePending, setMutePending] = useState(false);
const [isMuted, setIsMuted] = useState(
mutedUsers.some((u: PublicUserEntity) => u.id === userId),
);

useEffect(() => {
const handleMuteEvent = (data: MuteEvent) => {
if (Number(data.userId) === userId && data.roomId === roomId) {
setIsMuted(true);
}
};
const handleUnmuteEvent = (data: MuteEvent) => {
if (Number(data.userId) === userId && data.roomId === roomId) {
setIsMuted(false);
}
};
socket.on("mute", handleMuteEvent);
socket.on("unmute", handleUnmuteEvent);

return () => {
socket.off("mute", handleMuteEvent);
socket.off("unmute", handleUnmuteEvent);
};
}, [roomId, userId]);

const mute = useCallback(
async (duration?: number) => {
setMutePending(true);
const res = await muteUser(roomId, userId, duration);
if (res === "Success") {
setIsMuted(true);
setMutePending(false);
} else {
showMuteErrorToast();
setMutePending(false);
}
},
[roomId, userId],
);
const unmute = useCallback(async () => {
setMutePending(true);
const res = await unmuteUser(roomId, userId);
if (res === "Success") {
setIsMuted(false);
setMutePending(false);
} else {
showUnmuteErrorToast();
setMutePending(false);
}
}, [roomId, userId]);
return { mutePending, isMuted, mute, unmute };
}
69 changes: 69 additions & 0 deletions frontend/app/lib/hooks/useUpdateRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { updateRoomUser } from "@/app/lib/actions";
import type { UserOnRoomEntity } from "@/app/lib/dtos";
import { toast } from "@/components/ui/use-toast";
import { useCallback, useEffect, useState } from "react";
import { chatSocket as socket } from "@/socket";

type Role = "ADMINISTRATOR" | "MEMBER";

interface UpdateRoleEvent {
roomId: number;
userId: number;
role: Role;
}

const showUpdateRoleErrorToast = () => {
toast({
title: "Error",
description: "failed to update user role",
});
};

export function useUpdateRole(
roomId: number,
me: UserOnRoomEntity,
user: UserOnRoomEntity,
) {
const [updateRolePending, setUpdateRolePending] = useState(false);
const [userRole, setUserRole] = useState(user.role);
const [meRole, setMeRole] = useState(me.role);
const isUserAdmin = userRole === "ADMINISTRATOR";
useEffect(() => {
const handleUpdateRoleEvent = (data: UpdateRoleEvent) => {
if (data.roomId === roomId && data.userId === user.userId) {
setUserRole(data.role);
} else if (data.roomId === roomId && data.userId === me.userId) {
setMeRole(data.role);
}
};
socket.on("update-role", handleUpdateRoleEvent);

return () => {
socket.off("update-role", handleUpdateRoleEvent);
};
}, [roomId, me.userId, user.userId]);

const updateUserRole = useCallback(async () => {
setUpdateRolePending(true);
const res = await updateRoomUser(
isUserAdmin ? "MEMBER" : "ADMINISTRATOR",
roomId,
user.userId,
);
if (res !== "Success") {
showUpdateRoleErrorToast();
setUpdateRolePending(false);
} else {
setUserRole(isUserAdmin ? "MEMBER" : "ADMINISTRATOR");
setUpdateRolePending(false);
}
}, [roomId, user.userId, isUserAdmin]);
return {
updateRolePending,
meRole,
userRole,
updateUserRole,
};
}
Loading

0 comments on commit 88fe248

Please sign in to comment.