Skip to content

Commit 1ea075b

Browse files
backnotpropclaude
andauthored
fix: show error dialog on shared URL failure and fix import annotation merge (#205)
* chore: open v0.10.0 pre-release fix branch Tracking two bugs found during pre-release testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: show error dialog on shared URL failure and fix import annotation merge Bug 1: When a shared URL fails to load (truncated hash, expired paste, missing encryption key), the portal now shows a warning dialog explaining the failure and an amber "Demo" badge so users know they're seeing sample content — not their data. Bug 2: Importing a teammate's share URL no longer replaces existing annotations. Uses React functional state updaters to avoid stale closures and passes the full merged annotation list to pendingSharedAnnotations so DOM highlights are re-applied for all annotations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce8946b commit 1ea075b

3 files changed

Lines changed: 70 additions & 19 deletions

File tree

packages/editor/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ const App: React.FC = () => {
453453
clearPendingSharedAnnotations,
454454
generateShortUrl,
455455
importFromShareUrl,
456+
shareLoadError,
457+
clearShareLoadError,
456458
} = useSharing(
457459
markdown,
458460
annotations,
@@ -1277,6 +1279,7 @@ const App: React.FC = () => {
12771279
isPlanDiffActive={isPlanDiffActive}
12781280
onPlanDiffToggle={() => setIsPlanDiffActive(!isPlanDiffActive)}
12791281
hasPreviousVersion={!linkedDocHook.isActive && planDiff.hasPreviousVersion}
1282+
showDemoBadge={!isApiMode && !isLoadingShared && !isSharedSession}
12801283
onOpenLinkedDoc={linkedDocHook.open}
12811284
linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: linkedDocHook.back } : null}
12821285
/>
@@ -1386,6 +1389,16 @@ const App: React.FC = () => {
13861389
showCancel
13871390
/>
13881391

1392+
{/* Shared URL load failure warning */}
1393+
<ConfirmDialog
1394+
isOpen={!!shareLoadError && !isApiMode}
1395+
onClose={clearShareLoadError}
1396+
title="Shared Plan Could Not Be Loaded"
1397+
message={shareLoadError}
1398+
subMessage="You are viewing a demo plan. This is sample content — it is not your data or anyone else's."
1399+
variant="warning"
1400+
/>
1401+
13891402
{/* Save-to-notes toast */}
13901403
{noteSaveToast && (
13911404
<div className={`fixed top-16 right-4 z-50 px-3 py-2 rounded-lg text-xs font-medium shadow-lg transition-opacity ${

packages/ui/components/Viewer.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ interface ViewerProps {
3333
isPlanDiffActive?: boolean;
3434
onPlanDiffToggle?: () => void;
3535
hasPreviousVersion?: boolean;
36+
/** Show amber "Demo" badge (portal mode, no shared content loaded) */
37+
showDemoBadge?: boolean;
3638
}
3739

3840
export interface ViewerHandle {
@@ -93,6 +95,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
9395
isPlanDiffActive,
9496
onPlanDiffToggle,
9597
hasPreviousVersion,
98+
showDemoBadge,
9699
onOpenLinkedDoc,
97100
linkedDocInfo,
98101
}, ref) => {
@@ -648,8 +651,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
648651
linkedDocInfo ? 'border-2 border-primary' : 'border border-border/50'
649652
}`}
650653
>
651-
{/* Repo info + plan diff badge + linked doc badge - top left */}
652-
{(repoInfo || hasPreviousVersion || linkedDocInfo) && (
654+
{/* Repo info + plan diff badge + demo badge + linked doc badge - top left */}
655+
{(repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo) && (
653656
<div className="absolute top-3 left-3 md:top-4 md:left-5 flex flex-col items-start gap-1 text-[9px] text-muted-foreground/50 font-mono">
654657
{repoInfo && !linkedDocInfo && (
655658
<div className="flex items-center gap-1.5">
@@ -674,6 +677,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
674677
hasPreviousVersion={hasPreviousVersion ?? false}
675678
/>
676679
)}
680+
{showDemoBadge && !linkedDocInfo && (
681+
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-amber-500/15 text-amber-600 dark:text-amber-400">
682+
Demo
683+
</span>
684+
)}
677685
{linkedDocInfo && (
678686
<div className="flex items-center gap-1.5">
679687
<button

packages/ui/hooks/useSharing.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* - Tracking whether current session is from a shared link
99
*/
1010

11-
import { useState, useEffect, useCallback } from 'react';
11+
import React, { useState, useEffect, useCallback } from 'react';
1212
import { Annotation, type ImageAttachment } from '../types';
1313
import {
1414
type SharePayload,
@@ -68,6 +68,12 @@ interface UseSharingResult {
6868

6969
/** Import annotations from a teammate's share URL */
7070
importFromShareUrl: (url: string) => Promise<ImportResult>;
71+
72+
/** Error message when a shared URL failed to load on mount */
73+
shareLoadError: string;
74+
75+
/** Clear the share load error */
76+
clearShareLoadError: () => void;
7177
}
7278

7379

@@ -76,8 +82,8 @@ export function useSharing(
7682
annotations: Annotation[],
7783
globalAttachments: ImageAttachment[],
7884
setMarkdown: (m: string) => void,
79-
setAnnotations: (a: Annotation[]) => void,
80-
setGlobalAttachments: (g: ImageAttachment[]) => void,
85+
setAnnotations: React.Dispatch<React.SetStateAction<Annotation[]>>,
86+
setGlobalAttachments: React.Dispatch<React.SetStateAction<ImageAttachment[]>>,
8187
onSharedLoad?: () => void,
8288
shareBaseUrl?: string,
8389
pasteApiUrl?: string
@@ -91,12 +97,15 @@ export function useSharing(
9197
const [shortUrlError, setShortUrlError] = useState('');
9298
const [pendingSharedAnnotations, setPendingSharedAnnotations] = useState<Annotation[] | null>(null);
9399
const [sharedGlobalAttachments, setSharedGlobalAttachments] = useState<ImageAttachment[] | null>(null);
100+
const [shareLoadError, setShareLoadError] = useState('');
94101

95102
const clearPendingSharedAnnotations = useCallback(() => {
96103
setPendingSharedAnnotations(null);
97104
setSharedGlobalAttachments(null);
98105
}, []);
99106

107+
const clearShareLoadError = useCallback(() => setShareLoadError(''), []);
108+
100109
// Load shared state from URL hash (or paste-service short URL)
101110
const loadFromHash = useCallback(async () => {
102111
try {
@@ -135,9 +144,11 @@ export function useSharing(
135144
}
136145
// Paste fetch failed — short URL path can't fall back to hash parsing
137146
// (the hash contains #key=, not plan data).
147+
setShareLoadError('Failed to load shared plan — the link may be expired or incomplete.');
138148
return false;
139149
}
140150

151+
const hash = window.location.hash.slice(1);
141152
const payload = await parseShareHash();
142153

143154
if (payload) {
@@ -173,9 +184,15 @@ export function useSharing(
173184

174185
return true;
175186
}
187+
188+
// Hash was present but failed to decompress (likely truncated by browser)
189+
if (hash) {
190+
setShareLoadError('Failed to load shared plan — the URL may have been truncated by your browser.');
191+
}
176192
return false;
177193
} catch (e) {
178194
console.error('Failed to load from share hash:', e);
195+
setShareLoadError('Failed to load shared plan — an unexpected error occurred.');
179196
return false;
180197
}
181198
}, [setMarkdown, setAnnotations, setGlobalAttachments, onSharedLoad, pasteApiUrl]);
@@ -297,35 +314,46 @@ export function useSharing(
297314
return { success: true, count: 0, planTitle, error: 'No annotations found in share link' };
298315
}
299316

300-
// Deduplicate: skip annotations that already exist (by originalText + type + text)
301-
const newAnnotations = importedAnnotations.filter(imp =>
317+
// Estimate count from current closure (may be slightly stale, but
318+
// the actual merge below uses the latest state via functional updater)
319+
const estimatedNew = importedAnnotations.filter(imp =>
302320
!annotations.some(existing =>
303321
existing.originalText === imp.originalText &&
304322
existing.type === imp.type &&
305323
existing.text === imp.text
306324
)
307325
);
308326

309-
if (newAnnotations.length > 0) {
310-
// Merge: append new annotations to existing ones
311-
setAnnotations([...annotations, ...newAnnotations]);
312-
313-
// Set as pending so they get applied to DOM highlights
314-
setPendingSharedAnnotations(newAnnotations);
327+
if (estimatedNew.length > 0) {
328+
// Merge using functional updater to avoid stale closure
329+
setAnnotations(prev => {
330+
const newAnnotations = importedAnnotations.filter(imp =>
331+
!prev.some(existing =>
332+
existing.originalText === imp.originalText &&
333+
existing.type === imp.type &&
334+
existing.text === imp.text
335+
)
336+
);
337+
if (newAnnotations.length === 0) return prev;
338+
const merged = [...prev, ...newAnnotations];
339+
// Set ALL annotations as pending so DOM highlights include originals
340+
setPendingSharedAnnotations(merged);
341+
return merged;
342+
});
315343

316344
// Handle global attachments (deduplicate by path)
317345
if (payload.g?.length) {
318346
const parsed = parseShareableImages(payload.g) ?? [];
319-
const existingPaths = new Set(globalAttachments.map(g => g.path));
320-
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
321-
if (newAttachments.length > 0) {
322-
setGlobalAttachments([...globalAttachments, ...newAttachments]);
323-
}
347+
setGlobalAttachments(prev => {
348+
const existingPaths = new Set(prev.map(g => g.path));
349+
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
350+
return newAttachments.length > 0 ? [...prev, ...newAttachments] : prev;
351+
});
324352
setSharedGlobalAttachments(parsed);
325353
}
326354
}
327355

328-
return { success: true, count: newAnnotations.length, planTitle };
356+
return { success: true, count: estimatedNew.length, planTitle };
329357
} catch (e) {
330358
const errorMessage = e instanceof Error ? e.message : 'Failed to decompress share URL';
331359
return { success: false, count: 0, planTitle: '', error: errorMessage };
@@ -346,5 +374,7 @@ export function useSharing(
346374
refreshShareUrl,
347375
generateShortUrl,
348376
importFromShareUrl,
377+
shareLoadError,
378+
clearShareLoadError,
349379
};
350380
}

0 commit comments

Comments
 (0)