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:
parent
bf28d81d13
commit
708eac51c0
@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user