Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

リクエストを送ったら、チャットでメッセージが送れるようになる #538

Merged
merged 9 commits into from
Dec 2, 2024
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