diff --git a/server/src/database/chat.ts b/server/src/database/chat.ts index 0373c7f1..43f6bdd8 100644 --- a/server/src/database/chat.ts +++ b/server/src/database/chat.ts @@ -127,11 +127,6 @@ export async function markAsRead( reader: UserID, message: MessageID, ) { - const val = { - readerId: reader, - messageId: message, - relationId: rel, - }; return await prisma.message.updateMany({ where: { id: { @@ -161,6 +156,7 @@ export async function sendDM( try { const message = await prisma.message.create({ data: { + // isPicture: false, // todo: bring it back relationId: relation, read: false, ...content, diff --git a/server/src/functions/chat.ts b/server/src/functions/chat.ts index 1e748918..eb891641 100644 --- a/server/src/functions/chat.ts +++ b/server/src/functions/chat.ts @@ -56,7 +56,7 @@ export async function sendDM( }; const result = await db.sendDM(rel.value.id, msg); - if (!result.ok) return http.internalError(""); + if (!result.ok) return http.internalError("Failed to send DM"); return http.created(result.value); } diff --git a/server/src/router/chat.ts b/server/src/router/chat.ts index ccf3ce8c..cf4381bb 100644 --- a/server/src/router/chat.ts +++ b/server/src/router/chat.ts @@ -38,7 +38,7 @@ router.post("/dm/to/:userid", async (req, res) => { const result = await core.sendDM(user.value, friend.value, send.data); if (result.ok) { - ws.sendMessage(result?.body, friend.value); + ws.sendMessage(result.body, friend.value); } res.status(result.code).send(result.body); }); diff --git a/web/api/internal/endpoints.ts b/web/api/internal/endpoints.ts index 50ea769c..a170eec1 100644 --- a/web/api/internal/endpoints.ts +++ b/web/api/internal/endpoints.ts @@ -1,8 +1,10 @@ import type { CourseID, Day, GUID } from "common/types"; import type { MessageID, ShareRoomID } from "common/types"; +import { panic } from "~/lib/utils"; -export const origin: string | null = process.env.NEXT_PUBLIC_API_ENDPOINT ?? ""; -if (!origin) throw new Error("process.env.NEXT_PUBLIC_API_ENDPOINT not found!"); +export const API_ENDPOINT: string = + process.env.NEXT_PUBLIC_API_ENDPOINT ?? + panic("process.env.NEXT_PUBLIC_API_ENDPOINT not found!"); // TODO: de-export this and use one from /common export type UserID = number; @@ -17,7 +19,7 @@ export type UserID = number; * - 500: internal error. **/ export const user = (userId: UserID) => { - return `${origin}/users/id/${userId}`; + return `${API_ENDPOINT}/users/id/${userId}`; }; /** @@ -36,7 +38,7 @@ export const user = (userId: UserID) => { * - 500: internal error. * **/ -export const users = `${origin}/users`; +export const users = `${API_ENDPOINT}/users`; /** * [v] 実装済み @@ -47,7 +49,7 @@ export const users = `${origin}/users`; * - 401: auth error. * - 500: internal error **/ -export const recommendedUsers = `${origin}/users/recommended`; +export const recommendedUsers = `${API_ENDPOINT}/users/recommended`; /** * [v] 実装済み @@ -72,7 +74,7 @@ export const recommendedUsers = `${origin}/users/recommended`; * - 500: internal error. * **/ -export const me = `${origin}/users/me`; +export const me = `${API_ENDPOINT}/users/me`; /** * [v] 実装済み @@ -83,7 +85,7 @@ export const me = `${origin}/users/me`; * - 401: unauthorized. * - 500: internal error. **/ -export const matchedUsers = `${origin}/users/matched`; +export const matchedUsers = `${API_ENDPOINT}/users/matched`; /** * [v] 実装済み @@ -94,7 +96,7 @@ export const matchedUsers = `${origin}/users/matched`; * - 401: unauthorized. * - 500: internal error. **/ -export const pendingRequestsToMe = `${origin}/users/pending/to-me`; +export const pendingRequestsToMe = `${API_ENDPOINT}/users/pending/to-me`; /** * [v] 実装済み @@ -105,7 +107,7 @@ export const pendingRequestsToMe = `${origin}/users/pending/to-me`; * - 401: unauthorized. * - 500: internal error. **/ -export const pendingRequestsFromMe = `${origin}/users/pending/from-me`; +export const pendingRequestsFromMe = `${API_ENDPOINT}/users/pending/from-me`; /** * [v] 実装済み @@ -117,7 +119,7 @@ export const pendingRequestsFromMe = `${origin}/users/pending/from-me`; * - 500: internal error. **/ export const userByGUID = (guid: GUID) => { - return `${origin}/users/guid/${guid}`; + return `${API_ENDPOINT}/users/guid/${guid}`; }; // this one may be public to anyone. @@ -131,7 +133,7 @@ export const userByGUID = (guid: GUID) => { * - 500: internal error. **/ export const userExists = (guid: GUID) => { - return `${origin}/users/exists/${guid}`; + return `${API_ENDPOINT}/users/exists/${guid}`; }; /** @@ -145,7 +147,7 @@ export const userExists = (guid: GUID) => { * - 500: internal error. **/ export const match = (opponentID: UserID) => { - return `${origin}/matches/${opponentID}`; + return `${API_ENDPOINT}/matches/${opponentID}`; }; /** @@ -158,7 +160,7 @@ export const match = (opponentID: UserID) => { * - 401: unauthorized. * - 500: internal error. **/ -export const matches = `${origin}/matches`; +export const matches = `${API_ENDPOINT}/matches`; /** * [x] 実装済み @@ -180,7 +182,7 @@ export const matches = `${origin}/matches`; * - 500: internal error. */ export const coursesUserId = (userId: UserID) => { - return `${origin}/courses/userId/${userId}`; + return `${API_ENDPOINT}/courses/userId/${userId}`; }; /** @@ -206,7 +208,7 @@ export const coursesUserId = (userId: UserID) => { * - 401: unauthorized. * - 500: internal error. */ -export const coursesMine = `${origin}/courses/mine`; +export const coursesMine = `${API_ENDPOINT}/courses/mine`; /** * [v] 実装済み @@ -218,7 +220,7 @@ export const coursesMine = `${origin}/courses/mine`; * - 500: internal error. */ export const coursesMineOverlaps = (courseId: CourseID) => { - return `${origin}/courses/mine/overlaps/${courseId}`; + return `${API_ENDPOINT}/courses/mine/overlaps/${courseId}`; }; /** @@ -231,7 +233,7 @@ export const coursesMineOverlaps = (courseId: CourseID) => { * - 500: internal error. **/ export const coursesDayPeriod = (day: Day, period: number) => { - return `${origin}/courses/day-period?day=${day}&period=${period}`; + return `${API_ENDPOINT}/courses/day-period?day=${day}&period=${period}`; }; /** @@ -243,7 +245,7 @@ export const coursesDayPeriod = (day: Day, period: number) => { * - 500: internal error. **/ export const sendRequest = (opponentId: UserID) => { - return `${origin}/requests/send/${opponentId}`; + return `${API_ENDPOINT}/requests/send/${opponentId}`; }; /** @@ -254,7 +256,7 @@ export const sendRequest = (opponentId: UserID) => { * - 500: internal error **/ export const cancelRequest = (opponentId: UserID) => { - return `${origin}/requests/cancel/${opponentId}`; + return `${API_ENDPOINT}/requests/cancel/${opponentId}`; }; /** * [v] 実装済み @@ -265,7 +267,7 @@ export const cancelRequest = (opponentId: UserID) => { * - 500: internal error. **/ export const acceptRequest = (opponentId: UserID) => { - return `${origin}/requests/accept/${opponentId}`; + return `${API_ENDPOINT}/requests/accept/${opponentId}`; }; /** @@ -278,13 +280,13 @@ export const acceptRequest = (opponentId: UserID) => { * - 500: internal error. **/ export const rejectRequest = (opponentId: UserID) => { - return `${origin}/requests/reject/${opponentId}`; + return `${API_ENDPOINT}/requests/reject/${opponentId}`; }; /** **/ export const markAsRead = (friendId: UserID, messageId: MessageID) => { - return `${origin}/chat/mark-as-read/${friendId}/${messageId}`; + return `${API_ENDPOINT}/chat/mark-as-read/${friendId}/${messageId}`; }; /** * []実装済み @@ -295,7 +297,7 @@ export const markAsRead = (friendId: UserID, messageId: MessageID) => { * - 401: unauthorized * - 500: internal error */ -export const roomOverview = `${origin}/chat/overview`; +export const roomOverview = `${API_ENDPOINT}/chat/overview`; /** * []実装済み @@ -308,7 +310,7 @@ export const roomOverview = `${origin}/chat/overview`; * - 403: Forbidden * - 500: internal error **/ -export const dmTo = (userId: UserID) => `${origin}/chat/dm/to/${userId}`; +export const dmTo = (userId: UserID) => `${API_ENDPOINT}/chat/dm/to/${userId}`; /** * PUT -> start dm with userId. created one if none was found. authorized. @@ -322,7 +324,8 @@ export const dmTo = (userId: UserID) => `${origin}/chat/dm/to/${userId}`; * - 403: forbidden. you and the user are not matched yet. * - 500: internal error. **/ -export const dmWith = (userId: UserID) => `${origin}/chat/dm/with/${userId}`; +export const dmWith = (userId: UserID) => + `${API_ENDPOINT}/chat/dm/with/${userId}`; /** * POST -> Create a room. authenticated @@ -334,7 +337,7 @@ export const dmWith = (userId: UserID) => `${origin}/chat/dm/with/${userId}`; * - 403: forbidden (cannot invite non-friends) * - 500: internal error **/ -export const sharedRooms = `${origin}/chat/shared`; +export const sharedRooms = `${API_ENDPOINT}/chat/shared`; /** authorized * GET -> Get info of a room (including the message log). @@ -342,7 +345,7 @@ export const sharedRooms = `${origin}/chat/shared`; * - body: UpdateRoom **/ export const sharedRoom = (roomId: ShareRoomID) => - `${origin}/chat/shared/${roomId}`; + `${API_ENDPOINT}/chat/shared/${roomId}`; /** POST: invite. authorized * - body: UserID[] @@ -353,24 +356,24 @@ export const sharedRoom = (roomId: ShareRoomID) => * - 500: internal error **/ export const roomInvite = (roomId: ShareRoomID) => - `${origin}/chat/shared/id/${roomId}/invite`; + `${API_ENDPOINT}/chat/shared/id/${roomId}/invite`; /** * PATCH: authorized body=SendMessage * DELETE: authorized **/ export const message = (messageId: MessageID) => - `${origin}/chat/messages/id/${messageId}`; + `${API_ENDPOINT}/chat/messages/id/${messageId}`; /** * GET: get profile picture of URL (this is usually hard-encoded in pictureURL so this variable is barely used) */ -export const pictureOf = (guid: GUID) => `${origin}/picture/${guid}`; +export const pictureOf = (guid: GUID) => `${API_ENDPOINT}/picture/${guid}`; /** * POST: update my profile picture. */ -export const picture = `${origin}/picture`; +export const picture = `${API_ENDPOINT}/picture`; export default { user, diff --git a/web/components/chat/RoomWindow.tsx b/web/components/chat/RoomWindow.tsx index 2b5cac4a..079d0556 100644 --- a/web/components/chat/RoomWindow.tsx +++ b/web/components/chat/RoomWindow.tsx @@ -2,16 +2,13 @@ import type { Message, MessageID, SendMessage, UserID } from "common/types"; import type { Content, DMRoom, PersonalizedDMRoom } from "common/zod/types"; import { useRouter } from "next/navigation"; -import { useSnackbar } from "notistack"; import { useCallback, useEffect, useRef, useState } from "react"; import * as chat from "~/api/chat/chat"; import { useMessages } from "~/api/chat/hooks"; import request from "~/api/request"; -import * as user from "~/api/user"; import { useMyID } from "~/api/user"; -import { getIdToken } from "~/firebase/auth/lib"; import Dots from "../common/Dots"; -import { socket } from "../data/socket"; +import { handlers } from "../data/socket"; import { MessageInput } from "./MessageInput"; import { RoomHeader } from "./RoomHeader"; @@ -47,7 +44,6 @@ export function RoomWindow(props: Props) { setMessages(state.data); }, [state.data]); - const { enqueueSnackbar } = useSnackbar(); const [editingMessageId, setEditingMessageId] = useState(null); const [editedContent, setEditedContent] = useState(""); @@ -68,54 +64,35 @@ export function RoomWindow(props: Props) { }, [write], ); - const updateLocalMessage = useCallback((_: Message) => reload(), [reload]); + // TODO: fix these + const updateLocalMessage = useCallback( + (_a: MessageID, _: Message) => reload(), + [reload], + ); const deleteLocalMessage = useCallback((_: MessageID) => reload(), [reload]); + // メッセージの受取 useEffect(() => { - async function registerSocket() { - if (!room) return; - const idToken = await getIdToken(); - socket.emit("register", idToken); - socket.on("newMessage", async (msg: Message) => { - if (msg.creator === friendId) { - appendLocalMessage(msg); - } else { - const creator = await user.get(msg.creator); - if (creator == null) return; - enqueueSnackbar( - `${creator.name}さんからのメッセージ : ${msg.content}`, - { - variant: "info", - }, - ); - } - }); - socket.on("updateMessage", async (msg: Message) => { - if (msg.creator === friendId) { - updateLocalMessage(msg); - } - }); - socket.on("deleteMessage", async (msgId: number) => { - deleteLocalMessage(msgId); - }); - } - registerSocket(); - // Clean up + handlers.onCreate = (msg) => { + if (msg.creator === friendId) { + appendLocalMessage(msg); + return true; // caught + } + return false; // didn't catch + }; + handlers.onUpdate = (id, msg) => { + updateLocalMessage(id, msg); + }; + handlers.onDelete = (id) => { + deleteLocalMessage(id); + }; return () => { - socket.off("newMessage"); - socket.off("updateMessage"); - socket.off("deleteMessage"); + handlers.onCreate = undefined; + handlers.onDelete = undefined; }; - }, [ - room, - friendId, - enqueueSnackbar, - appendLocalMessage, - updateLocalMessage, - deleteLocalMessage, - ]); + }, [friendId, appendLocalMessage, updateLocalMessage, deleteLocalMessage]); - //画面スクロール + // 画面スクロール const scrollDiv = useRef(null); const scrollToBottom = useCallback(() => { if (scrollDiv.current) { @@ -149,7 +126,7 @@ export function RoomWindow(props: Props) { { content }, friendId, ); - updateLocalMessage(editedMessage); + updateLocalMessage(editedMessage.id, editedMessage); }, [updateLocalMessage, friendId], ); diff --git a/web/components/data/socket.ts b/web/components/data/socket.ts index 0a4ab13d..630b024c 100644 --- a/web/components/data/socket.ts +++ b/web/components/data/socket.ts @@ -1,13 +1,43 @@ +import type { Message } from "common/types"; +import { enqueueSnackbar } from "notistack"; import { io } from "socket.io-client"; +import { API_ENDPOINT } from "~/api/internal/endpoints"; +import * as user from "~/api/user"; +import { getIdToken } from "~/firebase/auth/lib"; -const URL = process.env.NEXT_PUBLIC_API_ENDPOINT; - -if (!URL) { - throw new Error("process.env.NEXT_PUBLIC_API_ENDPOINT not found!"); -} - -export const socket = io(URL, { +export const socket = io(API_ENDPOINT, { auth: { serverOffset: 0, //TODO: ちゃんと実装する }, }); + +export const handlers: { + onCreate: undefined | ((data: Message) => boolean); // should return if it matched + onUpdate: undefined | ((id: number, data: Message) => void); + onDelete: undefined | ((id: number) => void); +} = { + onCreate: undefined, + onUpdate: undefined, + onDelete: undefined, +}; + +(async () => { + const idToken = await getIdToken(); + socket.emit("register", idToken); +})(); +socket.on("newMessage", async (msg: Message) => { + if (handlers.onCreate?.(msg)) return; // if it exists and caught the create sig + + const creator = await user.get(msg.creator); + if (!creator) return; + enqueueSnackbar(`${creator.name}さんからのメッセージ : ${msg.content}`, { + variant: "info", + }); +}); + +socket.on("updateMessage", async (msg: Message) => { + if (handlers.onUpdate) handlers.onUpdate(msg.id, msg); +}); +socket.on("deleteMessage", async (msgId: number) => { + if (handlers.onDelete) handlers.onDelete(msgId); +}); diff --git a/web/lib/utils.ts b/web/lib/utils.ts new file mode 100644 index 00000000..abbe234d --- /dev/null +++ b/web/lib/utils.ts @@ -0,0 +1,3 @@ +export function panic(reason: string): never { + throw new Error(`function panic() called for reason: "${reason}"`); +}