fix(chat): Listen-Spinner-Hänger + Auto-Scroll bei Bild-Nachrichten

Bug 1 — Chat-Liste: RefreshControl nutzte React-Querys `isRefetching`,
das bei JEDEM Background-Refetch (focus-/stale-getriggert) true wird →
nach Zurück-Navigation hing der Pull-to-Refresh-Spinner endlos. Fix:
eigener `userRefreshing`-State, nur bei explizitem Pull-to-Refresh true,
im finally zurückgesetzt.

Bug 2 — Conversation scrollte nicht bis zur letzten Nachricht, wenn die
ein Bild war: onContentSizeChange-scrollToEnd feuerte vor dem Bild-Load.
Fix: ChatBubble bekommt onImageLoad-Callback, die letzte Bild-Nachricht
triggert nach dem Laden erneut scrollToBottom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-22 19:53:31 +02:00
parent bf28d81d13
commit 708eac51c0
3 changed files with 27 additions and 6 deletions

View File

@ -94,11 +94,11 @@ export default function ChatScreen() {
const colors = useColors(); const colors = useColors();
const styles = makeStyles(colors); const styles = makeStyles(colors);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [userRefreshing, setUserRefreshing] = useState(false);
const { const {
data: convs = [], data: convs = [],
isLoading: loadingDms, isLoading: loadingDms,
isRefetching: refetchingDms,
refetch: refetchDms, refetch: refetchDms,
} = useQuery<DmConversation[]>({ } = useQuery<DmConversation[]>({
queryKey: ['dm-conversations'], queryKey: ['dm-conversations'],
@ -106,6 +106,15 @@ export default function ChatScreen() {
staleTime: 30_000, staleTime: 30_000,
}); });
const handleRefresh = useCallback(async () => {
setUserRefreshing(true);
try {
await refetchDms();
} finally {
setUserRefreshing(false);
}
}, [refetchDms]);
const filtered = search.trim() const filtered = search.trim()
? convs.filter((c) => ? convs.filter((c) =>
c.partnerName.toLowerCase().includes(search.toLowerCase()) || c.partnerName.toLowerCase().includes(search.toLowerCase()) ||
@ -152,8 +161,8 @@ export default function ChatScreen() {
keyExtractor={(item) => item.partnerId} keyExtractor={(item) => item.partnerId}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refetchingDms} refreshing={userRefreshing}
onRefresh={refetchDms} onRefresh={handleRefresh}
tintColor={colors.brandOrange} tintColor={colors.brandOrange}
/> />
} }

View File

@ -93,6 +93,7 @@ export default function DmScreen() {
setPartner(null); setPartner(null);
partnerRef.current = null; partnerRef.current = null;
setReplyTo(null); setReplyTo(null);
isInitialLoad.current = true;
}, [userId]); }, [userId]);
// Lade DM-History — staleTime:0 erzwingt immer frischen Fetch (kein Cache-Hit-Bug) // Lade DM-History — staleTime:0 erzwingt immer frischen Fetch (kein Cache-Hit-Bug)
@ -174,12 +175,20 @@ export default function DmScreen() {
); );
useDmRealtime(userId, onDmInsert, !!myUserId); useDmRealtime(userId, onDmInsert, !!myUserId);
const isInitialLoad = useRef(true);
const scrollToBottom = useCallback((animated: boolean) => {
setTimeout(() => flatRef.current?.scrollToEnd({ animated }), 80);
}, []);
// Auto-Scroll bei neuen Messages // Auto-Scroll bei neuen Messages
useEffect(() => { useEffect(() => {
if (messages.length > 0) { if (messages.length > 0) {
requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); const animated = !isInitialLoad.current;
isInitialLoad.current = false;
scrollToBottom(animated);
} }
}, [messages.length]); }, [messages.length, scrollToBottom]);
async function handleSend(payload: SendPayload) { async function handleSend(payload: SendPayload) {
if (sending) return; if (sending) return;
@ -310,12 +319,12 @@ export default function DmScreen() {
onReply={startReply} onReply={startReply}
onLike={toggleLike} onLike={toggleLike}
onOpenImage={() => {}} onOpenImage={() => {}}
onImageLoad={index === messages.length - 1 ? () => scrollToBottom(false) : undefined}
/> />
)} )}
keyExtractor={(m) => m.id} keyExtractor={(m) => m.id}
contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }} contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
onContentSizeChange={() => flatRef.current?.scrollToEnd({ animated: false })}
/> />
)} )}
</View> </View>

View File

@ -47,6 +47,7 @@ type Props = {
onReply: (msg: ChatMsg) => void; onReply: (msg: ChatMsg) => void;
onLike: (msg: ChatMsg) => void; onLike: (msg: ChatMsg) => void;
onOpenImage: (url: string) => void; onOpenImage: (url: string) => void;
onImageLoad?: () => void;
}; };
function formatTime(ts: string) { function formatTime(ts: string) {
@ -75,6 +76,7 @@ export function ChatBubble({
onReply, onReply,
onLike, onLike,
onOpenImage, onOpenImage,
onImageLoad,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const colors = useColors(); const colors = useColors();
@ -201,6 +203,7 @@ export function ChatBubble({
contentFit="cover" contentFit="cover"
cachePolicy="memory-disk" cachePolicy="memory-disk"
transition={200} transition={200}
onLoad={onImageLoad ? () => onImageLoad() : undefined}
/> />
{isImageOnly && ( {isImageOnly && (
<View style={styles.imageTimeOverlay}> <View style={styles.imageTimeOverlay}>