Update splash screen config and add session unread indicator
High-level changes:
- Configure expo-splash-screen plugin with unified icon asset
- Add visual unread indicator for sessions with new activity
- Improve session rename UX by auto-scrolling to edited item
app.json:
- Replace splash-icon.png reference with icon.png for
consistency
- Add expo-splash-screen plugin configuration with 200px
image width and dark background color
components/SessionDrawer.tsx:
- Add `isUnread` prop to `SessionRow` to show server-pushed
unread state
- Display blue dot next to session name when `isUnread` is
true but `unreadCount` is 0 (indicates new activity not
yet reflected in count)
- Add `listRef` to DraggableFlatList for programmatic
scrolling
- Auto-scroll to session being renamed after keyboard
appears (400ms delay) so input remains visible
- Update `renderItem` dependencies to include
`unreadSessions` and `orderedSessions`
components/SessionPicker.tsx:
- Add `sessionScrollRef` to ScrollView for programmatic
scrolling
- Auto-scroll to session being renamed after keyboard
appears to keep rename input visible during editing
These changes improve session management UX by providing
better visual feedback for unread sessions and ensuring
rename inputs remain visible when the keyboard appears.
| .. | .. |
|---|
| 9 | 9 | "newArchEnabled": true, |
|---|
| 10 | 10 | "scheme": "pailot", |
|---|
| 11 | 11 | "splash": { |
|---|
| 12 | | - "image": "./assets/splash-icon.png", |
|---|
| 12 | + "image": "./assets/icon.png", |
|---|
| 13 | 13 | "resizeMode": "contain", |
|---|
| 14 | 14 | "backgroundColor": "#0A0A0F" |
|---|
| 15 | 15 | }, |
|---|
| .. | .. |
|---|
| 45 | 45 | "bundler": "metro" |
|---|
| 46 | 46 | }, |
|---|
| 47 | 47 | "plugins": [ |
|---|
| 48 | + ["expo-splash-screen", { |
|---|
| 49 | + "image": "./assets/icon.png", |
|---|
| 50 | + "backgroundColor": "#0A0A0F", |
|---|
| 51 | + "imageWidth": 200 |
|---|
| 52 | + }], |
|---|
| 48 | 53 | "expo-router", |
|---|
| 49 | 54 | [ |
|---|
| 50 | 55 | "expo-audio", |
|---|
| .. | .. |
|---|
| 44 | 44 | function SessionRow({ |
|---|
| 45 | 45 | session, |
|---|
| 46 | 46 | unreadCount, |
|---|
| 47 | + isUnread, |
|---|
| 47 | 48 | onSwitch, |
|---|
| 48 | 49 | onLongPress, |
|---|
| 49 | 50 | onDelete, |
|---|
| .. | .. |
|---|
| 53 | 54 | }: { |
|---|
| 54 | 55 | session: WsSession; |
|---|
| 55 | 56 | unreadCount: number; |
|---|
| 57 | + isUnread: boolean; |
|---|
| 56 | 58 | onSwitch: () => void; |
|---|
| 57 | 59 | onLongPress: () => void; |
|---|
| 58 | 60 | onDelete: () => void; |
|---|
| .. | .. |
|---|
| 152 | 154 | |
|---|
| 153 | 155 | {/* Name + subtitle — middle */} |
|---|
| 154 | 156 | <View style={{ flex: 1, marginLeft: 14 }}> |
|---|
| 155 | | - <Text |
|---|
| 156 | | - style={{ |
|---|
| 157 | | - color: session.isActive ? colors.accent : colors.text, |
|---|
| 158 | | - fontSize: 17, |
|---|
| 159 | | - fontWeight: session.isActive ? "700" : "600", |
|---|
| 160 | | - }} |
|---|
| 161 | | - numberOfLines={1} |
|---|
| 162 | | - > |
|---|
| 163 | | - {session.name} |
|---|
| 164 | | - </Text> |
|---|
| 157 | + <View style={{ flexDirection: "row", alignItems: "center" }}> |
|---|
| 158 | + <Text |
|---|
| 159 | + style={{ |
|---|
| 160 | + color: session.isActive ? colors.accent : colors.text, |
|---|
| 161 | + fontSize: 17, |
|---|
| 162 | + fontWeight: session.isActive ? "700" : "600", |
|---|
| 163 | + flexShrink: 1, |
|---|
| 164 | + }} |
|---|
| 165 | + numberOfLines={1} |
|---|
| 166 | + > |
|---|
| 167 | + {session.name} |
|---|
| 168 | + </Text> |
|---|
| 169 | + {/* Server-pushed unread dot — shown when the server signals new activity */} |
|---|
| 170 | + {isUnread && unreadCount === 0 && ( |
|---|
| 171 | + <View |
|---|
| 172 | + style={{ |
|---|
| 173 | + width: 8, |
|---|
| 174 | + height: 8, |
|---|
| 175 | + borderRadius: 4, |
|---|
| 176 | + backgroundColor: colors.accent, |
|---|
| 177 | + marginLeft: 6, |
|---|
| 178 | + flexShrink: 0, |
|---|
| 179 | + }} |
|---|
| 180 | + /> |
|---|
| 181 | + )} |
|---|
| 182 | + </View> |
|---|
| 165 | 183 | <Text |
|---|
| 166 | 184 | style={{ |
|---|
| 167 | 185 | color: colors.textMuted, |
|---|
| .. | .. |
|---|
| 266 | 284 | fetchProjects, |
|---|
| 267 | 285 | projects, |
|---|
| 268 | 286 | unreadCounts, |
|---|
| 287 | + unreadSessions, |
|---|
| 269 | 288 | } = useChat(); |
|---|
| 270 | 289 | const { colors } = useTheme(); |
|---|
| 271 | 290 | const [editingId, setEditingId] = useState<string | null>(null); |
|---|
| .. | .. |
|---|
| 276 | 295 | const [rendered, setRendered] = useState(false); |
|---|
| 277 | 296 | const [keyboardHeight, setKeyboardHeight] = useState(0); |
|---|
| 278 | 297 | const pickerScrollRef = useRef<ScrollView>(null); |
|---|
| 298 | + const listRef = useRef<DraggableFlatList<WsSession>>(null); |
|---|
| 279 | 299 | |
|---|
| 280 | 300 | useEffect(() => { |
|---|
| 281 | 301 | const showSub = Keyboard.addListener("keyboardWillShow", (e) => { |
|---|
| .. | .. |
|---|
| 361 | 381 | const handleStartRename = useCallback((session: WsSession) => { |
|---|
| 362 | 382 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 363 | 383 | setEditingId(session.id); |
|---|
| 364 | | - }, []); |
|---|
| 384 | + // Scroll to the item after keyboard appears so it's visible |
|---|
| 385 | + const idx = orderedSessions.findIndex(s => s.id === session.id); |
|---|
| 386 | + if (idx >= 0 && listRef.current) { |
|---|
| 387 | + setTimeout(() => { |
|---|
| 388 | + try { |
|---|
| 389 | + (listRef.current as any)?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.3 }); |
|---|
| 390 | + } catch { /* ignore if index out of range */ } |
|---|
| 391 | + }, 400); |
|---|
| 392 | + } |
|---|
| 393 | + }, [orderedSessions]); |
|---|
| 365 | 394 | |
|---|
| 366 | 395 | const handleConfirmRename = useCallback( |
|---|
| 367 | 396 | (sessionId: string, newName: string) => { |
|---|
| .. | .. |
|---|
| 414 | 443 | <SessionRow |
|---|
| 415 | 444 | session={item} |
|---|
| 416 | 445 | unreadCount={unreadCounts[item.id] ?? 0} |
|---|
| 446 | + isUnread={unreadSessions.has(item.id)} |
|---|
| 417 | 447 | onSwitch={() => handleSwitch(item)} |
|---|
| 418 | 448 | onLongPress={() => handleStartRename(item)} |
|---|
| 419 | 449 | onDelete={() => handleRemove(item)} |
|---|
| .. | .. |
|---|
| 424 | 454 | </ScaleDecorator> |
|---|
| 425 | 455 | ); |
|---|
| 426 | 456 | }, |
|---|
| 427 | | - [editingId, unreadCounts, colors, handleSwitch, handleStartRename, handleRemove, handleConfirmRename], |
|---|
| 457 | + [editingId, unreadCounts, unreadSessions, colors, handleSwitch, handleStartRename, handleRemove, handleConfirmRename], |
|---|
| 428 | 458 | ); |
|---|
| 429 | 459 | |
|---|
| 430 | 460 | const keyExtractor = useCallback((item: WsSession) => item.id, []); |
|---|
| .. | .. |
|---|
| 537 | 567 | </View> |
|---|
| 538 | 568 | ) : ( |
|---|
| 539 | 569 | <DraggableFlatList |
|---|
| 570 | + ref={listRef} |
|---|
| 540 | 571 | data={orderedSessions} |
|---|
| 541 | 572 | keyExtractor={keyExtractor} |
|---|
| 542 | 573 | renderItem={renderItem} |
|---|
| .. | .. |
|---|
| 265 | 265 | } = useChat(); |
|---|
| 266 | 266 | const [editingId, setEditingId] = useState<string | null>(null); |
|---|
| 267 | 267 | const [keyboardHeight, setKeyboardHeight] = useState(0); |
|---|
| 268 | + const sessionScrollRef = useRef<ScrollView>(null); |
|---|
| 268 | 269 | |
|---|
| 269 | 270 | // Sort: active first, then by index |
|---|
| 270 | 271 | const sortedSessions = [...sessions].sort((a, b) => { |
|---|
| .. | .. |
|---|
| 312 | 313 | const handleStartRename = useCallback((session: WsSession) => { |
|---|
| 313 | 314 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 314 | 315 | setEditingId(session.id); |
|---|
| 315 | | - }, []); |
|---|
| 316 | + // Scroll down after keyboard appears so the rename field is visible |
|---|
| 317 | + const idx = sortedSessions.findIndex(s => s.id === session.id); |
|---|
| 318 | + if (idx >= 0 && sessionScrollRef.current) { |
|---|
| 319 | + setTimeout(() => { |
|---|
| 320 | + sessionScrollRef.current?.scrollTo({ y: idx * 60, animated: true }); |
|---|
| 321 | + }, 400); |
|---|
| 322 | + } |
|---|
| 323 | + }, [sortedSessions]); |
|---|
| 316 | 324 | |
|---|
| 317 | 325 | const handleConfirmRename = useCallback( |
|---|
| 318 | 326 | (sessionId: string, newName: string) => { |
|---|
| .. | .. |
|---|
| 407 | 415 | |
|---|
| 408 | 416 | {/* Session list */} |
|---|
| 409 | 417 | <ScrollView |
|---|
| 418 | + ref={sessionScrollRef} |
|---|
| 410 | 419 | style={{ paddingHorizontal: 16 }} |
|---|
| 411 | 420 | showsVerticalScrollIndicator={false} |
|---|
| 412 | 421 | keyboardShouldPersistTaps="handled" |
|---|