feat(native): chat v1.0 — DM-only layout, search field, theme colors

Removes 2-tab Groups/DMs layout; Chat screen is now DM-only for v1.0.
Groups tab state, rooms query, RoomCard/CreateRoomSheet imports removed.
Replaces static title+create-button header with sticky search field
(client-side filter on partnerName + lastMessage). No create-DM button
added — /dm-new route does not exist yet (follow-up task).

All #007AFF in chat.tsx replaced with colors.brandOrange.
Adds chat.search_placeholder to de/en/fr locales.
Tab-bar styles kept in makeStyles (dead code, v1.1 Groups comeback path).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-16 01:28:04 +02:00
parent 500f673e53
commit a8ccfab274
4 changed files with 115 additions and 196 deletions

View File

@ -4,6 +4,7 @@ import {
Text, Text,
FlatList, FlatList,
TouchableOpacity, TouchableOpacity,
TextInput,
ActivityIndicator, ActivityIndicator,
Image, Image,
RefreshControl, RefreshControl,
@ -15,8 +16,6 @@ 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 { AppHeader } from '../../components/AppHeader'; import { AppHeader } from '../../components/AppHeader';
import { RoomCard, type Room } from '../../components/chat/RoomCard';
import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet';
import { useColors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
type DmConversation = { type DmConversation = {
@ -60,7 +59,7 @@ function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }
<Text style={styles.dmName} numberOfLines={1}> <Text style={styles.dmName} numberOfLines={1}>
{conv.partnerName} {conv.partnerName}
</Text> </Text>
<Text style={[styles.dmTime, { color: hasUnread ? '#007AFF' : colors.textMuted }]}> <Text style={[styles.dmTime, { color: hasUnread ? colors.brandOrange : colors.textMuted }]}>
{formatTime(conv.lastMessageAt, t('chat.just_now'))} {formatTime(conv.lastMessageAt, t('chat.just_now'))}
</Text> </Text>
</View> </View>
@ -97,19 +96,7 @@ export default function ChatScreen() {
const router = useRouter(); const router = useRouter();
const colors = useColors(); const colors = useColors();
const styles = makeStyles(colors); const styles = makeStyles(colors);
const [tab, setTab] = useState<'groups' | 'direct'>('groups'); const [search, setSearch] = useState('');
const [createOpen, setCreateOpen] = useState(false);
const {
data: rooms = [],
isLoading: loadingRooms,
isRefetching: refetchingRooms,
refetch: refetchRooms,
} = useQuery<Room[]>({
queryKey: ['chat-rooms'],
queryFn: () => apiFetch('/api/chat/rooms'),
staleTime: 30_000,
});
const { const {
data: convs = [], data: convs = [],
@ -120,17 +107,14 @@ export default function ChatScreen() {
queryKey: ['dm-conversations'], queryKey: ['dm-conversations'],
queryFn: () => apiFetch('/api/chat/dm-conversations'), queryFn: () => apiFetch('/api/chat/dm-conversations'),
staleTime: 30_000, staleTime: 30_000,
enabled: tab === 'direct',
}); });
const unreadDms = convs.reduce((s, c) => s + (c.unreadCount ?? 0), 0); const filtered = search.trim()
? convs.filter((c) =>
const openRoom = useCallback( c.partnerName.toLowerCase().includes(search.toLowerCase()) ||
(roomId: string) => { c.lastMessage.toLowerCase().includes(search.toLowerCase()),
router.push(`/room?roomId=${roomId}`); )
}, : convs;
[router],
);
const openDm = useCallback( const openDm = useCallback(
(userId: string) => { (userId: string) => {
@ -143,100 +127,43 @@ export default function ChatScreen() {
<View style={styles.container}> <View style={styles.container}>
<AppHeader /> <AppHeader />
{/* Header */} {/* Search header */}
<View style={styles.headerSection}> <View style={styles.headerSection}>
<View style={styles.titleRow}> <View style={styles.searchRow}>
<Text style={styles.title}>{t('chat.title')}</Text> <Ionicons name="search" size={16} color={colors.textMuted} style={styles.searchIcon} />
{tab === 'groups' && ( <TextInput
<TouchableOpacity style={styles.searchInput}
onPress={() => setCreateOpen(true)} value={search}
activeOpacity={0.7} onChangeText={setSearch}
style={styles.createBtn} placeholder={t('chat.search_placeholder')}
> placeholderTextColor={colors.textMuted}
<Ionicons name="add" size={20} color="#fff" /> returnKeyType="search"
clearButtonMode="never"
autoCorrect={false}
autoCapitalize="none"
/>
{search.length > 0 && (
<TouchableOpacity onPress={() => setSearch('')} activeOpacity={0.7} hitSlop={8}>
<Ionicons name="close-circle" size={16} color={colors.textMuted} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
{/* Tabs */}
<View style={styles.tabs}>
<TouchableOpacity
onPress={() => setTab('groups')}
activeOpacity={0.7}
style={[styles.tab, tab === 'groups' && styles.tabActive]}
>
<Ionicons
name="people"
size={14}
color={tab === 'groups' ? '#007AFF' : '#737373'}
/>
<Text style={[styles.tabText, tab === 'groups' && styles.tabTextActive]}>
{t('chat.groups')}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setTab('direct')}
activeOpacity={0.7}
style={[styles.tab, tab === 'direct' && styles.tabActive]}
>
<Ionicons
name="chatbubbles"
size={14}
color={tab === 'direct' ? '#007AFF' : '#737373'}
/>
<Text style={[styles.tabText, tab === 'direct' && styles.tabTextActive]}>
{t('chat.direct')}
</Text>
{unreadDms > 0 && (
<View style={styles.tabBadge}>
<Text style={styles.tabBadgeText}>{unreadDms}</Text>
</View>
)}
</TouchableOpacity>
</View>
</View> </View>
{tab === 'groups' ? (
<FlatList <FlatList
data={loadingRooms ? [] : rooms} data={loadingDms ? [] : filtered}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl
refreshing={refetchingRooms}
onRefresh={refetchRooms}
tintColor="#007AFF"
/>
}
ListEmptyComponent={
loadingRooms ? (
<View style={styles.emptyBox}>
<ActivityIndicator color="#007AFF" />
</View>
) : (
<View style={styles.emptyBox}>
<Ionicons name="people-outline" size={42} color="#d4d4d4" />
<Text style={styles.emptyText}>{t('chat.no_rooms')}</Text>
</View>
)
}
renderItem={({ item }) => <RoomCard room={item} onPress={() => openRoom(item.id)} />}
contentContainerStyle={{ paddingBottom: 100 }}
/>
) : (
<FlatList
data={loadingDms ? [] : convs}
keyExtractor={(item) => item.partnerId} keyExtractor={(item) => item.partnerId}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refetchingDms} refreshing={refetchingDms}
onRefresh={refetchDms} onRefresh={refetchDms}
tintColor="#007AFF" tintColor={colors.brandOrange}
/> />
} }
ListEmptyComponent={ ListEmptyComponent={
loadingDms ? ( loadingDms ? (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
<ActivityIndicator color="#007AFF" /> <ActivityIndicator color={colors.brandOrange} />
</View> </View>
) : ( ) : (
<View style={styles.emptyBox}> <View style={styles.emptyBox}>
@ -248,16 +175,6 @@ export default function ChatScreen() {
renderItem={({ item }) => <DmItem conv={item} onPress={() => openDm(item.partnerId)} />} renderItem={({ item }) => <DmItem conv={item} onPress={() => openDm(item.partnerId)} />}
contentContainerStyle={{ paddingBottom: 100 }} contentContainerStyle={{ paddingBottom: 100 }}
/> />
)}
<CreateRoomSheet
visible={createOpen}
onClose={() => setCreateOpen(false)}
onCreated={(room) => {
refetchRooms();
openRoom(room.id);
}}
/>
</View> </View>
); );
} }
@ -267,76 +184,27 @@ function makeStyles(colors: ReturnType<typeof useColors>) {
container: { flex: 1, backgroundColor: colors.bg }, container: { flex: 1, backgroundColor: colors.bg },
headerSection: { headerSection: {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 14, paddingTop: 12,
paddingBottom: 10, paddingBottom: 10,
backgroundColor: colors.bg, backgroundColor: colors.bg,
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border, borderBottomColor: colors.border,
}, },
titleRow: { searchRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontSize: 22,
fontFamily: 'Nunito_800ExtraBold',
color: colors.text,
},
createBtn: {
width: 34,
height: 34,
borderRadius: 17,
backgroundColor: '#007AFF',
alignItems: 'center',
justifyContent: 'center',
},
tabs: {
flexDirection: 'row',
marginTop: 12,
backgroundColor: colors.surfaceElevated, backgroundColor: colors.surfaceElevated,
borderRadius: 10, borderRadius: 10,
padding: 3, paddingHorizontal: 10,
paddingVertical: 8,
}, },
tab: { searchIcon: { marginRight: 7 },
searchInput: {
flex: 1, flex: 1,
flexDirection: 'row', fontSize: 14,
alignItems: 'center', fontFamily: 'Nunito_500Medium',
justifyContent: 'center', color: colors.text,
paddingVertical: 7, paddingVertical: 0,
borderRadius: 8,
},
tabActive: {
backgroundColor: colors.surface,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
},
tabText: {
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
marginLeft: 5,
},
tabTextActive: {
color: '#007AFF',
fontFamily: 'Nunito_700Bold',
},
tabBadge: {
minWidth: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#007AFF',
paddingHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5,
},
tabBadgeText: {
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: '#fff',
}, },
emptyBox: { emptyBox: {
alignItems: 'center', alignItems: 'center',
@ -403,7 +271,7 @@ function makeStyles(colors: ReturnType<typeof useColors>) {
height: 20, height: 20,
paddingHorizontal: 6, paddingHorizontal: 6,
borderRadius: 10, borderRadius: 10,
backgroundColor: '#007AFF', backgroundColor: colors.brandOrange,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginLeft: 8, marginLeft: 8,
@ -413,5 +281,53 @@ function makeStyles(colors: ReturnType<typeof useColors>) {
fontFamily: 'Nunito_700Bold', fontFamily: 'Nunito_700Bold',
color: '#fff', color: '#fff',
}, },
// Kept for v1.1 Groups comeback — tab styles no longer rendered
tabs: {
flexDirection: 'row',
marginTop: 12,
backgroundColor: colors.surfaceElevated,
borderRadius: 10,
padding: 3,
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 7,
borderRadius: 8,
},
tabActive: {
backgroundColor: colors.surface,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
},
tabText: {
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
marginLeft: 5,
},
tabTextActive: {
color: colors.brandOrange,
fontFamily: 'Nunito_700Bold',
},
tabBadge: {
minWidth: 16,
height: 16,
borderRadius: 8,
backgroundColor: colors.brandOrange,
paddingHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5,
},
tabBadgeText: {
fontSize: 9,
fontFamily: 'Nunito_700Bold',
color: '#fff',
},
}); });
} }

View File

@ -741,7 +741,8 @@
"approve": "Annehmen", "approve": "Annehmen",
"reject": "Ablehnen", "reject": "Ablehnen",
"avatar_updated": "Gruppenbild aktualisiert", "avatar_updated": "Gruppenbild aktualisiert",
"send": "Senden" "send": "Senden",
"search_placeholder": "Konversationen durchsuchen…"
}, },
"community": { "community": {
"compose_placeholder": "Was bewegt dich gerade?", "compose_placeholder": "Was bewegt dich gerade?",

View File

@ -741,7 +741,8 @@
"approve": "Approve", "approve": "Approve",
"reject": "Reject", "reject": "Reject",
"avatar_updated": "Group photo updated", "avatar_updated": "Group photo updated",
"send": "Send" "send": "Send",
"search_placeholder": "Search conversations…"
}, },
"community": { "community": {
"compose_placeholder": "What's on your mind?", "compose_placeholder": "What's on your mind?",

View File

@ -741,7 +741,8 @@
"approve": "Accepter", "approve": "Accepter",
"reject": "Refuser", "reject": "Refuser",
"avatar_updated": "Photo du groupe mise à jour", "avatar_updated": "Photo du groupe mise à jour",
"send": "Envoyer" "send": "Envoyer",
"search_placeholder": "Rechercher des conversations…"
}, },
"community": { "community": {
"compose_placeholder": "Qu'est-ce qui vous préoccupe en ce moment ?", "compose_placeholder": "Qu'est-ce qui vous préoccupe en ce moment ?",