Rombak total alur data frontend: dari static-export CSR (tiap browser fetch sendiri + akumulasi state voice per-tab) jadi server-side rendering. Frontend (Next.js): - next.config: output export -> standalone; halaman jadi server components - server data layer baru src/lib/api/server.ts (GMW_BACKEND_URL, no window) - dashboard/media/messages/moderation/recordings/voice page -> RSC yang fetch backend di render-time, seed ke client view (SWR fallbackData) - hook-hook utama terima initialData -> first paint data server, revalidate SWR setelahnya, tanpa spinner-blank-load - messages: guild/channel/tab/selected dibaca dari URL di server, page awal di-fetch server-side Shared realtime state (voice) server-authoritative: - backend src/modules/voice/live-speaker.ts: agregat voice_active_user dari gateway jadi snapshot authoritatif (single source of truth semua browser) - GET /api/voice/status kini include activeSpeakers - WS initial states kirim voice_state snapshot saat connect (late join langsung dapat state yang sama, bukan daftar kosong) - useSpeakers seed dari server snapshot + voice_state full-replace + voice_active_user delta upsert Deploy: - flake.nix: frontend package build SSR standalone (server.js wrapper, GMW_FRONTEND_PORT=4017); proxy nginx template proxy / -> Next server, /api + /ws tetap ke backend :4001
42 lines
1.2 KiB
TypeScript
42 lines
1.2 KiB
TypeScript
/**
|
|
* Messages page — Server Component.
|
|
*
|
|
* Reads the URL (guild/channel/tab/selected) on the server and, when a guild
|
|
* is already selected, fetches the first message page server-side so the
|
|
* initial list is server-rendered, not a client round-trip.
|
|
*/
|
|
import { getMessages, type MessagePageResult } from "@/lib/api/server";
|
|
import MessagesView from "./view";
|
|
|
|
export default async function MessagesPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
}) {
|
|
const sp = await searchParams;
|
|
const guild = typeof sp.guild === "string" ? sp.guild : "";
|
|
const channel = typeof sp.channel === "string" ? sp.channel : "";
|
|
const selected = typeof sp.selected === "string" ? sp.selected : null;
|
|
const tab =
|
|
typeof sp.tab === "string" && ["all", "images", "review"].includes(sp.tab)
|
|
? (sp.tab as "all" | "images" | "review")
|
|
: "all";
|
|
|
|
let initialPage: MessagePageResult | undefined;
|
|
if (guild) {
|
|
initialPage = await getMessages(guild, channel || undefined).catch(
|
|
() => undefined,
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MessagesView
|
|
initialGuild={guild}
|
|
initialChannel={channel}
|
|
initialDetailId={selected}
|
|
initialTab={tab}
|
|
initialMessagePage={initialPage}
|
|
/>
|
|
);
|
|
}
|