-
-
Notifications
You must be signed in to change notification settings - Fork 37
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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 everythingPhase 3: Cleanup
- Remove
isNewCastModalOpen,castModalDraftId,castModalViewfromuseNavigationStore - Remove
isDraftsModalOpenfromuseDraftStore
Acceptance Criteria
- Create
useComposerStatestore with all state fields - Implement
openNewComposer,openReplyComposer,openQuoteComposer - Implement
closeComposerwith smart draft cleanup - Implement
switchToDraftfor switching between drafts - Update
CastRow.tsxto use new hook for reply/quote - Update
feeds/page.tsxto use new hook - Update
inbox/page.tsxto use new hook - Update
home/index.tsxto use new hook - Remove old state from
useNavigationStore - Remove
isDraftsModalOpenfromuseDraftStore
Files to Create
src/stores/useComposerState.ts- New hook
Files 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 hook
Dependencies
- Depends on Bug: Draft store issues causing data loss and race conditions #667 (Draft store bug fixes)
- Depends on Refactor: Extend draft store to support thread composition #668 (Thread draft support) for thread mode
Related Issues
- Enables cleaner composer UI redesign
- Enables Feature: Thread composer with drag-and-drop reordering #666 (Thread composer)
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request