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