From 07438e010e46c6a05b286c703b2fff0c9f5f6511 Mon Sep 17 00:00:00 2001 From: arnauda-gh <59512940+arnauda-gh@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:04:01 +0200 Subject: [PATCH] fix(composer): improve contact suggestion layout and prevent focus loss & duplicate recipients --- src/screens/ComposeScreen.tsx | 55 +++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/screens/ComposeScreen.tsx b/src/screens/ComposeScreen.tsx index 1dfda9ac..80f2a707 100644 --- a/src/screens/ComposeScreen.tsx +++ b/src/screens/ComposeScreen.tsx @@ -113,10 +113,11 @@ function RecipientChip({ recipient, onRemove }: { recipient: Recipient; onRemove } function SuggestionList({ - suggestions, onPick, + suggestions, onPick, onPressIn, }: { suggestions: Array<{ contact: ContactCard; name: string; email: string }>; onPick: (s: { name: string; email: string }) => void; + onPressIn?: () => void; }) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); @@ -127,6 +128,7 @@ function SuggestionList({ return ( onPick(s)} style={({ pressed }) => [styles.suggestionRow, pressed && styles.suggestionRowPressed]} > @@ -343,6 +345,7 @@ export default function ComposeScreen({ route, navigation }: Props) { }); const editorRef = React.useRef(null); + const isPickingSuggestion = React.useRef(false); // Track inline-image placeholders that haven't yet been rewritten to cid: // until send time. Maps cid → blobId/type/name/size. const inlineRegistryRef = React.useRef>(new Map()); @@ -375,6 +378,7 @@ export default function ComposeScreen({ route, navigation }: Props) { }, [suggestionQuery, individuals, alreadySelected]); const pickSuggestion = (s: { name: string; email: string }) => { + isPickingSuggestion.current = true; const recipient: Recipient = { name: s.name, email: s.email }; if (activeField === 'cc') { setCcRecipients((prev) => [...prev, recipient]); @@ -976,7 +980,7 @@ export default function ComposeScreen({ route, navigation }: Props) { @@ -993,7 +997,7 @@ export default function ComposeScreen({ route, navigation }: Props) { - + {t('email_composer.to', 'To')} {toRecipients.map((r, i) => ( @@ -1011,8 +1015,13 @@ export default function ComposeScreen({ route, navigation }: Props) { onChangeText={setToInput} onFocus={() => setActiveField('to')} onBlur={() => { - addTyped(); - setTimeout(() => setActiveField((f) => (f === 'to' ? null : f)), 200); + setTimeout(() => { + if (!isPickingSuggestion.current) { + addTyped(); + } + setActiveField((f) => (f === 'to' ? null : f)); + isPickingSuggestion.current = false; + }, 200); }} onSubmitEditing={addTyped} keyboardType="email-address" @@ -1021,7 +1030,13 @@ export default function ComposeScreen({ route, navigation }: Props) { /> {activeField === 'to' && suggestions.length > 0 && ( - + { + isPickingSuggestion.current = true; + }} + /> )} {!ccVisible && ( setCcVisible(true)} style={styles.ccToggle}> @@ -1031,7 +1046,7 @@ export default function ComposeScreen({ route, navigation }: Props) { {ccVisible && ( - + {t('email_composer.cc', 'Cc')} {ccRecipients.map((r, i) => ( @@ -1049,8 +1064,13 @@ export default function ComposeScreen({ route, navigation }: Props) { onChangeText={setCcInput} onFocus={() => setActiveField('cc')} onBlur={() => { - addTyped(); - setTimeout(() => setActiveField((f) => (f === 'cc' ? null : f)), 200); + setTimeout(() => { + if (!isPickingSuggestion.current) { + addTyped(); + } + setActiveField((f) => (f === 'cc' ? null : f)); + isPickingSuggestion.current = false; + }, 200); }} onSubmitEditing={addTyped} keyboardType="email-address" @@ -1059,7 +1079,13 @@ export default function ComposeScreen({ route, navigation }: Props) { /> {activeField === 'cc' && suggestions.length > 0 && ( - + { + isPickingSuggestion.current = true; + }} + /> )} )} @@ -1332,6 +1358,7 @@ function makeStyles(c: ThemePalette) { borderBottomWidth: 1, borderBottomColor: c.borderLight, gap: spacing.md, + position: 'relative', }, fieldLabel: { ...typography.body, @@ -1374,12 +1401,16 @@ function makeStyles(c: ThemePalette) { paddingVertical: 6, }, suggestionBox: { - marginTop: spacing.xs, - marginLeft: 60, + position: 'absolute', + top: '100%', + left: spacing.lg + 56 + spacing.md, + right: spacing.lg, borderWidth: 1, borderColor: c.borderLight, borderRadius: radius.sm, backgroundColor: c.card, + zIndex: 20, + elevation: 5, overflow: 'hidden', }, suggestionRow: {