Summary
Extract composer UI state from useNavigationStore and useDraftStore into a dedicated useComposerState hook. This separates concerns: data (drafts) vs UI (modal open/close, active draft selection).
Current Problem
Composer state is scattered across two stores:
// useNavigationStore.ts
isNewCastModalOpen: boolean;
castModalDraftId?: UUID;
castModalView: CastModalView; // 'new' | 'reply' | 'quote'
// useDraftStore.ts
isDraftsModalOpen: boolean; // Different modal\!
This creates:
- Confusion about which store to use
- Tight coupling between draft creation and modal opening
- Duplicate modal state for related features
Proposed Solution
Create useComposerState hook that owns all composer UI state:
// src/stores/useComposerState.ts
interface ComposerState {
// Modal state
isOpen: boolean;
// Active draft
activeDraftId: UUID | null;
// Compose mode
mode: 'new' | 'reply' | 'quote';
// Context for reply/quote (the cast being replied to or quoted)
targetCast: CastType | null;
// For thread mode
isThreadMode: boolean;
activeThreadId: UUID | null;
}
interface ComposerActions {
// Open composer
openNewComposer: (options?: { parentUrl?: string }) => void;
openReplyComposer: (cast: CastType) => void;
openQuoteComposer: (cast: CastType) => void;
openThreadComposer: () => void;
// Close composer
closeComposer: (options?: { keepDraft?: boolean }) => void;
// Draft management within composer
switchToDraft: (draftId: UUID) => void;
// Thread mode
toggleThreadMode: () => void;
}
Key Behaviors
Smart Close Logic
closeComposer: ({ keepDraft = false } = {}) => {
const { activeDraftId } = get();
const draft = useDraftStore.getState().getDraft(activeDraftId);
if (\!keepDraft && draft) {
const isEmpty = \!draft.text?.trim() && \!draft.embeds?.length;
const wasPublished = draft.status === 'published';
if (isEmpty || wasPublished) {
useDraftStore.getState().deleteDraft(activeDraftId);
}
// Content exists → keep draft, user can find it in /post
}
set({
isOpen: false,
activeDraftId: null,
targetCast: null,
});
}
Open Reply Composer
openReplyComposer: (cast: CastType) => {
const { createDraft, findDraftByParent } = useDraftStore.getState();
// Check for existing reply draft
const existingDraft = findDraftByParent(undefined, {
fid: parseInt(cast.author.fid),
hash: cast.hash,
});
const draftId = existingDraft?.id ?? createDraft({
parentCastId: { fid: parseInt(cast.author.fid), hash: cast.hash },
});
set({
isOpen: true,
activeDraftId: draftId,
mode: 'reply',
targetCast: cast,
});
}
Migration Path
Phase 1: Create Hook
- Create
src/stores/useComposerState.ts
- Implement all state and actions
- Add to app initialization
Phase 2: Update Components
Replace scattered state usage:
// Before (in CastRow.tsx)
const { setCastModalDraftId, setCastModalView, openNewCastModal } = useNavigationStore();
addNewPostDraft({
parentCastId: {...},
onSuccess(draftId) {
setCastModalDraftId(draftId);
setCastModalView(CastModalView.Reply);
openNewCastModal();
},
});
// After
const { openReplyComposer } = useComposerState();
openReplyComposer(cast); // One call does everything
Phase 3: Cleanup
- Remove
isNewCastModalOpen, castModalDraftId, castModalView from useNavigationStore
- Remove
isDraftsModalOpen from useDraftStore
Acceptance Criteria
Files to Create
src/stores/useComposerState.ts - New hook
Files to Modify
src/stores/useNavigationStore.ts - Remove composer state
src/stores/useDraftStore.ts - Remove isDraftsModalOpen
src/common/components/CastRow.tsx - Use new hook
src/common/components/NewCastModal.tsx - Use new hook
app/(app)/feeds/page.tsx - Use new hook
app/(app)/inbox/page.tsx - Use new hook
src/home/index.tsx - Use new hook
Dependencies
Related Issues
Summary
Extract composer UI state from
useNavigationStoreanduseDraftStoreinto a dedicateduseComposerStatehook. This separates concerns: data (drafts) vs UI (modal open/close, active draft selection).Current Problem
Composer state is scattered across two stores:
This creates:
Proposed Solution
Create
useComposerStatehook that owns all composer UI state:Key Behaviors
Smart Close Logic
Open Reply Composer
Migration Path
Phase 1: Create Hook
src/stores/useComposerState.tsPhase 2: Update Components
Replace scattered state usage:
Phase 3: Cleanup
isNewCastModalOpen,castModalDraftId,castModalViewfromuseNavigationStoreisDraftsModalOpenfromuseDraftStoreAcceptance Criteria
useComposerStatestore with all state fieldsopenNewComposer,openReplyComposer,openQuoteComposercloseComposerwith smart draft cleanupswitchToDraftfor switching between draftsCastRow.tsxto use new hook for reply/quotefeeds/page.tsxto use new hookinbox/page.tsxto use new hookhome/index.tsxto use new hookuseNavigationStoreisDraftsModalOpenfromuseDraftStoreFiles to Create
src/stores/useComposerState.ts- New hookFiles to Modify
src/stores/useNavigationStore.ts- Remove composer statesrc/stores/useDraftStore.ts- RemoveisDraftsModalOpensrc/common/components/CastRow.tsx- Use new hooksrc/common/components/NewCastModal.tsx- Use new hookapp/(app)/feeds/page.tsx- Use new hookapp/(app)/inbox/page.tsx- Use new hooksrc/home/index.tsx- Use new hookDependencies
Related Issues