178 lines
6.1 KiB
TypeScript
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;
|
|
}
|