| .. | .. |
|---|
| 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} |
|---|