Skip to content

feat(web): redirect logged-in users from root to /threads (opt-in)#950

Open
AchoArnold wants to merge 10 commits into
mainfrom
feat/redirect-to-threads
Open

feat(web): redirect logged-in users from root to /threads (opt-in)#950
AchoArnold wants to merge 10 commits into
mainfrom
feat/redirect-to-threads

Conversation

@AchoArnold

Copy link
Copy Markdown
Member

What

Logged-in users can opt in (per-browser) to skip the marketing landing page and go straight to /threads.

  • Optimistic redirect — a page middleware on / reads a localStorage flag synchronously and redirects to /threads before the heavy landing page renders. /threads' existing auth guard is the correctness backstop.
  • Opt-in UX — a persistent popover under the Dashboard button ("Skip this page next time?" + ✕ + "Always open dashboard →"). The link enables the redirect; ✕ dismisses for the session only.
  • Configurable & per-browser — state lives in localStorage (key httpsms_redirect_to_threads). No backend/API changes.
  • Logout clears it — both logout sites reset the store value and remove the localStorage flag, so a fresh login starts opted-out.

Testing

  • pnpm lint (eslint + stylelint + prettier) — pass
  • pnpm run generate — pass (25 routes prerendered, 0 link errors)

Design spec and implementation plan are included under docs/superpowers/ (force-added; docs/ is gitignored).

AchoArnold and others added 8 commits July 13, 2026 22:48
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Back button

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
@codacy-production

codacy-production Bot commented Jul 13, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 high

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
ErrorProne 2 high

View in Codacy

🟢 Metrics 8 complexity · 0 duplication

Metric Results
Complexity 8
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR lets logged-in users opt in (per-browser) to skip the marketing landing page and be redirected straight to /threads. State is stored in localStorage under the key httpsms_redirect_to_threads, owned by a new Pinia store. An optimistic page-scoped middleware on index.vue reads the flag synchronously before render, and a persistent popover under the Dashboard button exposes the opt-in/dismiss UX. Both logout paths (MessageThreadHeader.vue and settings/index.vue) reset the preference.

  • New redirectPreference Pinia store handles localStorage reads/writes with try/catch and exposes enable, dismiss, and resetState actions.
  • A redirectToThreads route middleware on index.vue reads the flag and issues an optimistic navigateTo('/threads', { replace: true }) before the heavy landing page renders.
  • The RedirectPromptPopover component is mounted in website.vue adjacent to the Dashboard button, and only renders when the user is authenticated, on the root route, and has not yet opted in or dismissed for the session.

Confidence Score: 4/5

The change is client-only with no backend impact; the worst-case failure is a redirect that doesn't persist across page reloads in private-mode browsers, with the existing /threads auth guard as a correctness backstop.

The implementation is well-structured and follows existing patterns. The two issues — a duplicated magic string between the store and the middleware, and navigation proceeding after a failed localStorage write — are both narrow edge cases that affect only private-mode or storage-disabled browsers and degrade gracefully rather than breaking anything.

web/app/stores/redirectPreference.ts and web/app/middleware/redirectToThreads.ts both define or read the same storage key independently; aligning them is the only area worth a second look before merge.

Important Files Changed

Filename Overview
web/app/stores/redirectPreference.ts New Pinia store owning the redirect preference; correctly wraps all localStorage access in try/catch and exposes enable/dismiss/resetState actions. Minor: enable() navigates to /threads even when the localStorage write fails, so the preference won't persist across reloads in private-mode browsers.
web/app/middleware/redirectToThreads.ts New page-scoped middleware that reads localStorage and issues an optimistic redirect; correctly returns the navigateTo result and wraps in try/catch. The localStorage key string is hardcoded rather than imported from the store's constant, creating a maintenance coupling.
web/app/components/RedirectPromptPopover.vue New component rendering the opt-in popover; showPopover computed correctly gates on route, auth state, and both preference flags. Positioned absolutely under the Dashboard button container.
web/app/layouts/website.vue Dashboard button wrapped in a relatively-positioned container to anchor the popover; auth guard moved from v-btn to the wrapper div. Clean change.
web/app/components/MessageThreadHeader.vue redirectPreferenceStore.resetState() added to logout() alongside existing store resets. Correct.
web/app/pages/settings/index.vue redirectPreferenceStore.resetState() added to deleteUserAccount() which is the only sign-out path in this file. Correct placement.
web/app/pages/index.vue middleware: ['redirect-to-threads'] added to definePageMeta. Minimal, correct change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User Browser
    participant MW as redirectToThreads Middleware
    participant LS as localStorage
    participant IDX as index.vue (Landing Page)
    participant PPOP as RedirectPromptPopover
    participant STORE as redirectPreferenceStore
    participant THR as /threads

    Note over U,THR: First visit (opted-out)
    U->>MW: Navigate to /
    MW->>LS: getItem('httpsms_redirect_to_threads')
    LS-->>MW: null
    MW-->>IDX: Proceed (no redirect)
    IDX->>PPOP: "Render popover (authUser != null)"
    PPOP->>U: Show Skip this page next time?

    Note over U,THR: User clicks Always open dashboard
    U->>STORE: enable()
    STORE->>LS: setItem('httpsms_redirect_to_threads', 'true')
    STORE->>THR: "navigateTo('/threads', {replace: true})"

    Note over U,THR: Subsequent visit (opted-in)
    U->>MW: Navigate to /
    MW->>LS: getItem('httpsms_redirect_to_threads')
    LS-->>MW: 'true'
    MW->>THR: "navigateTo('/threads', {replace: true})"

    Note over U,THR: Logout
    U->>STORE: resetState()
    STORE->>LS: removeItem('httpsms_redirect_to_threads')
    STORE-->>U: "enabled=false, dismissedThisSession=false"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User Browser
    participant MW as redirectToThreads Middleware
    participant LS as localStorage
    participant IDX as index.vue (Landing Page)
    participant PPOP as RedirectPromptPopover
    participant STORE as redirectPreferenceStore
    participant THR as /threads

    Note over U,THR: First visit (opted-out)
    U->>MW: Navigate to /
    MW->>LS: getItem('httpsms_redirect_to_threads')
    LS-->>MW: null
    MW-->>IDX: Proceed (no redirect)
    IDX->>PPOP: "Render popover (authUser != null)"
    PPOP->>U: Show Skip this page next time?

    Note over U,THR: User clicks Always open dashboard
    U->>STORE: enable()
    STORE->>LS: setItem('httpsms_redirect_to_threads', 'true')
    STORE->>THR: "navigateTo('/threads', {replace: true})"

    Note over U,THR: Subsequent visit (opted-in)
    U->>MW: Navigate to /
    MW->>LS: getItem('httpsms_redirect_to_threads')
    LS-->>MW: 'true'
    MW->>THR: "navigateTo('/threads', {replace: true})"

    Note over U,THR: Logout
    U->>STORE: resetState()
    STORE->>LS: removeItem('httpsms_redirect_to_threads')
    STORE-->>U: "enabled=false, dismissedThisSession=false"
Loading

Reviews (1): Last reviewed commit: "fix(web): use replace navigation in redi..." | Re-trigger Greptile

Comment on lines +1 to +9
export default defineNuxtRouteMiddleware(() => {
try {
if (localStorage.getItem('httpsms_redirect_to_threads') === 'true') {
return navigateTo('/threads', { replace: true })
}
} catch (error) {
console.error(error)
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The localStorage key is hardcoded here as a string literal, while the store defines it as a constant STORAGE_KEY. If the key ever needs to change, this file won't be caught by a simple search-and-replace on the constant, and the redirect middleware will silently stop working while the store continues writing the new key.

Suggested change
export default defineNuxtRouteMiddleware(() => {
try {
if (localStorage.getItem('httpsms_redirect_to_threads') === 'true') {
return navigateTo('/threads', { replace: true })
}
} catch (error) {
console.error(error)
}
})
import { STORAGE_KEY } from '~/stores/redirectPreference'
export default defineNuxtRouteMiddleware(() => {
try {
if (localStorage.getItem(STORAGE_KEY) === 'true') {
return navigateTo('/threads', { replace: true })
}
} catch (error) {
console.error(error)
}
})

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@@ -0,0 +1,46 @@
import { defineStore } from 'pinia'

const STORAGE_KEY = 'httpsms_redirect_to_threads'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The STORAGE_KEY constant is not exported, so the middleware cannot import it and must hardcode the string. Exporting it removes the duplication.

Suggested change
const STORAGE_KEY = 'httpsms_redirect_to_threads'
export const STORAGE_KEY = 'httpsms_redirect_to_threads'

Comment on lines +20 to +28
function enable() {
enabled.value = true
try {
localStorage.setItem(STORAGE_KEY, 'true')
} catch (error) {
console.error(error)
}
navigateTo('/threads', { replace: true })
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Navigation proceeds even when persistence fails

enabled.value = true is set unconditionally, then localStorage.setItem is attempted. If the write throws (e.g. private-mode quota denial), the catch block is entered but navigateTo('/threads', ...) still runs afterward. The user is taken to /threads for this session, but on their next visit the middleware finds no key in localStorage and the landing page renders again — giving the impression that the "Always open dashboard" action had no lasting effect.

pnpm 11.12.0 crashes pnpm/action-setup (pnpm/action-setup#276).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
Resolves Codacy type findings; removes gitignored docs/ files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
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