As a user I want to open a document directly at a specific comment so I can jump into a discussion from an email or notification #73
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User Journey
A user receives an email notification — either because they were @mentioned in a comment or because someone replied in a thread they're part of. The email contains a direct link. They click it, the document page opens, the comment panel (side drawer or bottom drawer, depending on viewport) slides open automatically, and the referenced comment is scrolled into view with a brief visual highlight so it's immediately obvious which comment prompted the notification. The same behavior applies when they click a notification entry in the in-app bell dropdown.
E2E Scenarios
Implementation Notes
URL scheme
/documents/{documentId}?commentId={commentId}No backend change needed —
commentIdis a frontend-only query param read on+page.svelte.Frontend logic (
documents/[id]/+page.svelte)$page.url.searchParams.get('commentId').activeAnnotationId = commentId(existing binding) OR set a newopenToCommentIdprop onCommentThread.showComments = trueor equivalent state).scrollIntoView({ behavior: 'smooth', block: 'center' })on the matching comment element.comment-highlightfor ~2 s (remove viasetTimeoutor a Svelte transition).Email links (#71)
NotificationServicealready builds{baseUrl}/documents/{documentId}links. Extend to append?commentId={referenceId}for both REPLY and MENTION notifications.In-app bell (#71)
NotificationBellalready navigates togoto('/documents/{documentId}')on item click. Extend togoto('/documents/{documentId}?commentId={referenceId}').Highlight style
Brief ring or left-border flash using a Tailwind animation (e.g.
animate-pulsefor one cycle, or a custom@keyframesflash). Should respect the active color theme.Dependencies
Both #71 and #72 should be implemented before or alongside this issue, since they generate the links that need to deep-link here.
Decision: smart routing via
annotationIdquery paramDocumentCommentalready has a nullableannotationIdfield — this is the discriminator:annotationIdpresent → annotation comment → side drawer (PDF annotation panel)annotationIdabsent → thread comment → bottom drawer (general comment thread)Updated URL scheme
/documents/{id}?commentId={commentId}&annotationId={annotationId}/documents/{id}?commentId={commentId}NotificationServicechangeWhen building the link for REPLY and MENTION notifications, load the comment and check
comment.getAnnotationId():Frontend routing logic (
documents/[id]/+page.svelte)Pass
openToCommentId={commentId}into whichever panel component is opened so it can scroll + highlight the specific comment once rendered.UX Review — @leonievoss
The URL scheme is clean, the
annotationIddiscriminator from the architect's comment is the right call. Two UX gaps to address.🔴 High — 2-second highlight timeout fails for seniors
A 2-second flash is sufficient for a millennial scanning quickly. A senior reading slowly will miss it entirely — they'll see the comment panel open but have no idea which comment triggered the notification.
Don't auto-remove the highlight on a timeout. Remove it on the user's first interaction after it appears:
This costs nothing and serves both audiences. The highlight disappears naturally the moment the user acts — which is exactly when they no longer need it.
🟡 Medium —
animate-pulseis the wrong highlight patternanimate-pulsesignals "loading" in most design systems — it will read as a spinner or skeleton state, not as "this is the comment you're looking for". Use a left-border flash instead:Clear, directional, non-alarming. Fades out smoothly when the class is removed.
🟡 Medium — Scroll timing must wait for panel render
The
$effectthat scrolls to the target comment must fire after the panel has rendered its comment list — comments are fetched async, so the element may not exist in the DOM when the query param is first read.The
comments.length > 0dependency guard is the key — it ensures the effect re-runs after the async fetch resolves. This is the most common failure point for deep-link scroll behavior and should be called out explicitly in implementation notes.🟡 Medium — Mobile panel behavior is unspecified
On desktop the comment panel is a side drawer; on mobile it's presumably a bottom sheet or full-screen. Auto-opening a bottom sheet can obscure the document entirely on a small viewport. Specify:
✅ What's good
annotationIddiscriminator to route between side drawer and bottom drawer — clean, uses existingDocumentComment.annotationIdfield, no new infrastructure./login, then back to full deep link after auth) is correctly specified.commentIdfalls back to normal page load without error — correct graceful degradation.QA Review — @saraholt
The spec is clean and Leonie's UX comments cover the critical interaction gaps. Two things need to be encoded as tests, not just implementation notes.
🔴 High — the interaction-based highlight must be verified in E2E, not just specified
Leonie's requirement (highlight persists until user interaction, not a 2-second timer) is easy to implement incorrectly and easy to verify if we have a test:
Without this test, a future refactor can swap in a
setTimeoutremoval and the requirement silently regresses.🔴 High — scroll timing must be tested with async comment loading
Leonie's note about
comments.length > 0as the$effectguard is the most common failure point for deep-link scroll behavior. Verify it:🟡 Complete E2E coverage for the three scenarios
The issue spec lists three E2E scenarios. Add these to the Playwright suite as written — they map directly to the implementation:
annotationIdpresent → side drawer opens (not bottom drawer)annotationIdabsent → bottom drawer opens/login→ after login, returns to full URL with?commentId=intactcommentId→ page loads normally, no panel auto-opens, no errorThe login-redirect scenario is the one most likely to regress silently — SvelteKit's
redirecthandling needs to preserve query params through the auth flow.✅ What's solid
annotationIddiscriminator to route between side drawer and bottom drawer — uses existingDocumentComment.annotationIdfield, no new infrastructure, cleancommentIdis correctly specifiedNotificationServicedeep-link construction (withannotationIdconditional) is covered in the #71 unit tests