feat(native/chat): partner avatars + pill-shape search field

- DmItem now goes through resolveAvatar(partnerAvatar, partnerName) so
  the Dicebear-initials fallback kicks in for null avatars, hero ids
  resolve to their image url, and direct URLs pass through. Adds the
  PostCard-style avatarLoadFailed state for graceful broken-image
  fallback.
- Search row pill-shaped (borderRadius 999) with 16px horizontal padding
  and the outline search icon for better visual rhythm.
This commit is contained in:
chahinebrini 2026-05-16 01:51:59 +02:00
parent 40ccefab5b
commit d11d548c10

View File

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@ -15,6 +15,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { resolveAvatar } from '../../lib/resolveAvatar';
import { AppHeader } from '../../components/AppHeader'; import { AppHeader } from '../../components/AppHeader';
import { useColors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
@ -42,16 +43,23 @@ function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }
const styles = makeStyles(colors); const styles = makeStyles(colors);
const hasUnread = conv.unreadCount > 0; const hasUnread = conv.unreadCount > 0;
const avatarUrl = resolveAvatar(conv.partnerAvatar, conv.partnerName);
const [avatarLoadFailed, setAvatarLoadFailed] = useState(false);
useEffect(() => { setAvatarLoadFailed(false); }, [avatarUrl]);
const avatarInitials = (conv.partnerName.slice(0, 2)).toUpperCase() || '?';
return ( return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7}> <TouchableOpacity onPress={onPress} activeOpacity={0.7}>
<View style={styles.dmRow}> <View style={styles.dmRow}>
<View style={styles.dmAvatar}> <View style={styles.dmAvatar}>
{conv.partnerAvatar ? ( {!avatarLoadFailed ? (
<Image source={{ uri: conv.partnerAvatar }} style={styles.dmAvatarImg} /> <Image
source={{ uri: avatarUrl }}
style={styles.dmAvatarImg}
onError={() => setAvatarLoadFailed(true)}
/>
) : ( ) : (
<Text style={styles.dmAvatarInitials}> <Text style={styles.dmAvatarInitials}>{avatarInitials}</Text>
{conv.partnerName.slice(0, 2).toUpperCase()}
</Text>
)} )}
</View> </View>
<View style={styles.dmInfo}> <View style={styles.dmInfo}>
@ -130,7 +138,7 @@ export default function ChatScreen() {
{/* Search header */} {/* Search header */}
<View style={styles.headerSection}> <View style={styles.headerSection}>
<View style={styles.searchRow}> <View style={styles.searchRow}>
<Ionicons name="search" size={16} color={colors.textMuted} style={styles.searchIcon} /> <Ionicons name="search-outline" size={16} color={colors.textMuted} style={styles.searchIcon} />
<TextInput <TextInput
style={styles.searchInput} style={styles.searchInput}
value={search} value={search}
@ -194,11 +202,11 @@ function makeStyles(colors: ReturnType<typeof useColors>) {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.surfaceElevated, backgroundColor: colors.surfaceElevated,
borderRadius: 10, borderRadius: 999,
paddingHorizontal: 10, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 8,
}, },
searchIcon: { marginRight: 7 }, searchIcon: { marginRight: 8 },
searchInput: { searchInput: {
flex: 1, flex: 1,
fontSize: 14, fontSize: 14,