import { useEffect, useMemo, useRef, useState } from 'react' import { ActivityIndicator, FlatList, InteractionManager, Pressable, Text, TextInput, View } from 'react-native' import type { RpcClient } from '../transport/rpc-client' import type { SmartWorkspaceSourceRow as SourceRow } from '../../../src/shared/new-workspace/smart-workspace-source-results' import { MR_STATE_FILTER_OPTIONS, resolveAvailableSmartModes, resolveDefaultSmartMode, SMART_MODE_OPTIONS, type SmartModeAvailabilityInput, type SmartModeOption } from '../tasks/mobile-smart-source-modes' import type { MrStateFilter, SmartNameMode } from '../tasks/mobile-composer-source-types' import { lookupGitHubItemByOwnerRepo, type PasteRepoCandidate } from '../tasks/smart-source-paste-intent' import { useSmartWorkspaceSource } from '../tasks/use-smart-workspace-source' import type { MobileComposerSource } from '../tasks/use-mobile-composer-source' import { colors } from '../theme/mobile-theme' import { BottomDrawer } from './BottomDrawer' import { smartWorkspaceSourceDrawerStyles as styles } from './smart-workspace-source-drawer-styles' import { SmartSourceModeIcon } from './SmartSourceModeIcon' import { SmartWorkspaceSourceRow } from './SmartWorkspaceSourceRow' // Why: match MobileSearchField — native autoFocus alone often fails to raise // the soft keyboard when the drawer is mid-present animation. const SOURCE_INPUT_FOCUS_DELAY_MS = 120 type Props = { visible: boolean client: RpcClient | null composer: MobileComposerSource availability: SmartModeAvailabilityInput repoId: string | null repos: readonly PasteRepoCandidate[] linearWorkspaceId?: string | null sshReady: boolean onRepoChange: (repoId: string) => void onClose: () => void } export function SmartWorkspaceSourceDrawer({ visible, client, composer, availability, repoId, repos, linearWorkspaceId, sshReady, onRepoChange, onClose }: Props) { const availableModes = useMemo(() => resolveAvailableSmartModes(availability), [availability]) const [mode, setMode] = useState(() => resolveDefaultSmartMode(availability)) const [mrStateFilter, setMrStateFilter] = useState('opened') const inputRef = useRef(null) // Why: read latest availability inside the open effect without making it a // reactive dep (the object is recreated each render), so re-seeding happens // only on open, not on every availability recompute. const availabilityRef = useRef(availability) availabilityRef.current = availability // Reset to the default mode each time the drawer opens. useEffect(() => { if (visible) { setMode(resolveDefaultSmartMode(availabilityRef.current)) } }, [visible]) // Why: focus after open interactions settle so the keyboard appears and the // caret lands in the docked field (same value as the form via composer.name). useEffect(() => { if (!visible) { return } let timeout: ReturnType | undefined const task = InteractionManager.runAfterInteractions(() => { timeout = setTimeout(() => { inputRef.current?.focus() }, SOURCE_INPUT_FOCUS_DELAY_MS) }) return () => { task.cancel() if (timeout) { clearTimeout(timeout) } } }, [visible]) // Snap the chosen mode back into the available set if availability changes. const effectiveMode = availableModes.includes(mode) ? mode : (availableModes[0] ?? 'text') // Linear searches without a repo; every other provider/branch search needs a // connected repo-backed target. const searchEnabled = visible && (effectiveMode === 'linear' || sshReady) const { rows, loading, error, needsGitHubRemote, emptyHint, crossRepoPrompt, dismissCrossRepoPrompt } = useSmartWorkspaceSource({ client, enabled: searchEnabled, mode: effectiveMode, query: composer.name, repoId, githubAvailable: availability.githubAvailable, gitlabAvailable: availability.gitlabAvailable, linearAvailable: availability.linearAvailable, mrStateFilter, linearWorkspaceId, repos }) function handleSelectRow(row: SourceRow): void { switch (row.kind) { case 'use-name': composer.setName(row.name) break case 'create-branch': composer.handleSmartCreateBranch(row.name) break case 'github': composer.handleSmartGitHubItemSelect(row.item) break case 'gitlab': composer.handleSmartGitLabItemSelect(row.item) break case 'branch': composer.handleSmartBranchSelect(row.refName, row.localBranchName) break case 'linear': composer.handleSmartLinearIssueSelect(row.issue) break } onClose() } async function handleAcceptCrossRepo(): Promise { if (!client || !crossRepoPrompt) { return } const { link, matchingRepo } = crossRepoPrompt try { const item = await lookupGitHubItemByOwnerRepo( client, matchingRepo.id, link.slug, link.number, link.type ) if (item) { onRepoChange(matchingRepo.id) composer.handleSmartGitHubItemSelect(item) onClose() } } catch { dismissCrossRepoPrompt() } } const showEmpty = !loading && !error && !needsGitHubRemote && effectiveMode !== 'text' && rows.length === 0 const modeTabs = SMART_MODE_OPTIONS.filter((option: SmartModeOption) => availableModes.includes(option.id) ) return ( {/* Why: column with results flex:1 + dock flex-shrink:0 at the end. Fill sheet height + marginBottom place this column on the keyboard top; dock must stay a non-flex sibling so FlatList cannot clip it. */} Name or 'Create From' Done {crossRepoPrompt ? ( This item lives in {crossRepoPrompt.link.slug.owner}/ {crossRepoPrompt.link.slug.repo}. Cancel void handleAcceptCrossRepo()} > Switch to {crossRepoPrompt.matchingRepo.displayName} ) : null} {!sshReady && effectiveMode !== 'text' && effectiveMode !== 'linear' ? ( Connect the repository to search sources. ) : needsGitHubRemote ? ( This SSH repo needs a GitHub remote to list issues and PRs. ) : error ? ( {error} ) : null} row.value} style={styles.list} contentContainerStyle={styles.listContent} keyboardShouldPersistTaps="handled" keyboardDismissMode="none" nestedScrollEnabled ListFooterComponent={ loading ? ( ) : showEmpty ? ( {emptyHint || 'No results found.'} ) : rows.length === 0 && effectiveMode === 'text' ? ( Type a workspace name in the field below. ) : null } renderItem={({ item }) => ( handleSelectRow(item)} /> )} /> {effectiveMode === 'gitlab' ? ( {MR_STATE_FILTER_OPTIONS.map((option) => { const selected = option.id === mrStateFilter return ( setMrStateFilter(option.id)} > {option.label} ) })} ) : null} {modeTabs.map((option) => { const selected = option.id === effectiveMode const tint = selected ? colors.textPrimary : colors.textSecondary return ( setMode(option.id)} > {option.label} ) })} ) }