first commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user