diff --git a/common/zod/schemas.ts b/common/zod/schemas.ts index 2c1c9cad..1c114305 100644 --- a/common/zod/schemas.ts +++ b/common/zod/schemas.ts @@ -123,8 +123,15 @@ export const SendMessageSchema = z.object({ content: z.string().min(1, { message: "Content must not be empty." }), }); +export const MatchingStatusSchema = z.union([ + z.literal("myRequest"), + z.literal("otherRequest"), + z.literal("matched"), +]); + export const DMOverviewSchema = z.object({ isDM: z.literal(true), + matchingStatus: MatchingStatusSchema, friendId: UserIDSchema, name: NameSchema, thumbnail: z.string(), @@ -153,6 +160,7 @@ export const DMRoomSchema = z.object({ export const PersonalizedDMRoomSchema = z.object({ name: NameSchema, thumbnail: z.string(), + matchingStatus: MatchingStatusSchema, }); export const SharedRoomSchema = z.object({ diff --git a/server/src/database/chat.ts b/server/src/database/chat.ts index 37f8a7cf..2a1e8307 100644 --- a/server/src/database/chat.ts +++ b/server/src/database/chat.ts @@ -14,9 +14,12 @@ import type { } from "common/types"; import { prisma } from "./client"; import { getRelation } from "./matches"; -import { getMatchedUser } from "./requests"; +import { + getMatchedUser, + getPendingRequestsFromUser, + getPendingRequestsToUser, +} from "./requests"; -// ユーザーの参加しているすべての Room の概要 (Overview) の取得 export async function getOverview( user: UserID, ): Promise> { @@ -24,7 +27,14 @@ export async function getOverview( const matched = await getMatchedUser(user); if (!matched.ok) return Err(matched.error); - const dm = await Promise.all( + const senders = await getPendingRequestsToUser(user); + if (!senders.ok) return Err(senders.error); + + const receivers = await getPendingRequestsFromUser(user); + if (!receivers.ok) return Err(receivers.error); + + //マッチングしている人のオーバービュー + const matchingOverview = await Promise.all( matched.value.map(async (friend) => { const lastMessageResult = await getLastMessage(user, friend.id); const lastMessage = lastMessageResult.ok @@ -32,6 +42,7 @@ export async function getOverview( : undefined; const overview: DMOverview = { isDM: true, + matchingStatus: "matched", friendId: friend.id, name: friend.name, thumbnail: friend.pictureUrl, @@ -41,6 +52,44 @@ export async function getOverview( }), ); + //自分にリクエストを送ってきた人のオーバービュー + const senderOverview = await Promise.all( + senders.value.map(async (sender) => { + const lastMessageResult = await getLastMessage(user, sender.id); + const lastMessage = lastMessageResult.ok + ? lastMessageResult.value + : undefined; + const overview: DMOverview = { + isDM: true, + matchingStatus: "otherRequest", + friendId: sender.id, + name: sender.name, + thumbnail: sender.pictureUrl, + lastMsg: lastMessage, + }; + return overview; + }), + ); + + //自分がリクエストを送った人のオーバービュー + const receiverOverview = await Promise.all( + receivers.value.map(async (receiver) => { + const lastMessageResult = await getLastMessage(user, receiver.id); + const lastMessage = lastMessageResult.ok + ? lastMessageResult.value + : undefined; + const overview: DMOverview = { + isDM: true, + matchingStatus: "myRequest", + friendId: receiver.id, + name: receiver.name, + thumbnail: receiver.pictureUrl, + lastMsg: lastMessage, + }; + return overview; + }), + ); + const sharedRooms: { id: number; name: string; @@ -61,7 +110,13 @@ export async function getOverview( }; return overview; }); - return Ok([...shared, ...dm]); + + return Ok([ + ...matchingOverview, + ...senderOverview, + ...receiverOverview, + ...shared, + ]); } catch (e) { return Err(e); } diff --git a/server/src/functions/chat.ts b/server/src/functions/chat.ts index 1277e54d..1e748918 100644 --- a/server/src/functions/chat.ts +++ b/server/src/functions/chat.ts @@ -42,8 +42,10 @@ export async function sendDM( send: SendMessage, ): Promise> { const rel = await getRelation(from, to); - if (!rel.ok || rel.value.status !== "MATCHED") - return http.forbidden("cannot send to non-friend"); + if (!rel.ok || rel.value.status === "REJECTED") + return http.forbidden( + "You cannot send a message because the friendship request was rejected.", + ); // they are now MATCHED const msg: Omit = { @@ -59,21 +61,28 @@ export async function sendDM( } export async function getDM( - requester: UserID, - _with: UserID, + user: UserID, + friend: UserID, ): Promise> { - if (!areMatched(requester, _with)) - return http.forbidden("cannot DM with a non-friend"); + const rel = await getRelation(user, friend); + if (!rel.ok || rel.value.status === "REJECTED") + return http.forbidden("cannot send to rejected-friend"); - const room = await db.getDMbetween(requester, _with); + const room = await db.getDMbetween(user, friend); if (!room.ok) return http.internalError(); - const friendData = await getUserByID(_with); + const friendData = await getUserByID(friend); if (!friendData.ok) return http.notFound("friend not found"); const personalized: PersonalizedDMRoom & DMRoom = { name: friendData.value.name, thumbnail: friendData.value.pictureUrl, + matchingStatus: + rel.value.status === "MATCHED" + ? "matched" + : rel.value.sendingUserId === user //どっちが送ったリクエストなのかを判定 + ? "myRequest" + : "otherRequest", ...room.value, }; diff --git a/server/src/router/chat.ts b/server/src/router/chat.ts index 2f4c11de..a6052e1f 100644 --- a/server/src/router/chat.ts +++ b/server/src/router/chat.ts @@ -23,12 +23,12 @@ router.get("/overview", async (req, res) => { res.status(result.code).send(result.body); }); -// send DM to userid. +// send DM to userId. router.post("/dm/to/:userid", async (req, res) => { const user = await safeGetUserId(req); if (!user.ok) return res.status(401).send("auth error"); const friend = safeParseInt(req.params.userid); - if (!friend.ok) return res.status(400).send("bad param encoding: `userid`"); + if (!friend.ok) return res.status(400).send("bad param encoding: `userId`"); const send = SendMessageSchema.safeParse(req.body); if (!send.success) { @@ -42,14 +42,14 @@ router.post("/dm/to/:userid", async (req, res) => { res.status(result.code).send(result.body); }); -// GET a DM Room with userid, CREATE one if not found. +// GET a DM Room with userId, CREATE one if not found. router.get("/dm/with/:userid", async (req, res) => { const user = await safeGetUserId(req); if (!user.ok) return res.status(401).send("auth error"); const friend = safeParseInt(req.params.userid); if (!friend.ok) - return res.status(400).send("invalid param `userid` formatting"); + return res.status(400).send("invalid param `userId` formatting"); const result = await core.getDM(user.value, friend.value); diff --git a/web/api/chat/chat.ts b/web/api/chat/chat.ts index 63788ea5..83e307d5 100644 --- a/web/api/chat/chat.ts +++ b/web/api/chat/chat.ts @@ -3,6 +3,7 @@ import type { InitRoom, Message, MessageID, + PersonalizedDMRoom, RoomOverview, SendMessage, ShareRoomID, @@ -83,22 +84,16 @@ export async function sendDM( return res.json(); } -export async function getDM(friendId: UserID): Promise< - DMRoom & { - name: string; - thumbnail: string; - } -> { +export async function getDM( + friendId: UserID, +): Promise { const res = await credFetch("GET", endpoints.dmWith(friendId)); if (res.status === 401) throw new ErrUnauthorized(); if (res.status !== 200) throw new Error( `getDM() failed: expected status code 200, got ${res.status}`, ); - const json: DMRoom & { - name: string; - thumbnail: string; - } = await res.json(); + const json: DMRoom & PersonalizedDMRoom = await res.json(); if (!Array.isArray(json?.messages)) return json; for (const m of json.messages) { m.createdAt = new Date(m.createdAt); diff --git a/web/app/chat/[id]/page.tsx b/web/app/chat/[id]/page.tsx index 1f717305..6db7a216 100644 --- a/web/app/chat/[id]/page.tsx +++ b/web/app/chat/[id]/page.tsx @@ -1,27 +1,13 @@ "use client"; +import type { DMRoom, PersonalizedDMRoom } from "common/types"; +import Link from "next/link"; import { useEffect, useState } from "react"; import * as chat from "~/api/chat/chat"; import { RoomWindow } from "~/components/chat/RoomWindow"; export default function Page({ params }: { params: { id: string } }) { const id = Number.parseInt(params.id); - const [room, setRoom] = useState< - | ({ - id: number; - isDM: true; - messages: { - id: number; - creator: number; - createdAt: Date; - content: string; - edited: boolean; - }[]; - } & { - name: string; - thumbnail: string; - }) - | null - >(null); + const [room, setRoom] = useState<(DMRoom & PersonalizedDMRoom) | null>(null); useEffect(() => { (async () => { const room = await chat.getDM(id); @@ -31,8 +17,16 @@ export default function Page({ params }: { params: { id: string } }) { return ( <> -

idは{id}です。

- {room ? :

データないよ

} + {room ? ( + + ) : ( +

+ Sorry, an unexpected error has occurred. + + Go Back + +

+ )} ); } diff --git a/web/components/chat/RoomList.tsx b/web/components/chat/RoomList.tsx index 1c6b6fad..710feb3c 100644 --- a/web/components/chat/RoomList.tsx +++ b/web/components/chat/RoomList.tsx @@ -34,6 +34,49 @@ export function RoomList(props: RoomListProps) {

{roomsData?.map((room) => { if (room.isDM) { + if (room.matchingStatus === "otherRequest") { + return ( + { + e.stopPropagation(); + navigateToRoom(room); + }} + > + + + ); + } + if (room.matchingStatus === "myRequest") { + return ( + { + e.stopPropagation(); + navigateToRoom(room); + }} + > + + + ); + } + // if (room.matchingStatus === "matched") return ( + {room.matchingStatus !== "matched" && ( + + )} +
@@ -261,3 +256,57 @@ export function RoomWindow(props: Props) { ); } + +type FloatingMessageProps = { + message: string; + friendId: number; + showButtons: boolean; +}; + +const FloatingMessage = ({ + message, + friendId, + showButtons, +}: FloatingMessageProps) => { + const router = useRouter(); + + return ( +
+
+

{message}

+ {showButtons && ( +
+ {/* biome-ignore lint/a11y/useButtonType: */} + + {/* biome-ignore lint/a11y/useButtonType: */} + +
+ )} +
+
+ ); +}; diff --git a/web/components/human/humanListItem.tsx b/web/components/human/humanListItem.tsx index b1a4a575..2fe31ebb 100644 --- a/web/components/human/humanListItem.tsx +++ b/web/components/human/humanListItem.tsx @@ -7,6 +7,7 @@ type HumanListItemProps = { pictureUrl: string; lastMessage?: string; rollUpName?: boolean; // is currently only intended to be used in Chat + statusMessage?: string; onDelete?: (id: number) => void; onOpen?: (user: { id: number; name: string; pictureUrl: string }) => void; onAccept?: (id: number) => void; @@ -23,6 +24,7 @@ export function HumanListItem(props: HumanListItemProps) { pictureUrl, rollUpName, lastMessage, + statusMessage, onDelete, onOpen, onAccept, @@ -61,6 +63,9 @@ export function HumanListItem(props: HumanListItemProps) { {lastMessage} )} + {statusMessage && ( + {statusMessage} + )}
@@ -68,14 +73,23 @@ export function HumanListItem(props: HumanListItemProps) { // biome-ignore lint/a11y/useButtonType: )} {onReject && ( // biome-ignore lint/a11y/useButtonType: - )}