web: fix mobile nav stuck-open on rejoin + paste-chip code fence
useViewport re-syncs the snapshot on pageshow/visibilitychange/resize/orientationchange — iOS reported a stale width on backgrounded-tab restore, leaving isMobile=false so the sidebar rendered as a permanent column with no close affordance. flattenToMessage now inserts pasted-text chips verbatim instead of wrapping them in a triple-backtick fence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,13 +31,48 @@ export function useViewport(): ViewportSnapshot {
|
||||
if (typeof window === 'undefined') return;
|
||||
const mobileMq = window.matchMedia(`(max-width: ${MOBILE_MAX}px)`);
|
||||
const tabletMq = window.matchMedia(`(min-width: ${MOBILE_MAX + 1}px) and (max-width: ${TABLET_MAX}px)`);
|
||||
const update = () => setState(snapshot());
|
||||
const update = () =>
|
||||
setState((prev) => {
|
||||
const next = snapshot();
|
||||
// Bail if nothing changed — visualViewport 'resize' fires on every
|
||||
// URL-bar show/hide and scroll, and a fresh object would re-render
|
||||
// every consumer needlessly.
|
||||
if (
|
||||
prev.isMobile === next.isMobile &&
|
||||
prev.isTablet === next.isTablet &&
|
||||
prev.width === next.width
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
// matchMedia 'change' alone is not enough on iOS Safari/Vivaldi: when a
|
||||
// backgrounded tab is restored (bfcache) or refocused, no 'change' fires,
|
||||
// and the width captured at first paint can be a stale/oversized value
|
||||
// (iOS reports the wrong innerWidth for a beat before layout settles). That
|
||||
// leaves isMobile=false on a phone, so the sidebar renders as a permanent
|
||||
// desktop column with no way to close it. Re-snapshot on every signal that
|
||||
// accompanies a rejoin/viewport correction, not just breakpoint crossings.
|
||||
const onVisibility = () => {
|
||||
if (document.visibilityState === 'visible') update();
|
||||
};
|
||||
mobileMq.addEventListener('change', update);
|
||||
tabletMq.addEventListener('change', update);
|
||||
window.addEventListener('resize', update);
|
||||
window.addEventListener('orientationchange', update);
|
||||
window.addEventListener('pageshow', update);
|
||||
document.addEventListener('visibilitychange', onVisibility);
|
||||
window.visualViewport?.addEventListener('resize', update);
|
||||
update();
|
||||
return () => {
|
||||
mobileMq.removeEventListener('change', update);
|
||||
tabletMq.removeEventListener('change', update);
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('orientationchange', update);
|
||||
window.removeEventListener('pageshow', update);
|
||||
document.removeEventListener('visibilitychange', onVisibility);
|
||||
window.visualViewport?.removeEventListener('resize', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -57,15 +57,17 @@ export function inferLanguage(filename: string): string | null {
|
||||
export function flattenToMessage(attachments: Attachment[], text: string): string {
|
||||
if (attachments.length === 0) return text;
|
||||
const blocks = attachments.map(a => {
|
||||
const fence = '```' + (a.language ?? '');
|
||||
let header: string;
|
||||
if (a.kind === 'lines') {
|
||||
header = `// from: ${a.filename}:${a.range?.[0] ?? '?'}-${a.range?.[1] ?? '?'}`;
|
||||
} else if (a.kind === 'paste') {
|
||||
header = `// from: pasted text (${a.content.split('\n').length} lines)`;
|
||||
} else {
|
||||
header = `// from: ${a.filename}`;
|
||||
// Pasted text is raw context, not code from a file — insert it verbatim with
|
||||
// no ``` fence or provenance header. The chip only exists to keep the textarea
|
||||
// tidy while composing; on send it should be exactly what the user pasted.
|
||||
if (a.kind === 'paste') {
|
||||
return a.content;
|
||||
}
|
||||
const fence = '```' + (a.language ?? '');
|
||||
const header =
|
||||
a.kind === 'lines'
|
||||
? `// from: ${a.filename}:${a.range?.[0] ?? '?'}-${a.range?.[1] ?? '?'}`
|
||||
: `// from: ${a.filename}`;
|
||||
return `${fence}\n${header}\n${a.content}\n\`\`\``;
|
||||
});
|
||||
return [...blocks, text].filter(Boolean).join('\n\n');
|
||||
|
||||
Reference in New Issue
Block a user