Skip to content

Commit

Permalink
Feat/frontend/ws notification (#213)
Browse files Browse the repository at this point in the history
* [frontend] Modify chatSocket autoConnect
* [frontend] Add WSNotificationToaster to show toast on WS event
* [backend] Remove unused code from chat.gateway.ts
* [frontend/backend] Fix socket event name `left-room` to `leave`
* [frontend] Add SocketProvider to re-connect on auth status change
* [prettier] Add .prettierignore
* [format] Format code with prettier v3.2.3
`npx prettier@latest --write frontend backend --plugin=prettier-plugin-organize-imports`
  • Loading branch information
usatie authored Jan 17, 2024
1 parent bce5153 commit 25d89ab
Show file tree
Hide file tree
Showing 20 changed files with 116 additions and 148 deletions.
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/.git
**/.svn
**/.hg
**/node_modules
**/*.json
2 changes: 1 addition & 1 deletion backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import type { User } from '@prisma/client';
import { CurrentUser } from 'src/common/decorators/current-user.decorator';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { OauthDto } from './dto/oauth.dto';
import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto';
import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto';
import { AuthEntity } from './entity/auth.entity';
import { JwtGuardWithout2FA } from './jwt-auth.guard';
import { OauthDto } from './dto/oauth.dto';

@Controller('auth')
@ApiTags('auth')
Expand Down
6 changes: 3 additions & 3 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import * as bcrypt from 'bcrypt';
import { authenticator } from 'otplib';
import { toFileStream } from 'qrcode';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDto } from 'src/user/dto/create-user.dto';
import { UserEntity } from 'src/user/entities/user.entity';
import { jwtConstants } from './auth.module';
import { OauthDto } from './dto/oauth.dto';
import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto';
import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto';
import { AuthEntity } from './entity/auth.entity';
import { CreateUserDto } from 'src/user/dto/create-user.dto';
import { UserEntity } from 'src/user/entities/user.entity';
import { OauthDto } from './dto/oauth.dto';

@Injectable()
export class AuthService {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/chat/chat.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from 'src/auth/auth.service';
import { UserService } from 'src/user/user.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
import { ChatController } from './chat.controller';
import { ChatService } from './chat.service';

Expand Down
54 changes: 4 additions & 50 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
ConnectedSocket,
MessageBody,
Expand All @@ -7,21 +8,10 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { UserService } from '../user/user.service';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
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;
// from: string;
// to: string;
// userName: string;
// text: string;
//};

@WebSocketGateway({
cors: {
Expand All @@ -31,49 +21,13 @@ import { OnEvent } from '@nestjs/event-emitter';
cookie: true,
})
export class ChatGateway {
constructor(
private readonly chatService: ChatService,
private readonly userService: UserService,
) {}
constructor(private readonly chatService: ChatService) {}

@WebSocketServer()
server: Server;

private logger: Logger = new Logger('ChatGateway');

private userMap = new Map<number, string>();

private getValueToKey = (map, findValue): number | undefined => {
for (const [key, value] of map.entries()) {
if (value == findValue) {
return key;
}
}
return undefined;
};

@SubscribeMessage('privateMessage')
privateMessageToUser(
@MessageBody() data: CreateDirectMessageDto,
@ConnectedSocket() client: Socket,
): void {
this.logger.log('private message received');
this.logger.log(data);

const userId = this.getValueToKey(this.userMap, client.id);
if (userId) {
const userName = 'hoge'; //TODO mapを増やすか、mapのvalueを増やすか user name取得関数実装
this.chatService.createDirectMessage(userId, data);
this.server
.except('block' + userId)
.to(client.id)
.to(this.userMap.get(data.receiverId)) //TODO receiverIdが見つからなかった時のvalidation
.emit('sendToUser', { ...data, senderId: userId, userName }, client.id);
} else {
this.logger.error('No user id was found for socket id');
}
}

@SubscribeMessage('message')
async handleMessage(
@MessageBody() data: CreateMessageDto,
Expand Down Expand Up @@ -107,7 +61,7 @@ export class ChatGateway {

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

Expand Down
2 changes: 1 addition & 1 deletion backend/src/chat/chat.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from 'src/auth/auth.service';
import { UserService } from 'src/user/user.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
import { ChatService } from './chat.service';

describe('ChatService', () => {
Expand Down
17 changes: 2 additions & 15 deletions backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import { WebSocketGateway, WsException } from '@nestjs/websockets';
import { User } from '@prisma/client';
import { Socket } from 'socket.io';
import { AuthService } from 'src/auth/auth.service';
import { UserService } from 'src/user/user.service';
import { BlockEvent } from 'src/common/events/block.event';
import { RoomCreatedEvent } from 'src/common/events/room-created.event';
import { RoomEnteredEvent } from 'src/common/events/room-entered.event';
import { RoomLeftEvent } from 'src/common/events/room-left.event';
import { BlockEvent } from 'src/common/events/block.event';
import { UnblockEvent } from 'src/common/events/unblock.event';
import { Logger } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateDirectMessageDto } from './dto/create-direct-message.dto';
import { UserService } from 'src/user/user.service';
import { CreateMessageDto } from './dto/create-message.dto';

@Injectable()
Expand All @@ -24,8 +22,6 @@ export class ChatService {
private userService: UserService,
) {}

private logger: Logger = new Logger('ChatService');

// Map<User.id, Socket>
private clients = new Map<User['id'], Socket>();
private users = new Map<Socket['id'], User>();
Expand Down Expand Up @@ -156,15 +152,6 @@ export class ChatService {
this.removeClient(client);
}

async createDirectMessage(senderId: number, dto: CreateDirectMessageDto) {
return this.prisma.directMessage.create({
data: {
senderId,
...dto, //TODO receiverIdのvalidationどうする?
},
});
}

private async expectNotBlockedBy(blockerId: number, userId: number) {
const blockedBy = await this.prisma.user
.findFirstOrThrow({
Expand Down
13 changes: 0 additions & 13 deletions backend/src/chat/dto/create-direct-message.dto.ts

This file was deleted.

4 changes: 2 additions & 2 deletions backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { User } from '@prisma/client';
import { hash } from 'bcrypt';
import { BlockEvent } from 'src/common/events/block.event';
import { UnblockEvent } from 'src/common/events/unblock.event';
import * as fs from 'fs';
import * as path from 'path';
import { BlockEvent } from 'src/common/events/block.event';
import { UnblockEvent } from 'src/common/events/unblock.event';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
Expand Down
61 changes: 19 additions & 42 deletions backend/test/chat-gateway.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,43 +487,24 @@ 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();
});
});
let ctx5: Promise<void[]>;
it('setup promises to recv leave event with user id', async () => {
const expectedEvent = {
userId: kickedUser1.id,
roomId: room.id,
};
const promises = [ws1, ws2, ws3, ws4, ws5, ws6].map(
(ws) =>
new Promise<void>((resolve) => {
ws.on('leave', (data) => {
expect(data).toEqual(expectedEvent);
ws.off('leave');
resolve();
});
}),
);

ctx5 = Promise.all(promises);
});

it('user1 kicks kickedUser1', async () => {
Expand All @@ -532,12 +513,8 @@ describe('ChatGateway and ChatController (e2e)', () => {
.expect(204);
});

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

it('kickedUser1 sends message', () => {
Expand Down
4 changes: 2 additions & 2 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
"noFallthroughCasesInSwitch": false,
},
}
2 changes: 2 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";

import AuthProvider from "@/app/lib/client-auth-provider";
import SocketProvider from "@/app/lib/client-socket-provider";
import { getAccessTokenPayload } from "@/app/lib/session";

// components
Expand Down Expand Up @@ -48,6 +49,7 @@ export default async function RootLayout({
<Nav />
{children}
</div>
<SocketProvider />
<Toaster />
</AuthProvider>
</ThemeProvider>
Expand Down
50 changes: 50 additions & 0 deletions frontend/app/lib/client-socket-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";
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 { useAuthContext } from "./client-auth";
import { MessageEvent } from "./dtos";

export default function SocketProvider() {
const { toast } = useToast();
const { currentUser } = useAuthContext();

const showMessageToast = (message: MessageEvent) => {
// TODO: If sender is me, don't show toast
toast({
title: `${message.user.name}`,
description: ` ${message.content}`,
action: (
<ToastAction altText="Open" asChild>
<Link href={`/room/${message.roomId}`}>Open</Link>
</ToastAction>
),
});
};

const showNotificationToast = (data: any) => {
toast({
title: `${data.title}`,
description: ` ${data.description}`,
});
};

useEffect(() => {
chatSocket.connect();
const handler = (event: string, data: any) => {
if (event === "message") {
showMessageToast(data);
} else {
showNotificationToast(data);
}
};
chatSocket.onAny(handler);
return () => {
chatSocket.offAny(handler);
chatSocket.disconnect();
};
}, [currentUser, toast]);
return <></>;
}
1 change: 1 addition & 0 deletions frontend/app/lib/dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type JwtPayload = {
export type MessageEvent = {
user: PublicUserEntity;
content: string;
roomId: number;
};

export type RoomEntity = { id: number; name: string; accessLevel: AccessLevel };
6 changes: 3 additions & 3 deletions frontend/app/room/[id]/message-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ function MessageArea({

// メッセージを取得
useEffect(() => {
socket.connect();

const handleMessage = (message: MessageEvent) => {
if (message.roomId !== roomId) {
return;
}
setMessages((messages) => [...messages, message]);
};

socket.on("message", handleMessage);
return () => {
socket.off("message", handleMessage);
socket.disconnect();
};
}, [roomId]);

Expand Down
Loading

0 comments on commit 25d89ab

Please sign in to comment.