Skip to content

feat/thread-deep-link#664

Merged
jbpenrath merged 1 commit into
developmentfrom
feat/thread-deep-link
May 19, 2026
Merged

feat/thread-deep-link#664
jbpenrath merged 1 commit into
developmentfrom
feat/thread-deep-link

Conversation

@jbpenrath
Copy link
Copy Markdown
Contributor

@jbpenrath jbpenrath commented May 12, 2026

Purpose

Add thread/message deep-linking feature to allow user to share thread with other mailbox users with ease.

Also, it allows to reference a thread-event or a message to automatically scroll to this resource.

CleanShot.2026-05-19.at.18.12.19.mp4

Summary by CodeRabbit

  • New Features

    • Copy-link actions for threads, messages, and comments with clipboard confirmation.
    • Deep-linking to specific messages/events with smooth scroll and visual highlight.
    • Access to threads not present in current filtered lists (deep-linked retrieval).
  • Bug Fixes

    • Thread panel auto-close behavior refined to avoid unexpectedly unselecting deep-linked/out-of-list threads.
    • Retrieve endpoint returns deep-linked threads even if marked trashed/spam for authorized users.
  • Localization

    • Added English and French translations for copy-link actions and confirmation.
  • Tests

    • Added API tests covering thread retrieval and access rules.

Review Change Stack

@jbpenrath jbpenrath self-assigned this May 12, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f79effe-037c-461c-b08e-c69d009fe2b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements deep-linking and hash-based navigation to specific messages and events within threads. It adds backend support for retrieving spam/trashed threads, frontend hooks for copying deep-link URLs to clipboard, mailbox-context-aware data loading for missing threads, hash parsing with deferred smooth-scroll and highlight mechanics, UI copy-link integrations, tests, locales, and highlight animations.

Changes

Deep-linking and hash-based navigation

Layer / File(s) Summary
Backend thread retrieve endpoint
src/backend/core/api/viewsets/thread.py, src/backend/core/tests/api/test_threads_list.py
ThreadViewSet.retrieve() override disables spam/trashed exclusions while enforcing object-level permissions; tests verify retrieval for users with access and 404 for users without access.
Frontend deep-link hook and localization
src/frontend/src/features/message/use-copy-deep-link.tsx, src/frontend/public/locales/common/en-US.json, src/frontend/public/locales/common/fr-FR.json
Adds useCopyDeepLink hook to build thread/message/event deep links and copy them to clipboard with toast/error handling; adds English and French locale keys for copy actions and clipboard confirmation.
Mailbox context deep-link aware thread loading
src/frontend/src/features/providers/mailbox.tsx
Provider reads threadId from route, uses useThreadsRetrieve fallback for missing threads, merges fallback into selectedThread, redirects to default folder on retrieve failure, and invalidates the retrieve-cache key; route rewrite uses searchParams.toString().
Thread panel auto-close logic for deep-linked threads
src/frontend/src/features/layouts/components/thread-panel/index.tsx, src/frontend/src/features/layouts/components/thread-panel/components/thread-panel-header.tsx
ThreadPanel tracks whether selectedThread was previously in list and only auto-unselects when a previously-seen thread disappears; ThreadPanelTitle falls back to translated "Messages" when folder name is absent.
Hash-based navigation and scroll-to-element with highlight
src/frontend/src/features/layouts/components/thread-view/index.tsx, src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx
ThreadView prioritizes hash anchors, defers scroll via rAF, re-queries target, performs measured scroll with scrollend/timeout fallback, and applies temporary highlight classes with cleanup; ThreadMessage auto-unfolds on matching hash.
Copy-link UI integrations in action menus
src/frontend/src/features/layouts/components/thread-view/components/thread-action-bar/index.tsx, src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx, src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx
Adds copy-link actions for thread/message/comment that call useCopyDeepLink and close menus on success while retaining conditional edit/delete/leave behavior.
CSS animations for focus highlight and dimming
src/frontend/src/features/layouts/components/thread-view/_index.scss
Adds .thread-view__highlight, thread-view-highlight-focus and thread-view-dim-cycle keyframes, and prefers-reduced-motion fallback rules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • suitenumerique/messages#642: Modifies mailbox/thread cache and retrieve/invalidation logic in src/frontend/src/features/providers/mailbox.tsx, overlapping provider behavior.
  • suitenumerique/messages#453: Previously changed thread-view initialization and scroll-to-element behavior that may interact with this PR's hash-based navigation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat/thread-deep-link' accurately summarizes the main feature added across the pull request: enabling deep-linking to threads, messages, and events.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jbpenrath jbpenrath changed the title !wip feat/thread-deep-link May 12, 2026
@jbpenrath jbpenrath marked this pull request as ready for review May 19, 2026 14:52
@jbpenrath jbpenrath force-pushed the feat/thread-deep-link branch 2 times, most recently from 72c4152 to a16bc49 Compare May 19, 2026 16:21
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx (1)

210-213: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enable long-press actions for non-authors on touch devices.

Line 210 to Line 213 gate long-press handlers behind canModify, but the copy-link action is available to everyone (Line 226 onward). On touch-only devices, non-authors can’t open the actions UI, so they can’t copy event links.

Suggested fix
-                        onTouchStart={canModify ? handleTouchStart : undefined}
-                        onTouchEnd={canModify ? cancelLongPress : undefined}
-                        onTouchMove={canModify ? cancelLongPress : undefined}
-                        onTouchCancel={canModify ? cancelLongPress : undefined}
+                        onTouchStart={handleTouchStart}
+                        onTouchEnd={cancelLongPress}
+                        onTouchMove={cancelLongPress}
+                        onTouchCancel={cancelLongPress}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx`
around lines 210 - 213, The touch handlers are gated by canModify so non-authors
on touch devices cannot trigger the long-press actions; remove the canModify
condition from the JSX props so onTouchStart uses handleTouchStart and
onTouchEnd/onTouchMove/onTouchCancel use cancelLongPress for all users (leave
internal logic in the action UI or handlers to hide actions that require
modification); specifically update the elements that currently conditionally set
onTouchStart={canModify ? handleTouchStart : undefined} and
onTouchEnd/onTouchMove/onTouchCancel={canModify ? cancelLongPress : undefined}
to always attach handleTouchStart and cancelLongPress so the copy-link and other
public actions are reachable via long-press.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/features/layouts/components/thread-panel/index.tsx`:
- Around line 65-77: The effect that updates selectedThreadWasInListRef can
carry over truthy state from a previous thread; to fix, reset the per-thread
flag whenever the selected thread changes: add a prevSelectedIdRef (or reuse
selectedThreadWasInListRef for storing the last selected id) and at the start of
the useEffect (which currently references selectedThread, threads and calls
unselectThread) check if selectedThread?.id !== prevSelectedIdRef.current — if
so set selectedThreadWasInListRef.current = false and update
prevSelectedIdRef.current = selectedThread?.id — then continue to compute
isInList and handle unselectThread as before so the “was in list” tracking is
fresh for each new selectedThread.

In `@src/frontend/src/features/providers/mailbox.tsx`:
- Around line 366-370: The fallback deep-link fetch via useThreadsRetrieve (the
fallbackThreadQuery) is missing the mailbox context, so update the call to pass
the current mailbox id (e.g., mailboxId, mailboxIdFromRoute, or
selectedMailbox?.id) into the retrieve options so mailbox-scoped
serialization/annotations are computed correctly; specifically, add mailbox_id
into the params/options object you pass to useThreadsRetrieve (while keeping the
existing enabled condition that uses threadIdFromRoute, flattenThreads and
threadInList).
- Around line 388-397: The effect handling fallbackThreadQuery.isError is
currently redirecting on any error; change it to only redirect when the query
error indicates authorization or not-found (e.g., HTTP status 401, 403, or 404
or a domain-specific error.code like 'NOT_FOUND'/'UNAUTHORIZED') by replacing
the simple truthy check with a guard that inspects fallbackThreadQuery.error
(e.g., fallbackThreadQuery.error?.status or error.code) before setting
redirectedOnErrorRef.current and calling
router.replace(`/mailbox/${selectedMailbox.id}?${defaultFilter}`) so transient
network/server errors (5xx, network) do not trigger the deep-link fallback; keep
existing usage of redirectedOnErrorRef, selectedMailbox, threadIdFromRoute and
MAILBOX_FOLDERS() in the same effect.

---

Outside diff comments:
In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx`:
- Around line 210-213: The touch handlers are gated by canModify so non-authors
on touch devices cannot trigger the long-press actions; remove the canModify
condition from the JSX props so onTouchStart uses handleTouchStart and
onTouchEnd/onTouchMove/onTouchCancel use cancelLongPress for all users (leave
internal logic in the action UI or handlers to hide actions that require
modification); specifically update the elements that currently conditionally set
onTouchStart={canModify ? handleTouchStart : undefined} and
onTouchEnd/onTouchMove/onTouchCancel={canModify ? cancelLongPress : undefined}
to always attach handleTouchStart and cancelLongPress so the copy-link and other
public actions are reachable via long-press.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 089579f2-7af0-4c57-bddb-cb35dad050e9

📥 Commits

Reviewing files that changed from the base of the PR and between 8e009c5 and a16bc49.

📒 Files selected for processing (14)
  • src/backend/core/api/viewsets/thread.py
  • src/backend/core/tests/api/test_threads_list.py
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-panel-header.tsx
  • src/frontend/src/features/layouts/components/thread-panel/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-action-bar/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx
  • src/frontend/src/features/layouts/components/thread-view/index.tsx
  • src/frontend/src/features/message/use-copy-deep-link.tsx
  • src/frontend/src/features/providers/mailbox.tsx

Comment thread src/frontend/src/features/layouts/components/thread-panel/index.tsx Outdated
Comment thread src/frontend/src/features/providers/mailbox.tsx
Comment thread src/frontend/src/features/providers/mailbox.tsx
@jbpenrath jbpenrath force-pushed the feat/thread-deep-link branch 3 times, most recently from de85d02 to e00d926 Compare May 19, 2026 17:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/frontend/src/features/providers/mailbox.tsx (1)

395-405: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict the fallback redirect to auth/not-found failures.

This still redirects on any retrieve error, so a transient network or 5xx failure drops the user out of the deep-link flow instead of letting the query recover. Only 401/403/404-style failures should trigger the mailbox fallback.

💡 Suggested fix
     useEffect(() => {
         if (!fallbackThreadQuery.isError) {
             redirectedOnErrorRef.current = false;
             return;
         }
+        const error = fallbackThreadQuery.error;
+        if (!(error instanceof APIError) || ![401, 403, 404].includes(error.code)) {
+            return;
+        }
         if (redirectedOnErrorRef.current) return;
         if (!selectedMailbox || !threadIdFromRoute) return;
         redirectedOnErrorRef.current = true;
         const defaultFilter = new URLSearchParams(MAILBOX_FOLDERS()[0].filter).toString();
         router.replace(`/mailbox/${selectedMailbox.id}?${defaultFilter}`);
-    }, [fallbackThreadQuery.isError, selectedMailbox, threadIdFromRoute]);
+    }, [fallbackThreadQuery.isError, fallbackThreadQuery.error, selectedMailbox, threadIdFromRoute, router]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/features/providers/mailbox.tsx` around lines 395 - 405, The
effect currently redirects whenever fallbackThreadQuery.isError is true; change
it to only redirect for authentication/not-found failures (HTTP 401, 403, 404).
In the useEffect that references fallbackThreadQuery.isError, inspect
fallbackThreadQuery.error (or its status/code) to determine the HTTP status and
only proceed with setting redirectedOnErrorRef.current = true and calling
router.replace(`/mailbox/${selectedMailbox.id}?${defaultFilter}`) when the
status is 401, 403, or 404; for other errors (network/5xx) return early and let
the query recover. Keep the existing guards for redirectedOnErrorRef,
selectedMailbox, and threadIdFromRoute and ensure redirectedOnErrorRef is only
set when performing the redirect; use MAILBOX_FOLDERS() to build defaultFilter
as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/features/layouts/components/thread-view/index.tsx`:
- Around line 194-208: When a deep-link hash (hashMatch) targets a trashed
message that isn't visible, enable showTrashedMessages before the filtered
timeline is built so the DOM element exists for selector/#thread-message-…;
specifically, in the initialization where you compute hash, hashMatch, selector,
scrollBehavior and highlightTargetId, detect the hash target id and, if that
message is trashed and showTrashedMessages is false (and we’re not already in
the trash view), set showTrashedMessages = true (or call the setter) before the
code that builds/filters the timeline so the subsequent scroll/highlight logic
(selector, scrollBehavior, highlightTargetId) can find the element.

---

Duplicate comments:
In `@src/frontend/src/features/providers/mailbox.tsx`:
- Around line 395-405: The effect currently redirects whenever
fallbackThreadQuery.isError is true; change it to only redirect for
authentication/not-found failures (HTTP 401, 403, 404). In the useEffect that
references fallbackThreadQuery.isError, inspect fallbackThreadQuery.error (or
its status/code) to determine the HTTP status and only proceed with setting
redirectedOnErrorRef.current = true and calling
router.replace(`/mailbox/${selectedMailbox.id}?${defaultFilter}`) when the
status is 401, 403, or 404; for other errors (network/5xx) return early and let
the query recover. Keep the existing guards for redirectedOnErrorRef,
selectedMailbox, and threadIdFromRoute and ensure redirectedOnErrorRef is only
set when performing the redirect; use MAILBOX_FOLDERS() to build defaultFilter
as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2750da11-f7a0-4a41-87c0-8cb65c9ffa61

📥 Commits

Reviewing files that changed from the base of the PR and between a16bc49 and e00d926.

📒 Files selected for processing (14)
  • src/backend/core/api/viewsets/thread.py
  • src/backend/core/tests/api/test_threads_list.py
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-panel-header.tsx
  • src/frontend/src/features/layouts/components/thread-panel/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-action-bar/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-event/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/thread-message-actions.tsx
  • src/frontend/src/features/layouts/components/thread-view/index.tsx
  • src/frontend/src/features/message/use-copy-deep-link.tsx
  • src/frontend/src/features/providers/mailbox.tsx

Comment thread src/frontend/src/features/layouts/components/thread-view/index.tsx
@jbpenrath jbpenrath force-pushed the feat/thread-deep-link branch from e00d926 to b747c4e Compare May 19, 2026 20:58
@jbpenrath jbpenrath changed the base branch from main to development May 19, 2026 20:58
Allow user to copy/paste a thread link with other mailbox users.
Currently, if the user copy the current url, the link is broken once
the thread has been moved from the origin folder. It is also possible
to target a message or internal message.
@jbpenrath jbpenrath force-pushed the feat/thread-deep-link branch from b747c4e to 8c3d576 Compare May 19, 2026 21:02
@jbpenrath jbpenrath merged commit 733be91 into development May 19, 2026
9 checks passed
@jbpenrath jbpenrath deleted the feat/thread-deep-link branch May 19, 2026 21:20
jbpenrath added a commit that referenced this pull request May 20, 2026
Allow user to copy/paste a thread link with other mailbox users.
Currently, if the user copy the current url, the link is broken once
the thread has been moved from the origin folder. It is also possible
to target a message or internal message.
jbpenrath added a commit that referenced this pull request May 20, 2026
Allow user to copy/paste a thread link with other mailbox users.
Currently, if the user copy the current url, the link is broken once
the thread has been moved from the origin folder. It is also possible
to target a message or internal message.
jbpenrath added a commit that referenced this pull request May 20, 2026
Allow user to copy/paste a thread link with other mailbox users.
Currently, if the user copy the current url, the link is broken once
the thread has been moved from the origin folder. It is also possible
to target a message or internal message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant