Files
message-inbox-web-frontend-sdk/apps/web/lib/ui-state.tsx
T
2026-07-17 21:48:37 +05:30

178 lines
6.1 KiB
TypeScript

"use client";
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
interface ThreadTarget {
channelId: string;
parentId: string;
}
export interface AiSeed {
context?: string;
prompt?: string;
}
export type ModalKind = "create-channel" | "new-message" | "invite" | "channel-details" | "channel-members" | "create-workspace" | "callback" | "set-status" | "edit-profile";
export interface Toast {
id: number;
message: string;
tone: "default" | "success" | "danger";
}
interface UiState {
thread: ThreadTarget | null;
openThread: (channelId: string, parentId: string) => void;
closeThread: () => void;
paletteOpen: boolean;
setPaletteOpen: (v: boolean) => void;
notifOpen: boolean;
setNotifOpen: (v: boolean) => void;
profileUserId: string | null;
openProfile: (id: string) => void;
closeProfile: () => void;
themingOpen: boolean;
setThemingOpen: (v: boolean) => void;
huddleChannelId: string | null;
startHuddle: (channelId: string) => void;
endHuddle: () => void;
aiPanel: AiSeed | null;
openAi: (seed?: AiSeed) => void;
closeAi: () => void;
sidebarOpen: boolean;
setSidebarOpen: (v: boolean) => void;
// desktop sidebar collapse
sidebarCollapsed: boolean;
toggleSidebarCollapsed: () => void;
// modals
modal: ModalKind | null;
modalArg?: string;
openModal: (m: ModalKind, arg?: string) => void;
closeModal: () => void;
// toasts
toasts: Toast[];
toast: (message: string, tone?: Toast["tone"]) => void;
dismissToast: (id: number) => void;
}
const Ctx = createContext<UiState | null>(null);
export function UiStateProvider({ children }: { children: React.ReactNode }) {
const [thread, setThread] = useState<ThreadTarget | null>(null);
const [paletteOpen, setPaletteOpen] = useState(false);
const [notifOpen, setNotifOpen] = useState(false);
const [profileUserId, setProfileUserId] = useState<string | null>(null);
const [themingOpen, setThemingOpen] = useState(false);
const [huddleChannelId, setHuddleChannelId] = useState<string | null>(null);
const [aiPanel, setAiPanel] = useState<AiSeed | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [modal, setModal] = useState<ModalKind | null>(null);
const [modalArg, setModalArg] = useState<string | undefined>(undefined);
const [toasts, setToasts] = useState<Toast[]>([]);
const toastId = useRef(1);
const openThread = useCallback((channelId: string, parentId: string) => setThread({ channelId, parentId }), []);
const closeThread = useCallback(() => setThread(null), []);
const openProfile = useCallback((id: string) => setProfileUserId(id), []);
const closeProfile = useCallback(() => setProfileUserId(null), []);
const startHuddle = useCallback((id: string) => setHuddleChannelId(id), []);
const endHuddle = useCallback(() => setHuddleChannelId(null), []);
const openAi = useCallback((seed?: AiSeed) => setAiPanel(seed ?? {}), []);
const closeAi = useCallback(() => setAiPanel(null), []);
const openModal = useCallback((m: ModalKind, arg?: string) => {
setModal(m);
setModalArg(arg);
}, []);
const closeModal = useCallback(() => {
setModal(null);
setModalArg(undefined);
}, []);
useEffect(() => {
try {
if (localStorage.getItem("lynkd.sidebarCollapsed") === "1") setSidebarCollapsed(true);
} catch {
/* ignore */
}
}, []);
const toggleSidebarCollapsed = useCallback(() => {
setSidebarCollapsed((v) => {
const next = !v;
try {
localStorage.setItem("lynkd.sidebarCollapsed", next ? "1" : "0");
} catch {
/* ignore */
}
return next;
});
}, []);
const dismissToast = useCallback((id: number) => setToasts((t) => t.filter((x) => x.id !== id)), []);
const toast = useCallback((message: string, tone: Toast["tone"] = "default") => {
const id = toastId.current++;
setToasts((t) => [...t, { id, message, tone }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3200);
}, []);
// ⌘/Ctrl+K opens palette; Escape closes the top-most open surface.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setPaletteOpen((v) => !v);
return;
}
if (e.key === "Escape") {
// close the most transient surface first
if (paletteOpen) return setPaletteOpen(false);
if (modal) return closeModal();
if (notifOpen) return setNotifOpen(false);
if (profileUserId) return setProfileUserId(null);
if (themingOpen) return setThemingOpen(false);
if (aiPanel) return setAiPanel(null);
if (thread) return setThread(null);
if (sidebarOpen) return setSidebarOpen(false);
if (huddleChannelId) return setHuddleChannelId(null);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [paletteOpen, modal, notifOpen, profileUserId, themingOpen, aiPanel, thread, sidebarOpen, huddleChannelId, closeModal]);
const value = useMemo<UiState>(
() => ({
thread, openThread, closeThread,
paletteOpen, setPaletteOpen,
notifOpen, setNotifOpen,
profileUserId, openProfile, closeProfile,
themingOpen, setThemingOpen,
huddleChannelId, startHuddle, endHuddle,
aiPanel, openAi, closeAi,
sidebarOpen, setSidebarOpen,
sidebarCollapsed, toggleSidebarCollapsed,
modal, modalArg, openModal, closeModal,
toasts, toast, dismissToast,
}),
[thread, openThread, closeThread, paletteOpen, notifOpen, profileUserId, openProfile, closeProfile, themingOpen, huddleChannelId, startHuddle, endHuddle, aiPanel, openAi, closeAi, sidebarOpen, sidebarCollapsed, toggleSidebarCollapsed, modal, modalArg, openModal, closeModal, toasts, toast, dismissToast],
);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export function useUi(): UiState {
const ctx = useContext(Ctx);
if (!ctx) throw new Error("useUi must be used within <UiStateProvider>");
return ctx;
}