Skip to content

Commit

Permalink
Feat/notification of kicks to client (#210)
Browse files Browse the repository at this point in the history
[backend] 
* Add test to ensure client receives notification when kicked
* Notify client on kick

[frontend]
* Add handling of kick notification
  • Loading branch information
lim396 authored Jan 16, 2024
1 parent 88945c6 commit 735cbbc
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 37 deletions.
8 changes: 8 additions & 0 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { ChatService } from './chat.service';
import { CreateDirectMessageDto } from './dto/create-direct-message.dto';
import { CreateMessageDto } from './dto/create-message.dto';
import { MessageEntity } from './entities/message.entity';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
import { OnEvent } from '@nestjs/event-emitter';

//type PrivateMessage = {
// conversationId: string;
Expand Down Expand Up @@ -103,6 +105,12 @@ export class ChatGateway {
);
}

@OnEvent('room.leave', { async: true })
async handleLeave(event: RoomLeftEvent) {
this.server.in(event.roomId.toString()).emit('left-room', event.userId);
await this.chatService.removeUserFromRoom(event);
}

async handleConnection(@ConnectedSocket() client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
await this.chatService.handleConnection(client);
Expand Down
1 change: 0 additions & 1 deletion backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export class ChatService {
await this.addUserToRoom(event.roomId, event.userId);
}

@OnEvent('room.leave', { async: true })
async removeUserFromRoom(event: RoomLeftEvent) {
const client = this.clients.get(event.userId);
if (client) {
Expand Down
47 changes: 47 additions & 0 deletions backend/test/chat-gateway.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,59 @@ describe('ChatGateway and ChatController (e2e)', () => {
.expect(200);
});

let ctx5, ctx6, ctx7, ctx8, ctx9: Promise<void>;
it('setup promises to recv left-room event with user id', async () => {
ctx5 = new Promise<void>((resolve) => {
ws1.on('left-room', (data) => {
expect(data).toEqual(kickedUser1.id);
ws1.off('left-room');
resolve();
});
});
ctx6 = new Promise<void>((resolve) => {
ws2.on('left-room', (data) => {
expect(data).toEqual(kickedUser1.id);
ws2.off('left-room');
resolve();
});
});
ctx7 = new Promise<void>((resolve) => {
ws3.on('left-room', (data) => {
expect(data).toEqual(kickedUser1.id);
ws3.off('left-room');
resolve();
});
});
ctx8 = new Promise<void>((resolve) => {
ws4.on('left-room', (data) => {
expect(data).toEqual(kickedUser1.id);
ws4.off('left-room');
resolve();
});
});
ctx9 = new Promise<void>((resolve) => {
ws6.on('left-room', (data) => {
expect(data).toEqual(kickedUser1.id);
ws6.off('left-room');
resolve();
});
});
});

it('user1 kicks kickedUser1', async () => {
await app
.kickFromRoom(room.id, kickedUser1.id, user1.accessToken)
.expect(204);
});

it('all users (except kickedUser1) should receive left-room event with kickedUser1 id', async () => {
await ctx5;
await ctx6;
await ctx7;
await ctx8;
await ctx9;
});

it('kickedUser1 sends message', () => {
const helloMessage = {
userId: kickedUser1.id,
Expand Down
92 changes: 56 additions & 36 deletions frontend/app/room/[id]/sidebar-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
import { chatSocket as socket } from "@/socket";

function truncateString(str: string | undefined, num: number): string {
if (!str) {
Expand Down Expand Up @@ -50,14 +51,31 @@ export default function SidebarItem({
me: UserOnRoomEntity;
blockingUsers: PublicUserEntity[];
}) {
const router = useRouter();
const [isBlocked, setIsBlocked] = useState(
blockingUsers.some((u: PublicUserEntity) => u.id === user.userId),
);
const [isKicked, setIsKicked] = useState(false);
useEffect(() => {
const handleLeftEvent = (userId: string) => {
if (Number(userId) === me.userId) {
router.push("/");
}
if (Number(userId) === user.userId) {
setIsKicked(true);
}
};
socket.on("left-room", handleLeftEvent);

return () => {
socket.off("left-room", handleLeftEvent);
};
}, [user.userId, me.userId, router]);

const isUserAdmin = user.role === "ADMINISTRATOR";
const isUserOwner = user.role === "OWNER";
const isMeAdminOrOwner = me.role === "ADMINISTRATOR" || me.role === "OWNER";

const router = useRouter();
const openProfile = () => {
if (user.userId === me.userId) {
router.push("/settings");
Expand All @@ -84,40 +102,42 @@ export default function SidebarItem({
: () => updateRoomUser("ADMINISTRATOR", room.id, user.userId);
return (
<>
<ContextMenu>
<ContextMenuTrigger className="flex gap-2 items-center group hover:opacity-60">
<Avatar avatarURL={user.user.avatarURL} />
<span className="text-muted-foreground text-sm whitespace-nowrap group-hover:text-primary">
{truncateString(user.user.name, 15)}
{room.accessLevel !== "DIRECT" && isUserOwner && " 👑"}
{room.accessLevel !== "DIRECT" && isUserAdmin && " 🛡"}
</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuItem onSelect={openProfile}>Go profile</ContextMenuItem>
{user.userId !== me.userId && (
<>
<ContextMenuSeparator />
<ContextMenuItem disabled={isBlocked} onSelect={block}>
Block
</ContextMenuItem>
<ContextMenuItem disabled={!isBlocked} onSelect={unblock}>
Unblock
</ContextMenuItem>
{isMeAdminOrOwner && !isUserOwner && (
<>
<ContextMenuItem disabled={isUserOwner} onSelect={kick}>
Kick
</ContextMenuItem>
<ContextMenuItem onSelect={updateUserRole}>
{isUserAdmin ? "Remove admin role" : "Promote to Admin"}
</ContextMenuItem>
</>
)}
</>
)}
</ContextMenuContent>
</ContextMenu>
{!isKicked && (
<ContextMenu>
<ContextMenuTrigger className="flex gap-2 items-center group hover:opacity-60">
<Avatar avatarURL={user.user.avatarURL} />
<span className="text-muted-foreground text-sm whitespace-nowrap group-hover:text-primary">
{truncateString(user.user.name, 15)}
{room.accessLevel !== "DIRECT" && isUserOwner && " 👑"}
{room.accessLevel !== "DIRECT" && isUserAdmin && " 🛡"}
</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuItem onSelect={openProfile}>Go profile</ContextMenuItem>
{user.userId !== me.userId && (
<>
<ContextMenuSeparator />
<ContextMenuItem disabled={isBlocked} onSelect={block}>
Block
</ContextMenuItem>
<ContextMenuItem disabled={!isBlocked} onSelect={unblock}>
Unblock
</ContextMenuItem>
{isMeAdminOrOwner && !isUserOwner && (
<>
<ContextMenuItem disabled={isUserOwner} onSelect={kick}>
Kick
</ContextMenuItem>
<ContextMenuItem onSelect={updateUserRole}>
{isUserAdmin ? "Remove admin role" : "Promote to Admin"}
</ContextMenuItem>
</>
)}
</>
)}
</ContextMenuContent>
</ContextMenu>
)}
</>
);
}

0 comments on commit 735cbbc

Please sign in to comment.