feat: deletion of messages
This commit is contained in:
parent
97f0483f9a
commit
4d87f86ff7
9 changed files with 111 additions and 24 deletions
|
|
@ -1,17 +1,25 @@
|
|||
import React, { useState } from "react";
|
||||
import { ClientChatMessage, ClientMessage } from "../../../shared";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSendMessage: (content: string) => void;
|
||||
sendMessage: (message: ClientMessage) => void;
|
||||
}
|
||||
|
||||
function ChatInput({ onSendMessage }: ChatInputProps) {
|
||||
function ChatInput({ sendMessage }: ChatInputProps) {
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
|
||||
onSendMessage(message);
|
||||
const chatMessage: ClientChatMessage = {
|
||||
type: "CHAT_MESSAGE",
|
||||
payload: {
|
||||
content: message,
|
||||
},
|
||||
};
|
||||
|
||||
sendMessage(chatMessage);
|
||||
setMessage("");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,19 @@ import { ChatMessage } from "../../../shared";
|
|||
function ChatMessageDisplay({
|
||||
message,
|
||||
userId,
|
||||
onDelete,
|
||||
onEdit,
|
||||
}: {
|
||||
message: ChatMessage;
|
||||
userId: string | undefined; // pass userId as string to avoid rerendering. if we useChatState here, it would rerender every single message every time a new chat message is added.
|
||||
userId: string | undefined;
|
||||
onDelete: (messageId: string) => void;
|
||||
onEdit: (messageId: string) => void;
|
||||
}) {
|
||||
const isOwn = userId === message.author.userId;
|
||||
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
||||
|
|
@ -19,22 +27,39 @@ function ChatMessageDisplay({
|
|||
{!isOwn && (
|
||||
<span className="font-bold text-gray-700">{message.author.name}</span>
|
||||
)}
|
||||
<span className="font-medium opacity-45">
|
||||
{new Date(message.timestamp).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
<span className="font-medium opacity-45">{timestamp}</span>
|
||||
{isOwn && (
|
||||
<span className="font-bold text-gray-700">{message.author.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`max-w-[80%] p-3 rounded-2xl shadow-sm bg-gray-200
|
||||
${isOwn ? "ml-auto rounded-tr-none" : "mr-auto rounded-tl-none"}`}
|
||||
>
|
||||
{message.content}
|
||||
</div>
|
||||
{isOwn && (
|
||||
<div
|
||||
className={`flex gap-2 mt-0.5 text-[10px]
|
||||
${isOwn ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => onEdit?.(message.id)}
|
||||
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<span className="text-gray-400">•</span>
|
||||
<button
|
||||
onClick={() => onDelete?.(message.id)}
|
||||
className="text-red-600 hover:text-red-800 active:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
|||
state,
|
||||
dispatch,
|
||||
actions: {
|
||||
deleteMessage: (messageId: string) =>
|
||||
dispatch({ type: "DELETE_MESSAGE", payload: messageId }),
|
||||
addMessage: (message: Message) =>
|
||||
dispatch({ type: "ADD_MESSAGE", payload: message }),
|
||||
setUser: (user: User) => dispatch({ type: "SET_USER", payload: user }),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import { ServerMessage } from "../../../shared";
|
||||
import {
|
||||
ClientDeleteMessage,
|
||||
ClientMessage,
|
||||
ServerMessage,
|
||||
} from "../../../shared";
|
||||
import { useChatState } from "../contexts/ChatContext";
|
||||
import ChatMessage from "./ChatMessage";
|
||||
|
||||
function ChatMessages() {
|
||||
interface MessageDisplayProps {
|
||||
sendMessage: (message: ClientMessage) => void;
|
||||
}
|
||||
|
||||
function ChatMessages({ sendMessage }: MessageDisplayProps) {
|
||||
const { state } = useChatState();
|
||||
const userId = state.currentUser?.userId;
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null); // ref to an empty div at the end to scroll to
|
||||
|
|
@ -12,6 +20,19 @@ function ChatMessages() {
|
|||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const onDelete = (messageId: string) => {
|
||||
const deleteMessage: ClientDeleteMessage = {
|
||||
type: "DELETE_MESSAGE",
|
||||
payload: {
|
||||
messageId,
|
||||
},
|
||||
};
|
||||
|
||||
sendMessage(deleteMessage);
|
||||
};
|
||||
|
||||
const onEdit = (messageId: string) => {};
|
||||
|
||||
// scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
|
|
@ -25,6 +46,8 @@ function ChatMessages() {
|
|||
message={message.payload}
|
||||
userId={userId}
|
||||
key={message.payload.id}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
);
|
||||
case "USER_JOINED":
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ export type ChatState = {
|
|||
|
||||
export type ChatAction =
|
||||
| { type: "ADD_MESSAGE"; payload: Message }
|
||||
| { type: "DELETE_MESSAGE"; payload: string }
|
||||
| { type: "SET_USER"; payload: User }
|
||||
| { type: "SET_CONNECTED"; payload: boolean }
|
||||
| { type: "CLEAR_MESSAGES" };
|
||||
|
||||
export type ChatActions = {
|
||||
addMessage: (message: Message) => void;
|
||||
deleteMessage: (messageId: string) => void;
|
||||
setUser: (user: User) => void;
|
||||
setConnected: (isConnected: boolean) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ export function useWebSocket(roomId: string) {
|
|||
actions.addMessage(message);
|
||||
break;
|
||||
|
||||
case "MESSAGE_DELETED":
|
||||
actions.deleteMessage(message.payload.messageId);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error("Unknown message type", message);
|
||||
}
|
||||
|
|
@ -37,12 +41,8 @@ export function useWebSocket(roomId: string) {
|
|||
|
||||
// Send a message to the server
|
||||
const sendMessage = useCallback(
|
||||
(content: string) => {
|
||||
(message: ClientMessage) => {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
const message: ClientMessage = {
|
||||
type: "CHAT_MESSAGE",
|
||||
payload: { content },
|
||||
};
|
||||
ws.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.error("WebSocket connection not established");
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ function Room() {
|
|||
{!isConnected && <Connect onConnect={connect} />}
|
||||
{isConnected && (
|
||||
<>
|
||||
<ChatMessages />
|
||||
<ChatInput onSendMessage={sendMessage} />
|
||||
<ChatMessages sendMessage={sendMessage} />
|
||||
<ChatInput sendMessage={sendMessage} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,17 @@ import { ChatAction, ChatState } from "../contexts/ChatContext";
|
|||
|
||||
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
||||
switch (action.type) {
|
||||
case "DELETE_MESSAGE":
|
||||
return {
|
||||
...state,
|
||||
messages: state.messages.filter((message) => {
|
||||
// only potentially delete chat messages, dont touch system messages
|
||||
if ("type" in message && message.type === "CHAT_MESSAGE") {
|
||||
return message.payload.id !== action.payload;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
case "ADD_MESSAGE":
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { randomUUIDv7, type ServerWebSocket } from "bun";
|
|||
import type {
|
||||
ClientMessage,
|
||||
ServerChatMessage,
|
||||
ServerMessageDeletedMessage,
|
||||
ServerRegistrationConfirmed,
|
||||
ServerUserJoinedMessage,
|
||||
User,
|
||||
|
|
@ -61,5 +62,20 @@ export default function handleClientMessage(
|
|||
socket.send(JSON.stringify(chatMessage));
|
||||
}
|
||||
break;
|
||||
|
||||
case "DELETE_MESSAGE":
|
||||
// verify that the sender is the author of the message
|
||||
// TODO: implement this, would need to store all messages on the server side
|
||||
|
||||
const deleteMessage: ServerMessageDeletedMessage = {
|
||||
type: "MESSAGE_DELETED",
|
||||
payload: {
|
||||
messageId: data.payload.messageId,
|
||||
},
|
||||
};
|
||||
|
||||
for (const [socket] of room.userConnections) {
|
||||
socket.send(JSON.stringify(deleteMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue