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;
|
||||
}, [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) {
|
||||
e?.preventDefault();
|
||||
if ((!text.trim() && attachments.length === 0) || sending) return;
|
||||
@@ -773,6 +791,7 @@ export function ConversationPage() {
|
||||
setStickToBottom(true);
|
||||
notifyStopTyping();
|
||||
if (id) clearDraft(id);
|
||||
snapToBottom();
|
||||
} catch (err: unknown) {
|
||||
const code = extractErrorCode(err);
|
||||
setSendError(
|
||||
@@ -798,13 +817,14 @@ export function ConversationPage() {
|
||||
setReplyTo(null);
|
||||
setStickToBottom(true);
|
||||
notifyStopTyping();
|
||||
snapToBottom();
|
||||
} catch (err: unknown) {
|
||||
setPollError(err instanceof Error ? err.message : 'Umfrage konnte nicht gesendet werden');
|
||||
} finally {
|
||||
setPollSending(false);
|
||||
}
|
||||
},
|
||||
[send, replyTo?.id, notifyStopTyping],
|
||||
[send, replyTo?.id, notifyStopTyping, snapToBottom],
|
||||
);
|
||||
|
||||
const handleCreateWhiteboard = useCallback(async () => {
|
||||
@@ -817,12 +837,13 @@ export function ConversationPage() {
|
||||
setReplyTo(null);
|
||||
setStickToBottom(true);
|
||||
setOpenWhiteboardId(board.id);
|
||||
snapToBottom();
|
||||
} catch (err: unknown) {
|
||||
setSendError(err instanceof Error ? err.message : 'Whiteboard konnte nicht angelegt werden');
|
||||
} finally {
|
||||
setCreatingWhiteboard(false);
|
||||
}
|
||||
}, [id, creatingWhiteboard, send, replyTo?.id]);
|
||||
}, [id, creatingWhiteboard, send, replyTo?.id, snapToBottom]);
|
||||
|
||||
const handleStartWatchTogether = useCallback(async () => {
|
||||
if (!id) return;
|
||||
@@ -842,12 +863,13 @@ export function ConversationPage() {
|
||||
setWatchDialogOpen(false);
|
||||
setWatchUrl('');
|
||||
setOpenWatchSessionId(ws.id);
|
||||
snapToBottom();
|
||||
} catch (err: unknown) {
|
||||
setWatchError(err instanceof Error ? err.message : 'Konnte Watch-Together nicht starten');
|
||||
} finally {
|
||||
setWatchCreating(false);
|
||||
}
|
||||
}, [id, watchUrl, send, replyTo?.id]);
|
||||
}, [id, watchUrl, send, replyTo?.id, snapToBottom]);
|
||||
|
||||
const handleStartGame = useCallback(async (gameType: GameType) => {
|
||||
if (!id) return;
|
||||
@@ -874,12 +896,13 @@ export function ConversationPage() {
|
||||
setStickToBottom(true);
|
||||
setGameDialogOpen(false);
|
||||
setOpenGameId(game.id);
|
||||
snapToBottom();
|
||||
} catch (err: unknown) {
|
||||
setGameError(err instanceof Error ? err.message : 'Konnte Spiel nicht starten');
|
||||
} finally {
|
||||
setGameCreating(false);
|
||||
}
|
||||
}, [id, conversation, myId, send, replyTo?.id]);
|
||||
}, [id, conversation, myId, send, replyTo?.id, snapToBottom]);
|
||||
|
||||
async function ingestFiles(files: File[]) {
|
||||
const compressed = await compressImages(files);
|
||||
|
||||
Reference in New Issue
Block a user