fix(chat): snap to bottom after send to mask composer-shrink layout shift
This commit is contained in:
@@ -754,6 +754,24 @@ export function ConversationPage() {
|
|||||||
lastPendingCountRef.current = pending.length;
|
lastPendingCountRef.current = pending.length;
|
||||||
}, [pending.length]);
|
}, [pending.length]);
|
||||||
|
|
||||||
|
// Snap the viewport back to the bottom after a send. The composer
|
||||||
|
// shrinks (cleared text, dismissed reply preview, dropped attachment
|
||||||
|
// thumbs) which lets the Virtuoso area grow vertically — leaving the
|
||||||
|
// just-sent bubble visibly above the new bottom for a frame.
|
||||||
|
// `requestAnimationFrame` defers the scroll until React has committed
|
||||||
|
// the composer-height change, so Virtuoso's ResizeObserver has
|
||||||
|
// already seen the new viewport and `index: 'LAST', align: 'end'`
|
||||||
|
// targets the correct bottom edge.
|
||||||
|
const snapToBottom = useCallback(() => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index: 'LAST',
|
||||||
|
align: 'end',
|
||||||
|
behavior: 'auto',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function handleSend(e?: React.FormEvent) {
|
async function handleSend(e?: React.FormEvent) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if ((!text.trim() && attachments.length === 0) || sending) return;
|
if ((!text.trim() && attachments.length === 0) || sending) return;
|
||||||
@@ -773,6 +791,7 @@ export function ConversationPage() {
|
|||||||
setStickToBottom(true);
|
setStickToBottom(true);
|
||||||
notifyStopTyping();
|
notifyStopTyping();
|
||||||
if (id) clearDraft(id);
|
if (id) clearDraft(id);
|
||||||
|
snapToBottom();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const code = extractErrorCode(err);
|
const code = extractErrorCode(err);
|
||||||
setSendError(
|
setSendError(
|
||||||
@@ -798,13 +817,14 @@ export function ConversationPage() {
|
|||||||
setReplyTo(null);
|
setReplyTo(null);
|
||||||
setStickToBottom(true);
|
setStickToBottom(true);
|
||||||
notifyStopTyping();
|
notifyStopTyping();
|
||||||
|
snapToBottom();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setPollError(err instanceof Error ? err.message : 'Umfrage konnte nicht gesendet werden');
|
setPollError(err instanceof Error ? err.message : 'Umfrage konnte nicht gesendet werden');
|
||||||
} finally {
|
} finally {
|
||||||
setPollSending(false);
|
setPollSending(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[send, replyTo?.id, notifyStopTyping],
|
[send, replyTo?.id, notifyStopTyping, snapToBottom],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateWhiteboard = useCallback(async () => {
|
const handleCreateWhiteboard = useCallback(async () => {
|
||||||
@@ -817,12 +837,13 @@ export function ConversationPage() {
|
|||||||
setReplyTo(null);
|
setReplyTo(null);
|
||||||
setStickToBottom(true);
|
setStickToBottom(true);
|
||||||
setOpenWhiteboardId(board.id);
|
setOpenWhiteboardId(board.id);
|
||||||
|
snapToBottom();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setSendError(err instanceof Error ? err.message : 'Whiteboard konnte nicht angelegt werden');
|
setSendError(err instanceof Error ? err.message : 'Whiteboard konnte nicht angelegt werden');
|
||||||
} finally {
|
} finally {
|
||||||
setCreatingWhiteboard(false);
|
setCreatingWhiteboard(false);
|
||||||
}
|
}
|
||||||
}, [id, creatingWhiteboard, send, replyTo?.id]);
|
}, [id, creatingWhiteboard, send, replyTo?.id, snapToBottom]);
|
||||||
|
|
||||||
const handleStartWatchTogether = useCallback(async () => {
|
const handleStartWatchTogether = useCallback(async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -842,12 +863,13 @@ export function ConversationPage() {
|
|||||||
setWatchDialogOpen(false);
|
setWatchDialogOpen(false);
|
||||||
setWatchUrl('');
|
setWatchUrl('');
|
||||||
setOpenWatchSessionId(ws.id);
|
setOpenWatchSessionId(ws.id);
|
||||||
|
snapToBottom();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setWatchError(err instanceof Error ? err.message : 'Konnte Watch-Together nicht starten');
|
setWatchError(err instanceof Error ? err.message : 'Konnte Watch-Together nicht starten');
|
||||||
} finally {
|
} finally {
|
||||||
setWatchCreating(false);
|
setWatchCreating(false);
|
||||||
}
|
}
|
||||||
}, [id, watchUrl, send, replyTo?.id]);
|
}, [id, watchUrl, send, replyTo?.id, snapToBottom]);
|
||||||
|
|
||||||
const handleStartGame = useCallback(async (gameType: GameType) => {
|
const handleStartGame = useCallback(async (gameType: GameType) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -874,12 +896,13 @@ export function ConversationPage() {
|
|||||||
setStickToBottom(true);
|
setStickToBottom(true);
|
||||||
setGameDialogOpen(false);
|
setGameDialogOpen(false);
|
||||||
setOpenGameId(game.id);
|
setOpenGameId(game.id);
|
||||||
|
snapToBottom();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setGameError(err instanceof Error ? err.message : 'Konnte Spiel nicht starten');
|
setGameError(err instanceof Error ? err.message : 'Konnte Spiel nicht starten');
|
||||||
} finally {
|
} finally {
|
||||||
setGameCreating(false);
|
setGameCreating(false);
|
||||||
}
|
}
|
||||||
}, [id, conversation, myId, send, replyTo?.id]);
|
}, [id, conversation, myId, send, replyTo?.id, snapToBottom]);
|
||||||
|
|
||||||
async function ingestFiles(files: File[]) {
|
async function ingestFiles(files: File[]) {
|
||||||
const compressed = await compressImages(files);
|
const compressed = await compressImages(files);
|
||||||
|
|||||||
Reference in New Issue
Block a user