a tool for shared writing and social publishing

use scoped context for interactionsdrawer

+89 -27
+5 -5
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx
··· 19 19 import { publishComment } from "./commentAction"; 20 20 import { ButtonPrimary } from "components/Buttons"; 21 21 import { ShareSmall } from "components/Icons/ShareSmall"; 22 - import { useInteractionState } from "../Interactions"; 22 + import { useInteractionState, setInteractionState } from "../Interactions"; 23 23 import { DotLoader } from "components/utils/DotLoader"; 24 24 import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 25 25 import { setMark } from "src/utils/prosemirror/setMark"; ··· 43 43 autoFocus?: boolean; 44 44 }) { 45 45 let mountRef = useRef<HTMLPreElement | null>(null); 46 - let quote = useInteractionState((s) => s.commentBox.quote); 46 + let { commentBox: { quote } } = useInteractionState(props.doc_uri); 47 47 let [editorState, setEditorState] = useState(() => 48 48 EditorState.create({ 49 49 schema: multiBlockSchema, ··· 118 118 if (!quoteParam) return; 119 119 const quotePosition = decodeQuotePosition(quoteParam); 120 120 if (!quotePosition) return; 121 - useInteractionState.setState({ 121 + setInteractionState(props.doc_uri, { 122 122 commentBox: { quote: quotePosition }, 123 123 }); 124 124 return true; ··· 165 165 <button 166 166 className="text-border absolute -top-3 right-1 bg-bg-page p-1 rounded-full" 167 167 onClick={() => 168 - useInteractionState.setState({ commentBox: { quote: null } }) 168 + setInteractionState(props.doc_uri, { commentBox: { quote: null } }) 169 169 } 170 170 > 171 171 <CloseFillTiny /> ··· 230 230 view.current?.dispatch(tr); 231 231 setLoading(false); 232 232 props.onSubmit?.(); 233 - useInteractionState.setState((s) => ({ 233 + setInteractionState(props.doc_uri, (s) => ({ 234 234 commentBox: { 235 235 quote: null, 236 236 },
+3 -3
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 1 1 "use client"; 2 2 import { CloseTiny } from "components/Icons/CloseTiny"; 3 - import { useInteractionState } from "../Interactions"; 3 + import { useInteractionState, setInteractionState } from "../Interactions"; 4 4 import { useIdentityData } from "components/IdentityProvider"; 5 5 import { CommentBox } from "./CommentBox"; 6 6 import { Json } from "supabase/database.types"; ··· 25 25 }; 26 26 export function Comments(props: { document_uri: string; comments: Comment[] }) { 27 27 let { identity } = useIdentityData(); 28 - let localComments = useInteractionState((l) => l.localComments); 28 + let { localComments } = useInteractionState(props.document_uri); 29 29 let comments = useMemo(() => { 30 30 return [...localComments, ...props.comments]; 31 31 }, [props.comments, localComments]); ··· 44 44 Comments 45 45 <button 46 46 className="text-tertiary" 47 - onClick={() => useInteractionState.setState({ drawerOpen: false })} 47 + onClick={() => setInteractionState(props.document_uri, { drawerOpen: false })} 48 48 > 49 49 <CloseTiny /> 50 50 </button>
+68 -13
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 6 6 import { create } from "zustand"; 7 7 import type { Comment } from "./Comments"; 8 8 import { QuotePosition } from "../quotePosition"; 9 + import { useContext } from "react"; 10 + import { PostPageContext } from "../PostPageContext"; 9 11 10 - export let useInteractionState = create(() => ({ 11 - drawerOpen: undefined as boolean | undefined, 12 - drawer: undefined as undefined | "comments" | "quotes", 13 - localComments: [] as Comment[], 14 - commentBox: { quote: null as QuotePosition | null }, 15 - })); 16 - export function openInteractionDrawer(drawer: "comments" | "quotes") { 12 + type InteractionState = { 13 + drawerOpen: undefined | boolean; 14 + drawer: undefined | "comments" | "quotes"; 15 + localComments: Comment[]; 16 + commentBox: { quote: QuotePosition | null }; 17 + }; 18 + 19 + const defaultInteractionState: InteractionState = { 20 + drawerOpen: undefined, 21 + drawer: undefined, 22 + localComments: [], 23 + commentBox: { quote: null }, 24 + }; 25 + 26 + export let useInteractionStateStore = create<{ 27 + [document_uri: string]: InteractionState; 28 + }>(() => ({})); 29 + 30 + export function useInteractionState(document_uri?: string) { 31 + return useInteractionStateStore((state) => { 32 + if (!document_uri || !state[document_uri]) { 33 + return defaultInteractionState; 34 + } 35 + return state[document_uri]; 36 + }); 37 + } 38 + 39 + export function setInteractionState( 40 + document_uri: string, 41 + update: 42 + | Partial<InteractionState> 43 + | ((state: InteractionState) => Partial<InteractionState>), 44 + ) { 45 + useInteractionStateStore.setState((state) => { 46 + if (!state[document_uri]) { 47 + state[document_uri] = { ...defaultInteractionState }; 48 + } 49 + 50 + const currentDocState = state[document_uri]; 51 + const updatedState = 52 + typeof update === "function" ? update(currentDocState) : update; 53 + 54 + return { 55 + ...state, 56 + [document_uri]: { 57 + ...currentDocState, 58 + ...updatedState, 59 + }, 60 + }; 61 + }); 62 + } 63 + export function openInteractionDrawer( 64 + drawer: "comments" | "quotes", 65 + document_uri: string, 66 + ) { 17 67 flushSync(() => { 18 - useInteractionState.setState({ drawerOpen: true, drawer }); 68 + setInteractionState(document_uri, { drawerOpen: true, drawer }); 19 69 }); 20 70 let el = document.getElementById("interaction-drawer"); 21 71 let isOffscreen = false; ··· 38 88 className?: string; 39 89 showComments?: boolean; 40 90 }) => { 41 - let { drawerOpen, drawer } = useInteractionState(); 91 + const data = useContext(PostPageContext); 92 + const document_uri = data?.uri; 93 + if (!document_uri) 94 + throw new Error("document_uri not available in PostPageContext"); 95 + 96 + let { drawerOpen, drawer } = useInteractionState(document_uri); 42 97 43 98 return ( 44 99 <div ··· 48 103 className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 49 104 onClick={() => { 50 105 if (!drawerOpen || drawer !== "quotes") 51 - openInteractionDrawer("quotes"); 52 - else useInteractionState.setState({ drawerOpen: false }); 106 + openInteractionDrawer("quotes", document_uri); 107 + else setInteractionState(document_uri, { drawerOpen: false }); 53 108 }} 54 109 > 55 110 <QuoteTiny /> {props.quotesCount}{" "} ··· 60 115 className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 61 116 onClick={() => { 62 117 if (!drawerOpen || drawer !== "comments") 63 - openInteractionDrawer("comments"); 64 - else useInteractionState.setState({ drawerOpen: false }); 118 + openInteractionDrawer("comments", document_uri); 119 + else setInteractionState(document_uri, { drawerOpen: false }); 65 120 }} 66 121 > 67 122 <CommentTiny /> {props.commentsCount}{" "}
+4 -2
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 2 2 import { CloseTiny } from "components/Icons/CloseTiny"; 3 3 import { useContext } from "react"; 4 4 import { useIsMobile } from "src/hooks/isMobile"; 5 - import { useInteractionState } from "./Interactions"; 5 + import { setInteractionState } from "./Interactions"; 6 6 import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 7 7 import { AtUri } from "@atproto/api"; 8 8 import { Json } from "supabase/database.types"; ··· 25 25 did: string; 26 26 }) => { 27 27 let data = useContext(PostPageContext); 28 + const document_uri = data?.uri; 29 + if (!document_uri) throw new Error('document_uri not available in PostPageContext'); 28 30 29 31 return ( 30 32 <div className="flex flex-col gap-2"> ··· 32 34 Quotes 33 35 <button 34 36 className="text-tertiary" 35 - onClick={() => useInteractionState.setState({ drawerOpen: false })} 37 + onClick={() => setInteractionState(document_uri, { drawerOpen: false })} 36 38 > 37 39 <CloseTiny /> 38 40 </button>
+2 -1
app/lish/[did]/[publication]/[rkey]/PostPage.tsx
··· 36 36 preferences: { showComments?: boolean }; 37 37 }) { 38 38 let { identity } = useIdentityData(); 39 - let { drawerOpen } = useInteractionState(); 39 + const document_uri = document?.uri; 40 + let { drawerOpen } = useInteractionState(document_uri); 40 41 if (!document || !document.documents_in_publications[0].publications) 41 42 return null; 42 43
+7 -3
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
··· 3 3 import { CopyTiny } from "components/Icons/CopyTiny"; 4 4 import { Separator } from "components/Layout"; 5 5 import { useSmoker } from "components/Toast"; 6 - import { useEffect, useMemo, useState } from "react"; 6 + import { useEffect, useMemo, useState, useContext } from "react"; 7 7 import { 8 8 encodeQuotePosition, 9 9 decodeQuotePosition, ··· 11 11 } from "./quotePosition"; 12 12 import { useIdentityData } from "components/IdentityProvider"; 13 13 import { CommentTiny } from "components/Icons/CommentTiny"; 14 - import { useInteractionState } from "./Interactions/Interactions"; 14 + import { setInteractionState } from "./Interactions/Interactions"; 15 + import { PostPageContext } from "./PostPageContext"; 15 16 16 17 export function QuoteHandler() { 17 18 let [position, setPosition] = useState<{ ··· 128 129 export const QuoteOptionButtons = (props: { position: string }) => { 129 130 let smoker = useSmoker(); 130 131 let { identity } = useIdentityData(); 132 + const data = useContext(PostPageContext); 133 + const document_uri = data?.uri; 134 + if (!document_uri) throw new Error('document_uri not available in PostPageContext'); 131 135 let [url, position] = useMemo(() => { 132 136 let currentUrl = new URL(window.location.href); 133 137 let pos = decodeQuotePosition(props.position); ··· 186 190 className="flex gap-1 items-center hover:font-bold px-1" 187 191 onClick={() => { 188 192 if (!position) return; 189 - useInteractionState.setState({ 193 + setInteractionState(document_uri, { 190 194 drawer: "comments", 191 195 drawerOpen: true, 192 196 commentBox: { quote: position },