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

View File

@ -93,6 +93,7 @@ export default function DmScreen() {
setPartner(null);
partnerRef.current = null;
setReplyTo(null);
isInitialLoad.current = true;
}, [userId]);
// 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);
const isInitialLoad = useRef(true);
const scrollToBottom = useCallback((animated: boolean) => {
setTimeout(() => flatRef.current?.scrollToEnd({ animated }), 80);
}, []);
// Auto-Scroll bei neuen Messages
useEffect(() => {
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) {
if (sending) return;
@ -310,12 +319,12 @@ export default function DmScreen() {
onReply={startReply}
onLike={toggleLike}
onOpenImage={() => {}}
onImageLoad={index === messages.length - 1 ? () => scrollToBottom(false) : undefined}
/>
)}
keyExtractor={(m) => m.id}
contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => flatRef.current?.scrollToEnd({ animated: false })}
/>
)}
</View>

View File

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