Added way too much stuff

This commit is contained in:
Logan Hunt 2022-04-01 16:04:00 -06:00
parent 1108970a6a
commit dbb9eea25f
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
13 changed files with 219 additions and 34 deletions

View File

@ -66,11 +66,23 @@ body {
box-shadow: rgb( 0, 0, 0, 0.6) 6px 45px 45px -12px; box-shadow: rgb( 0, 0, 0, 0.6) 6px 45px 45px -12px;
} }
.chat { .chat-container {
border-bottom: 1px solid #d65d0e; border-bottom: 1px solid #d65d0e;
height: 200px; height: 200px;
overflow-y: scroll;
padding-bottom: 12px; padding-bottom: 12px;
display: flex;
flex-direction: row;
min-height: min-content;
}
.chat-box {
flex: 3;
overflow-y: scroll;
}
.chat-connections {
flex: 1;
overflow-y: scroll;
} }
* { * {
@ -117,3 +129,4 @@ body {
border-radius: 8px; border-radius: 8px;
display: inline-block; display: inline-block;
} }

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useContext } from 'react'; import { useEffect, useState, useContext } from 'react';
import { ApiContext } from '../../utils/api_context'; import { ApiContext } from '../../utils/api_context';
import { useMessages } from '../../utils/use_messages'; import { useChat } from '../../utils/use_chat';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { generateGruvboxFromString } from '../../utils/generate_gruvbox'; import { generateGruvboxFromString } from '../../utils/generate_gruvbox';
@ -11,7 +11,7 @@ import { generateGruvboxFromString } from '../../utils/generate_gruvbox';
export const ChatRoom = () => { export const ChatRoom = () => {
const { id } = useParams(); const { id } = useParams();
const [chatRoom, setChatRoom] = useState(''); const [chatRoom, setChatRoom] = useState('');
const [messages, sendMessage] = useMessages(chatRoom); const [connections, messages, sendMessage] = useChat(chatRoom);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [color, setColor] = useState(generateGruvboxFromString('placeholder')); const [color, setColor] = useState(generateGruvboxFromString('placeholder'));
const [user, setUser] = useState({}); const [user, setUser] = useState({});
@ -56,8 +56,8 @@ export const ChatRoom = () => {
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<h2>{chatRoom?.name || `Chat Room ${chatRoom?.id}`}</h2> <h2>{chatRoom?.name || `Chat Room ${chatRoom?.id}`}</h2>
</div> </div>
<div id="chat" className="chat"> <div className="chat-container">
<p>Welcome!</p> <div id="chat" className="chat-box">
{messages.map((message) => ( {messages.map((message) => (
<div key={message.id} style={{ lineBreak: 'normal' }}> <div key={message.id} style={{ lineBreak: 'normal' }}>
<span style={{ color: generateGruvboxFromString(message.userName) }}>{message.userName}: </span> <span style={{ color: generateGruvboxFromString(message.userName) }}>{message.userName}: </span>
@ -65,6 +65,18 @@ export const ChatRoom = () => {
</div> </div>
))} ))}
</div> </div>
<div className="chat-connections">
<h1>Connected Users ({connections.length})</h1>
<hr />
<ul>
{connections.map((user) => (
<li key={user.id} style={{ color: generateGruvboxFromString(user.userName) }}>
{user.userName}
</li>
))}
</ul>
</div>
</div>
<div> <div>
<textarea <textarea
placeholder={'Message'} placeholder={'Message'}

View File

@ -1,4 +1,5 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { ApiContext } from '../../utils/api_context'; import { ApiContext } from '../../utils/api_context';
import { AuthContext } from '../../utils/auth_context'; import { AuthContext } from '../../utils/auth_context';
@ -31,6 +32,8 @@ export const Home = () => {
const joinable = await api.get(`/chat_rooms/${id}/joinable?lat=${userPosition.lat}&lng=${userPosition.lng}`); const joinable = await api.get(`/chat_rooms/${id}/joinable?lat=${userPosition.lat}&lng=${userPosition.lng}`);
if (joinable) { if (joinable) {
navigate(`/rooms/${id}`); navigate(`/rooms/${id}`);
} else {
toast.error('Room is not joinable');
} }
}; };

View File

@ -2,6 +2,7 @@ import { useLeafletContext } from '@react-leaflet/core';
import L from 'leaflet'; import L from 'leaflet';
import markerIconPng from 'leaflet/dist/images/marker-icon.png'; import markerIconPng from 'leaflet/dist/images/marker-icon.png';
import { useEffect, useContext, useState } from 'react'; import { useEffect, useContext, useState } from 'react';
import toast from 'react-hot-toast';
import { ApiContext } from '../../utils/api_context'; import { ApiContext } from '../../utils/api_context';
const userPositionBubble = { const userPositionBubble = {
@ -162,7 +163,12 @@ export const Geoman = ({ user, userPos, joinRoom }) => {
longitude, longitude,
radius: shape.layer.getRadius(), radius: shape.layer.getRadius(),
}); });
console.log(chatRoom); if (chatRoom.error) {
toast.error(chatRoom.error);
} else if (chatRoom.id) {
toast.success('Chat room created');
}
reRender(); reRender();
} }
}); });

View File

@ -2,9 +2,11 @@ import { useState, useContext, useEffect, useRef } from 'react';
import { AuthContext } from './auth_context'; import { AuthContext } from './auth_context';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
export const useMessages = (chatRoom) => { export const useChat = (chatRoom) => {
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [connections, setConnections] = useState([]);
const messageRef = useRef([]); const messageRef = useRef([]);
const connectionsRef = useRef([]);
const [socket, setSocket] = useState({}); const [socket, setSocket] = useState({});
const [authToken] = useContext(AuthContext); const [authToken] = useContext(AuthContext);
@ -25,6 +27,10 @@ export const useMessages = (chatRoom) => {
messageRef.current.push(message); messageRef.current.push(message);
setMessages([...messageRef.current]); setMessages([...messageRef.current]);
}); });
socket.on('userlist', ({ users }) => {
connectionsRef.current = users;
setConnections([...connectionsRef.current]);
});
return () => { return () => {
socket.off('new-message'); socket.off('new-message');
socket.close(); socket.close();
@ -38,5 +44,5 @@ export const useMessages = (chatRoom) => {
} }
}; };
return [messages, sendMessage]; return [connections, messages, sendMessage];
}; };

View File

@ -19,7 +19,11 @@ const haversine = (p1, p2) => {
}; };
@Controller() @Controller()
export class ChatRoomController { export class ChatRoomController {
constructor(private chatRoomService: ChatRoomService, private usersService: UsersService) {} constructor(private chatRoomService: ChatRoomService, private usersService: UsersService) {
setInterval(() => {
console.log('Hello');
}, 60 * 1000);
}
@Get('/chat_rooms') @Get('/chat_rooms')
async get(@JwtBody() jwtBody: JwtBodyDto, @Query() query: any) { async get(@JwtBody() jwtBody: JwtBodyDto, @Query() query: any) {
@ -57,6 +61,11 @@ export class ChatRoomController {
@Post('/chat_rooms') @Post('/chat_rooms')
async create(@JwtBody() jwtBody: JwtBodyDto, @Body() chatRoom: any) { async create(@JwtBody() jwtBody: JwtBodyDto, @Body() chatRoom: any) {
if (chatRoom.radius > 1000) {
return {
error: 'Radius cannot be greater than 1000 meters',
};
}
chatRoom.user = await this.usersService.find(jwtBody.userId); chatRoom.user = await this.usersService.find(jwtBody.userId);
return await this.chatRoomService.create(chatRoom); return await this.chatRoomService.create(chatRoom);
} }

View File

@ -43,6 +43,11 @@ export class AddChatRoom1648605030863 implements MigrationInterface {
type: 'varchar', type: 'varchar',
isNullable: true, isNullable: true,
}, },
{
name: 'lastConnection',
type: 'timestamp',
default: 'now()',
},
], ],
}), }),
true, true,

View File

@ -0,0 +1,54 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class AddConnectedUsers1648844808010 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'chat_room_connection',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
},
{
name: 'userId',
type: 'int',
isNullable: false,
},
{
name: 'chatRoomId',
type: 'text',
isNullable: false,
},
],
}),
true,
);
await queryRunner.createForeignKey(
'chat_room_connection',
new TableForeignKey({
columnNames: ['userId'],
referencedColumnNames: ['id'],
referencedTableName: 'user',
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'chat_room_connection',
new TableForeignKey({
columnNames: ['chatRoomId'],
referencedColumnNames: ['id'],
referencedTableName: 'chat_room',
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('connected_users');
}
}

View File

@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
import { ChatRoomConnection } from './chat_room_connection.entity';
import { User } from './user.entity'; import { User } from './user.entity';
@Entity() @Entity()
@ -18,6 +19,12 @@ export class ChatRoom {
@Column() @Column()
name: string; name: string;
@Column()
lastConnection: Date;
@ManyToOne(() => User, (user) => user.chatRooms) @ManyToOne(() => User, (user) => user.chatRooms)
user: User; user: User;
@OneToMany(() => ChatRoomConnection, (chatRoomConnection) => chatRoomConnection.chatRoom)
chatRoomConnections: ChatRoomConnection[];
} }

View File

@ -0,0 +1,15 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user.entity';
import { ChatRoom } from './chat_room.entity';
@Entity()
export class ChatRoomConnection {
@PrimaryGeneratedColumn()
id: string;
@ManyToOne(() => User, (user) => user.chatRooms)
user: User;
@ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chatRoomConnections)
chatRoom: ChatRoom;
}

View File

@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatRoomController } from 'server/controllers/chat_room.controller'; import { ChatRoomController } from 'server/controllers/chat_room.controller';
import { ChatRoom } from 'server/entities/chat_room.entity'; import { ChatRoom } from 'server/entities/chat_room.entity';
import { ChatRoomConnection } from 'server/entities/chat_room_connection.entity';
import { ChatRoomService } from 'server/providers/services/chat_room.service'; import { ChatRoomService } from 'server/providers/services/chat_room.service';
import { UsersService } from 'server/providers/services/users.service'; import { UsersService } from 'server/providers/services/users.service';
import { UsersModule } from './users.module'; import { UsersModule } from './users.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([ChatRoom]), UsersModule], imports: [TypeOrmModule.forFeature([ChatRoom, ChatRoomConnection]), UsersModule],
controllers: [ChatRoomController], controllers: [ChatRoomController],
providers: [ChatRoomService, UsersService], providers: [ChatRoomService, UsersService],
exports: [TypeOrmModule], exports: [TypeOrmModule],

View File

@ -14,6 +14,7 @@ import { GatewayJwtBody } from 'server/decorators/gateway_jwt_body.decorator';
import { JwtBodyDto } from 'server/dto/jwt_body.dto'; import { JwtBodyDto } from 'server/dto/jwt_body.dto';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { GatewayAuthGuard } from '../guards/gatewayauth.guard'; import { GatewayAuthGuard } from '../guards/gatewayauth.guard';
import { ChatRoomService } from '../services/chat_room.service';
import { JwtService } from '../services/jwt.service'; import { JwtService } from '../services/jwt.service';
import { UsersService } from '../services/users.service'; import { UsersService } from '../services/users.service';
@ -23,31 +24,40 @@ export class ChatRoomGateway implements OnGatewayInit, OnGatewayConnection, OnGa
@WebSocketServer() @WebSocketServer()
server: Server; server: Server;
constructor(private jwtService: JwtService, private userService: UsersService) {} constructor(
private jwtService: JwtService,
private userService: UsersService,
private chatRoomService: ChatRoomService,
) {}
afterInit(server: Server) { afterInit(server: Server) {
console.log('Sockets initialized'); console.log('Sockets initialized');
} }
handleConnection(client: Socket) { async handleConnection(client: Socket) {
// you can do things like add users to rooms
// or emit events here.
// IMPORTANT! The GatewayAuthGuard doesn't trigger on these handlers
// if you need to do anything in this method you need to authenticate the JWT
// manually.
try { try {
const jwt = client.handshake.auth.token; const jwtBody = this.jwtService.parseToken(client.handshake.auth.token);
const jwtBody = this.jwtService.parseToken(jwt); const user = await this.userService.find(jwtBody.userId);
const chatRoomId = client.handshake.query.chatRoomId; const chatRoom = await this.chatRoomService.findById(client.handshake.query.chatRoomId as unknown as string);
console.log('Client Connected: ', jwtBody.userId); await this.chatRoomService.connectUser(chatRoom, user);
client.join(chatRoomId); client.join(chatRoom.id);
this.server.to(chatRoom.id).emit('userlist', {
users: await this.chatRoomService.connectedUsers(chatRoom),
});
} catch (e) { } catch (e) {
throw new WsException('Invalid token'); throw new WsException(e.message);
} }
} }
handleDisconnect(client: Socket) { async handleDisconnect(client: Socket) {
console.log('Client Disconnected'); console.log('Client Disconnected');
const jwtBody = this.jwtService.parseToken(client.handshake.auth.token);
const user = await this.userService.find(jwtBody.userId);
const chatRoom = await this.chatRoomService.findById(client.handshake.query.chatRoomId as unknown as string);
await this.chatRoomService.disconnectUser(chatRoom, user);
this.server.to(chatRoom.id).emit('userlist', {
users: await this.chatRoomService.connectedUsers(chatRoom),
});
} }
@SubscribeMessage('message') @SubscribeMessage('message')
@ -58,7 +68,7 @@ export class ChatRoomGateway implements OnGatewayInit, OnGatewayConnection, OnGa
) { ) {
const user = await this.userService.find(jwtBody.userId); const user = await this.userService.find(jwtBody.userId);
this.server.to(client.handshake.query.chatRoomId).emit('new-message', { this.server.to(client.handshake.query.chatRoomId).emit('new-message', {
id: user.id * Math.random() * 2048 * Date.now(), id: user.id * Math.random() * Math.pow(2, 16) * Date.now(),
content: data, content: data,
userName: `${user.firstName} ${user.lastName}`, userName: `${user.firstName} ${user.lastName}`,
userId: user.id, userId: user.id,

View File

@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ChatRoom } from 'server/entities/chat_room.entity'; import { ChatRoom } from 'server/entities/chat_room.entity';
import { User } from 'server/entities/user.entity';
import { ChatRoomConnection } from 'server/entities/chat_room_connection.entity';
@Injectable() @Injectable()
export class ChatRoomService { export class ChatRoomService {
constructor( constructor(
@InjectRepository(ChatRoom) @InjectRepository(ChatRoom)
private chatRoomRepository: Repository<ChatRoom>, private chatRoomRepository: Repository<ChatRoom>,
@InjectRepository(ChatRoomConnection)
private connectedUsersRepository: Repository<ChatRoomConnection>,
) {} ) {}
create(chatRoom: ChatRoom) { create(chatRoom: ChatRoom) {
@ -29,6 +33,46 @@ export class ChatRoomService {
return this.chatRoomRepository.findOne(id, { relations }); return this.chatRoomRepository.findOne(id, { relations });
} }
async connectedUsers(chatRoom: ChatRoom) {
return this.connectedUsersRepository
.find({
where: { chatRoom },
relations: ['user'],
})
.then((x) =>
x.map((x) => {
return {
id: x.user.id,
userName: `${x.user.firstName} ${x.user.lastName}`,
};
}),
);
}
connectUser = async function (chatRoom: ChatRoom, user: User) {
const connectedUser = await this.connectedUsersRepository.findOne({
where: { chatRoom, user },
});
if (connectedUser) {
return connectedUser;
}
const chatRoomConnection = new ChatRoomConnection();
chatRoomConnection.chatRoom = chatRoom;
chatRoomConnection.user = user;
await this.connectedUsersRepository.save(chatRoomConnection);
return this.connectedUsers(chatRoom);
};
disconnectUser = async function (chatRoom: ChatRoom, user: User) {
const connectedUser = await this.connectedUsersRepository.findOne({
where: { chatRoom, user },
});
if (connectedUser) {
return this.connectedUsersRepository.remove(connectedUser);
}
return false;
};
save(chatRoom: ChatRoom) { save(chatRoom: ChatRoom) {
return this.chatRoomRepository.save(chatRoom); return this.chatRoomRepository.save(chatRoom);
} }