2026-03-30 18:05:54 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
2026-04-01 12:13:00 +09:00
|
|
|
import { apiClient, API_BASE_URL } from "@/lib/api/client";
|
2026-03-30 18:41:13 +09:00
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
2026-03-30 18:05:54 +09:00
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// Types
|
|
|
|
|
// ============================================
|
|
|
|
|
export interface Room {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
type: "dm" | "group" | "channel";
|
|
|
|
|
lastMessage?: string;
|
|
|
|
|
lastMessageAt?: string;
|
|
|
|
|
unreadCount: number;
|
|
|
|
|
participants: Participant[];
|
|
|
|
|
description?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Participant {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
photo?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Message {
|
|
|
|
|
id: string;
|
|
|
|
|
roomId: string;
|
|
|
|
|
senderId: string;
|
|
|
|
|
senderName: string;
|
|
|
|
|
senderPhoto?: string | null;
|
|
|
|
|
content: string;
|
|
|
|
|
type: "text" | "file" | "system";
|
|
|
|
|
fileUrl?: string;
|
|
|
|
|
fileName?: string;
|
2026-04-01 12:13:00 +09:00
|
|
|
fileMimeType?: string | null;
|
2026-03-30 18:05:54 +09:00
|
|
|
reactions: Reaction[];
|
|
|
|
|
threadCount?: number;
|
|
|
|
|
parentId?: string | null;
|
|
|
|
|
isDeleted: boolean;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Reaction {
|
|
|
|
|
emoji: string;
|
|
|
|
|
users: { userId: string; userName: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CompanyUser {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
deptName?: string;
|
|
|
|
|
positionName?: string;
|
|
|
|
|
photo?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// API helpers
|
|
|
|
|
// ============================================
|
|
|
|
|
async function fetchApi<T>(url: string): Promise<T> {
|
|
|
|
|
const res = await apiClient.get(url);
|
|
|
|
|
return res.data?.data ?? res.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function postApi<T>(url: string, data?: unknown): Promise<T> {
|
|
|
|
|
const res = await apiClient.post(url, data);
|
|
|
|
|
return res.data?.data ?? res.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// Hooks
|
|
|
|
|
// ============================================
|
|
|
|
|
export function useRooms() {
|
2026-03-30 18:41:13 +09:00
|
|
|
const { user } = useAuth();
|
|
|
|
|
const companyCode = user?.companyCode || user?.company_code;
|
2026-03-30 18:05:54 +09:00
|
|
|
return useQuery<Room[]>({
|
2026-03-30 18:41:13 +09:00
|
|
|
queryKey: ["messenger", "rooms", companyCode],
|
2026-03-30 18:05:54 +09:00
|
|
|
queryFn: async () => {
|
|
|
|
|
const data = await fetchApi<any[]>("/messenger/rooms");
|
|
|
|
|
return data.map((r) => ({
|
|
|
|
|
id: String(r.id),
|
|
|
|
|
name: r.room_name ?? r.name ?? "",
|
|
|
|
|
type: r.room_type ?? r.type,
|
|
|
|
|
lastMessage: r.last_message ?? r.lastMessage,
|
|
|
|
|
lastMessageAt: r.last_message_at ?? r.lastMessageAt,
|
|
|
|
|
unreadCount: r.unread_count ?? r.unreadCount ?? 0,
|
|
|
|
|
description: r.description,
|
|
|
|
|
participants: (r.participants ?? []).map((p: any) => ({
|
|
|
|
|
userId: p.user_id ?? p.userId,
|
|
|
|
|
userName: p.user_name ?? p.userName,
|
|
|
|
|
photo: p.photo ? `data:image/jpeg;base64,${p.photo}` : null,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
refetchInterval: 30000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useMessages(roomId: string | null) {
|
|
|
|
|
return useQuery<Message[]>({
|
|
|
|
|
queryKey: ["messenger", "messages", roomId],
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
const data = await fetchApi<any[]>(`/messenger/rooms/${roomId}/messages`);
|
|
|
|
|
return data.map((m) => ({
|
|
|
|
|
id: String(m.id),
|
|
|
|
|
roomId: String(m.room_id ?? m.roomId),
|
|
|
|
|
senderId: m.sender_id ?? m.senderId,
|
|
|
|
|
senderName: m.sender_name ?? m.senderName ?? m.sender_id,
|
|
|
|
|
senderPhoto: m.sender_photo
|
|
|
|
|
? `data:image/jpeg;base64,${m.sender_photo}`
|
|
|
|
|
: (m.senderPhoto ?? null),
|
|
|
|
|
content: m.content ?? "",
|
|
|
|
|
type: m.message_type ?? m.type ?? "text",
|
2026-04-01 12:13:00 +09:00
|
|
|
fileUrl: m.files?.[0]?.id
|
|
|
|
|
? `${API_BASE_URL}/messenger/files/${m.files[0].id}`
|
|
|
|
|
: (m.file_url ?? m.fileUrl),
|
|
|
|
|
fileName: m.files?.[0]?.original_name ?? m.file_name ?? m.fileName,
|
|
|
|
|
fileMimeType: m.files?.[0]?.mime_type ?? null,
|
2026-03-30 18:05:54 +09:00
|
|
|
reactions: m.reactions ?? [],
|
|
|
|
|
threadCount: m.thread_count ?? m.threadCount ?? 0,
|
|
|
|
|
parentId: m.parent_message_id ?? m.parentId ?? null,
|
|
|
|
|
isDeleted: m.is_deleted ?? m.isDeleted ?? false,
|
|
|
|
|
createdAt: m.created_at ?? m.createdAt,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
enabled: !!roomId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useCompanyUsers() {
|
2026-03-30 18:41:13 +09:00
|
|
|
const { user } = useAuth();
|
|
|
|
|
const companyCode = user?.companyCode || user?.company_code;
|
2026-03-30 18:05:54 +09:00
|
|
|
return useQuery<CompanyUser[]>({
|
2026-03-30 18:41:13 +09:00
|
|
|
queryKey: ["messenger", "users", companyCode],
|
2026-03-30 18:05:54 +09:00
|
|
|
queryFn: async () => {
|
|
|
|
|
const data = await fetchApi<any[]>("/messenger/users");
|
|
|
|
|
return data.map((u) => ({
|
|
|
|
|
userId: u.user_id ?? u.userId,
|
|
|
|
|
userName: u.user_name ?? u.userName,
|
|
|
|
|
deptName: u.dept_name ?? u.deptName,
|
|
|
|
|
photo: u.photo ? `data:image/jpeg;base64,${u.photo}` : null,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUnreadCount() {
|
|
|
|
|
return useQuery<number>({
|
|
|
|
|
queryKey: ["messenger", "unread"],
|
|
|
|
|
queryFn: () => fetchApi("/messenger/unread"),
|
|
|
|
|
refetchInterval: 15000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useSendMessage() {
|
|
|
|
|
const qc = useQueryClient();
|
2026-04-01 12:13:00 +09:00
|
|
|
const { user } = useAuth();
|
2026-03-30 18:05:54 +09:00
|
|
|
return useMutation({
|
2026-04-01 12:13:00 +09:00
|
|
|
mutationFn: (payload: { roomId: string; content: string; type?: string; parentId?: string | null; fileUrl?: string; fileName?: string }) =>
|
|
|
|
|
postApi(`/messenger/rooms/${payload.roomId}/messages`, {
|
|
|
|
|
content: payload.content,
|
|
|
|
|
type: payload.type,
|
|
|
|
|
parentId: payload.parentId,
|
|
|
|
|
file_url: payload.fileUrl,
|
|
|
|
|
file_name: payload.fileName,
|
|
|
|
|
}),
|
|
|
|
|
onMutate: async (variables) => {
|
|
|
|
|
const queryKey = ["messenger", "messages", variables.roomId];
|
|
|
|
|
await qc.cancelQueries({ queryKey });
|
|
|
|
|
const previous = qc.getQueryData<Message[]>(queryKey);
|
|
|
|
|
const optimistic: Message = {
|
|
|
|
|
id: `optimistic-${Date.now()}`,
|
|
|
|
|
roomId: variables.roomId,
|
|
|
|
|
senderId: user?.userId ?? "me",
|
|
|
|
|
senderName: "",
|
|
|
|
|
content: variables.content,
|
|
|
|
|
type: (variables.type as Message["type"]) ?? "text",
|
|
|
|
|
reactions: [],
|
|
|
|
|
isDeleted: false,
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
qc.setQueryData<Message[]>(queryKey, (old) => [...(old ?? []), optimistic]);
|
|
|
|
|
return { previous, queryKey };
|
|
|
|
|
},
|
|
|
|
|
onError: (_err, _vars, context) => {
|
|
|
|
|
if (context) qc.setQueryData(context.queryKey, context.previous);
|
|
|
|
|
},
|
2026-03-30 18:05:54 +09:00
|
|
|
onSuccess: (_data, variables) => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "messages", variables.roomId] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useCreateRoom() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (payload: { type: "dm" | "group" | "channel"; name?: string; description?: string; participantIds: string[] }) =>
|
|
|
|
|
postApi<Room>("/messenger/rooms", payload),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useMarkAsRead() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (roomId: string) => postApi(`/messenger/rooms/${roomId}/read`),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "unread"] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useAddReaction() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (payload: { messageId: string; roomId: string; emoji: string }) =>
|
|
|
|
|
postApi(`/messenger/messages/${payload.messageId}/reactions`, { emoji: payload.emoji }),
|
|
|
|
|
onSuccess: (_data, variables) => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "messages", variables.roomId] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 18:41:13 +09:00
|
|
|
export function useUpdateRoom() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ roomId, name }: { roomId: string; name: string }) =>
|
|
|
|
|
apiClient.put(`/messenger/rooms/${roomId}`, { room_name: name }).then((res) => res.data?.data ?? res.data),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 18:05:54 +09:00
|
|
|
export function useUploadFile() {
|
2026-04-01 12:13:00 +09:00
|
|
|
const qc = useQueryClient();
|
2026-03-30 18:05:54 +09:00
|
|
|
return useMutation({
|
2026-04-01 12:13:00 +09:00
|
|
|
mutationFn: async ({ file, roomId }: { file: File; roomId: string }) => {
|
2026-03-30 18:05:54 +09:00
|
|
|
const formData = new FormData();
|
2026-04-01 12:13:00 +09:00
|
|
|
formData.append("files", file);
|
|
|
|
|
formData.append("room_id", roomId);
|
2026-03-30 18:05:54 +09:00
|
|
|
const res = await apiClient.post("/messenger/files/upload", formData, {
|
|
|
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
|
|
|
});
|
|
|
|
|
return res.data?.data ?? res.data;
|
|
|
|
|
},
|
2026-04-01 12:13:00 +09:00
|
|
|
onSuccess: (_data, variables) => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "messages", variables.roomId] });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ["messenger", "rooms"] });
|
|
|
|
|
},
|
2026-03-30 18:05:54 +09:00
|
|
|
});
|
|
|
|
|
}
|