From bd2f0358c21a57b3cbb1559d48d73dde9217653a Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Wed, 11 Nov 2026 18:24:48 +0100 Subject: [PATCH] fix: improve drawer layout --- .../ExpoCrewAssistant/screens/MenuDrawer.tsx | 176 +++++++++++++++--- 1 file changed, 146 insertions(+), 30 deletions(-) diff --git a/examples/ExpoCrewAssistant/screens/MenuDrawer.tsx b/examples/ExpoCrewAssistant/screens/MenuDrawer.tsx index 472d7172bd..bbe3e3e367 100644 --- a/examples/ExpoCrewAssistant/screens/MenuDrawer.tsx +++ b/examples/ExpoCrewAssistant/screens/MenuDrawer.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -10,10 +10,13 @@ import { type DrawerContentComponentProps, useDrawerStatus } from 'expo-router/d import { Plus, useStableCallback } from 'stream-chat-expo'; import { agentSessions } from '../agent/agents'; +import { AgentConfig } from '../agent/config'; import { useAppContext } from '../contexts/AppContext'; type SessionRow = Schemas['Session']; +type Section = { data: SessionRow[]; title: string }; + const keyExtractor = (session: SessionRow) => session.id; // When the conversation was last answered, or else started. The router lists conversations by when @@ -22,8 +25,43 @@ const keyExtractor = (session: SessionRow) => session.id; const lastUsed = (session: SessionRow) => Date.parse(session.last_response_at ?? session.created_at ?? '') || 0; -const byLastUsed = (sessions: readonly SessionRow[]) => - [...sessions].sort((a, b) => lastUsed(b) - lastUsed(a)); +const DAY = 24 * 60 * 60 * 1000; + +// Most recently used first, under Today, Yesterday, Previous 7 days and Older. +const sectionsOf = (sessions: readonly SessionRow[]): Section[] => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const startOfToday = today.getTime(); + const sections: Section[] = [ + { data: [], title: 'Today' }, + { data: [], title: 'Yesterday' }, + { data: [], title: 'Previous 7 days' }, + { data: [], title: 'Older' }, + ]; + [...sessions] + .sort((a, b) => lastUsed(b) - lastUsed(a)) + .forEach((session) => { + const at = lastUsed(session); + const index = + at >= startOfToday + ? 0 + : at >= startOfToday - DAY + ? 1 + : at >= startOfToday - 7 * DAY + ? 2 + : 3; + sections[index].data.push(session); + }); + return sections.filter((section) => section.data.length > 0); +}; + +const initialsOf = (name: string) => + name + .split(' ') + .map((part) => part[0]) + .join('') + .slice(0, 2) + .toUpperCase(); /** * The agent's conversations, most recently used first. Only those kept in writing: a session that @@ -34,6 +72,7 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { useAppContext(); const [sessions, setSessions] = useState([]); const isOpen = useDrawerStatus() === 'open'; + const sections = useMemo(() => sectionsOf(sessions), [sessions]); useEffect(() => { if (!isReady || !isOpen) return; @@ -41,7 +80,7 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { agentSessions() .query({ limit: 50 }) .then((page) => { - if (!cancelled) setSessions(byLastUsed(page.items.filter((item) => item.conversation_id))); + if (!cancelled) setSessions(page.items.filter((item) => item.conversation_id)); }) .catch(() => undefined); return () => { @@ -61,42 +100,119 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { navigation.closeDrawer(); }); - const renderItem = ({ item }: { item: SessionRow }) => ( - onSelect(item)} - style={({ pressed }) => [styles.row, { opacity: pressed ? 0.6 : 1 }]} - > - - {item.title || 'New conversation'} - - + const renderItem = ({ item }: { item: SessionRow }) => { + const isCurrent = item.id === sessionId; + return ( + onSelect(item)} + style={({ pressed }) => [ + styles.row, + isCurrent ? styles.rowCurrent : pressed ? styles.rowPressed : null, + ]} + > + + {item.title || 'New conversation'} + + + ); + }; + + const renderSectionHeader = ({ section }: { section: Section }) => ( + + {section.title} + ); return ( - - - Conversations - - + + + + Conversations + + [styles.newButton, pressed && styles.newButtonPressed]} + > + + New conversation - + + + + {initialsOf(AgentConfig.user.name)} + + + {AgentConfig.user.name} + + ); }; +const ACCENT = '#C8102E'; +const ACCENT_TINT = '#FDECEE'; +const BORDER = '#E6E8EC'; +const TEXT = '#0B1220'; +const TEXT_SECONDARY = '#5C6370'; + const styles = StyleSheet.create({ - container: { - borderBottomColor: 'grey', - borderBottomWidth: 1, + avatar: { + alignItems: 'center', + backgroundColor: ACCENT_TINT, + borderRadius: 18, + height: 36, + justifyContent: 'center', + width: 36, + }, + avatarText: { color: ACCENT, fontSize: 14, fontWeight: '700' }, + footer: { + alignItems: 'center', + borderTopColor: BORDER, + borderTopWidth: StyleSheet.hairlineWidth, flexDirection: 'row', - justifyContent: 'space-between', - marginHorizontal: 12, - paddingVertical: 16, + gap: 12, + paddingHorizontal: 20, + paddingVertical: 14, + }, + footerName: { color: TEXT, flex: 1, fontSize: 15, fontWeight: '600' }, + header: { gap: 16, paddingBottom: 8, paddingHorizontal: 20, paddingTop: 16 }, + list: { paddingBottom: 16, paddingHorizontal: 12 }, + newButton: { + alignItems: 'center', + alignSelf: 'stretch', + backgroundColor: ACCENT_TINT, + borderRadius: 14, + flexDirection: 'row', + gap: 8, + paddingHorizontal: 14, + paddingVertical: 12, + }, + newButtonPressed: { opacity: 0.7 }, + newButtonText: { color: ACCENT, fontSize: 15, fontWeight: '600' }, + row: { borderRadius: 12, paddingHorizontal: 12, paddingVertical: 11 }, + rowCurrent: { backgroundColor: ACCENT_TINT }, + rowPressed: { backgroundColor: '#F2F3F5' }, + rowText: { color: TEXT, fontSize: 15 }, + rowTextCurrent: { color: ACCENT, fontWeight: '600' }, + sectionTitle: { + color: TEXT_SECONDARY, + fontSize: 13, + fontWeight: '600', + paddingBottom: 6, + paddingHorizontal: 12, + paddingTop: 18, }, - current: { color: '#005FFF' }, - previewText: { fontSize: 15, fontWeight: 'bold' }, - row: { paddingHorizontal: 12, paddingVertical: 8 }, - title: { fontSize: 15, fontWeight: 'bold' }, + title: { color: TEXT, fontSize: 22, fontWeight: '700' }, wrapper: { backgroundColor: '#FFFFFF', flex: 1 }, });