Skip to content
2 changes: 2 additions & 0 deletions common/zod/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const SendMessageSchema = z.object({

export const DMOverviewSchema = z.object({
isDM: z.literal(true),
isFriend: z.boolean(),
friendId: UserIDSchema,
name: NameSchema,
thumbnail: z.string(),
Expand Down Expand Up @@ -153,6 +154,7 @@ export const DMRoomSchema = z.object({
export const PersonalizedDMRoomSchema = z.object({
name: NameSchema,
thumbnail: z.string(),
isFriend: z.boolean(),
});

export const SharedRoomSchema = z.object({
Expand Down
29 changes: 26 additions & 3 deletions server/src/database/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ import type {
} from "common/types";
import { prisma } from "./client";
import { getRelation } from "./matches";
import { getMatchedUser } from "./requests";
import { getMatchedUser, getPendingRequestsToUser } from "./requests";

// ユーザーの参加しているすべての Room の概要 (Overview) の取得
export async function getOverview(
user: UserID,
): Promise<Result<RoomOverview[]>> {
try {
const matched = await getMatchedUser(user);
if (!matched.ok) return Err(matched.error);

const requester = await getPendingRequestsToUser(user);
if (!requester.ok) return Err(requester.error);

const dm = await Promise.all(
matched.value.map(async (friend) => {
const lastMessageResult = await getLastMessage(user, friend.id);
Expand All @@ -32,6 +34,7 @@ export async function getOverview(
: undefined;
const overview: DMOverview = {
isDM: true,
isFriend: true,
friendId: friend.id,
name: friend.name,
thumbnail: friend.pictureUrl,
Expand Down Expand Up @@ -61,7 +64,27 @@ export async function getOverview(
};
return overview;
});
return Ok([...shared, ...dm]);

// リクエスター (友達申請者) のオーバービュー作成
const requesterOverview = await Promise.all(
requester.value.map(async (requester) => {
const lastMessageResult = await getLastMessage(user, requester.id);
const lastMessage = lastMessageResult.ok
? lastMessageResult.value
: undefined;
const overview: DMOverview = {
isDM: true,
isFriend: false,
friendId: requester.id,
name: requester.name,
thumbnail: requester.pictureUrl,
lastMsg: lastMessage,
};
return overview;
}),
);

return Ok([...shared, ...dm, ...requesterOverview]);
} catch (e) {
return Err(e);
}
Expand Down
12 changes: 8 additions & 4 deletions server/src/functions/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ export async function sendDM(
send: SendMessage,
): Promise<http.Response<Message>> {
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<Message, "id"> = {
Expand All @@ -62,8 +64,9 @@ export async function getDM(
requester: UserID,
_with: UserID,
): Promise<http.Response<PersonalizedDMRoom & DMRoom>> {
if (!areMatched(requester, _with))
return http.forbidden("cannot DM with a non-friend");
const rel = await getRelation(requester, _with);
if (!rel.ok || rel.value.status === "REJECTED")
return http.forbidden("cannot send to rejected-friend");

const room = await db.getDMbetween(requester, _with);
if (!room.ok) return http.internalError();
Expand All @@ -74,6 +77,7 @@ export async function getDM(
const personalized: PersonalizedDMRoom & DMRoom = {
name: friendData.value.name,
thumbnail: friendData.value.pictureUrl,
isFriend: rel.value.status === "MATCHED",
...room.value,
};

Expand Down
15 changes: 5 additions & 10 deletions web/api/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
InitRoom,
Message,
MessageID,
PersonalizedDMRoom,
RoomOverview,
SendMessage,
ShareRoomID,
Expand Down Expand Up @@ -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<DMRoom & PersonalizedDMRoom> {
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);
Expand Down
19 changes: 2 additions & 17 deletions web/app/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
"use client";
import type { DMRoom, PersonalizedDMRoom } from "common/types";
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);
Expand Down
27 changes: 27 additions & 0 deletions web/components/chat/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Box, List, Typography } from "@mui/material";
import type { RoomOverview } from "common/types";
import { useRouter } from "next/navigation";
import request from "~/api/request";
import { HumanListItem } from "../human/humanListItem";

type RoomListProps = {
Expand Down Expand Up @@ -34,6 +35,32 @@ export function RoomList(props: RoomListProps) {
</p>
{roomsData?.map((room) => {
if (room.isDM) {
if (!room.isFriend) {
return (
<Box
key={room.friendId}
onClick={(e) => {
e.stopPropagation();
navigateToRoom(room);
}}
>
<HumanListItem
key={room.friendId}
id={room.friendId}
name={room.name}
pictureUrl={room.thumbnail}
rollUpName={true}
lastMessage={room.lastMsg?.content}
onAccept={() => {
request.accept(room.friendId).then(() => location.reload());
}}
onReject={() => {
request.reject(room.friendId).then(() => location.reload());
}}
/>
</Box>
);
}
return (
<Box
key={room.friendId}
Expand Down
74 changes: 56 additions & 18 deletions web/components/chat/RoomWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"use client";
import type { Message, MessageID, SendMessage, UserID } from "common/types";
import type { Content } from "common/zod/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";
Expand All @@ -13,23 +15,8 @@ import { socket } from "../data/socket";
import { MessageInput } from "./MessageInput";
import { RoomHeader } from "./RoomHeader";

type Props = {
friendId: UserID;
room: {
id: number;
messages: {
id: number;
creator: number;
createdAt: Date;
content: string;
edited: boolean;
}[];
isDM: true;
} & {
name: string;
thumbnail: string;
};
};
type Props = { friendId: UserID; room: DMRoom & PersonalizedDMRoom };

export function RoomWindow(props: Props) {
const { friendId, room } = props;

Expand Down Expand Up @@ -171,6 +158,13 @@ export function RoomWindow(props: Props) {

return (
<>
{!room.isFriend && (
<FloatingMessage
message="この人とはマッチングしていません。"
friendId={friendId}
/>
)}

<div className="fixed top-14 z-50 w-full bg-white">
<RoomHeader room={room} />
</div>
Expand Down Expand Up @@ -261,3 +255,47 @@ export function RoomWindow(props: Props) {
</>
);
}

type FloatingMessageProps = {
message: string;
friendId: UserID;
};

const FloatingMessage = ({ message, friendId }: FloatingMessageProps) => {
const router = useRouter();
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
style={{
pointerEvents: "none", // 背景はクリック可能にする
}}
>
<div
className="w-11/12 max-w-md rounded-lg bg-white p-6 text-center shadow-lg"
style={{
pointerEvents: "auto", // モーダル内はクリック可能にする
}}
>
<p>{message}</p>
{/* biome-ignore lint/a11y/useButtonType: <explanation> */}
<button
className="btn btn-success btn-sm"
onClick={() => {
request.accept(friendId).then(() => router.push("/chat"));
}}
>
承認
</button>
{/* biome-ignore lint/a11y/useButtonType: <explanation> */}
<button
className="btn btn-error btn-sm"
onClick={() => {
request.reject(friendId).then(() => router.push("/chat"));
}}
>
拒否
</button>
</div>
</div>
);
};
13 changes: 11 additions & 2 deletions web/components/human/humanListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,23 @@ export function HumanListItem(props: HumanListItemProps) {
// biome-ignore lint/a11y/useButtonType: <explanation>
<button
className="btn btn-success btn-sm"
onClick={() => onAccept(id)}
onClick={(e) => {
e.stopPropagation();
onAccept(id);
}}
>
承認
</button>
)}
{onReject && (
// biome-ignore lint/a11y/useButtonType: <explanation>
<button className="btn btn-error btn-sm" onClick={() => onReject(id)}>
<button
className="btn btn-error btn-sm"
onClick={(e) => {
e.stopPropagation();
onReject(id);
}}
>
拒否
</button>
)}
Expand Down