Shared foundation: - New Modal component on native <dialog>.showModal() (focus trap, inert background, Escape, focus restore, scroll lock); all eight dialogs and both globalThis.confirm calls migrated to it - Design tokens for success/warning/danger surfaces; pill danger variant; .sr-only utility; toast layering above dialog backdrops; not-allowed cursor for validity-disabled buttons Conversations/attention: auto-scroll with jump-to-latest, per-conversation draft persistence, suggestion undo, 30s auto-refresh with refresh controls, unread emphasis and counts, blur-consistent share dialog, empty-accounts guidance, collapsed user lookup, relative timestamps, danger pills for critical items Media/library/outreach: click-blurred-to-reveal, lightbox with arrow navigation and captions, dirty-form preservation across refetches, upload validation with thumbnail preview, save feedback, result counts with load-more and clear-filters, ticking countdowns from a single clock Overview/performance/operations/settings: mutation errors inside the confirm dialog, live sending-window countdown, offline-aware loading states, per-panel loading/error with retry, 7/30/90-day range toggle, console scroll yielding with shown/retained counts, risky-save matching by subreddit name instead of index, useBlocker unsaved-changes guard, single-pass validation with focus-to-first-error, access panel JSON validation and input reset Tests: 806 -> 844 passing; both typechecks clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
6.1 KiB
TypeScript
113 lines
6.1 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
|
|
import {
|
|
useImportRedditSessionMutation,
|
|
useLoginStatusQuery,
|
|
useRedditAccountsQuery,
|
|
} from './settings-queries.js';
|
|
|
|
function errorMessage(error: Error | null): string {
|
|
return error?.message ?? 'Something went wrong.';
|
|
}
|
|
|
|
function sessionLabel(status: ReturnType<typeof useLoginStatusQuery>['data']): string {
|
|
if (!status) return 'Unknown';
|
|
if (!status.ready) return 'Browser unavailable';
|
|
if ('error' in status) return 'Check failed';
|
|
return {
|
|
unknown: 'Unknown',
|
|
authenticated: 'Authenticated',
|
|
'login-required': 'Login required',
|
|
unreachable: 'Reddit unreachable',
|
|
}[status.status];
|
|
}
|
|
|
|
export function AccessPanel() {
|
|
const statusQuery = useLoginStatusQuery();
|
|
const accountsQuery = useRedditAccountsQuery();
|
|
const importMutation = useImportRedditSessionMutation();
|
|
const importInput = useRef<HTMLInputElement>(null);
|
|
const [importFileError, setImportFileError] = useState<string | null>(null);
|
|
const resetImportMutation = importMutation.reset;
|
|
|
|
useEffect(() => {
|
|
if (!importMutation.isSuccess) return;
|
|
const timer = globalThis.setTimeout(() => resetImportMutation(), 5_000);
|
|
return () => globalThis.clearTimeout(timer);
|
|
}, [importMutation.isSuccess, resetImportMutation]);
|
|
|
|
async function importSession(file: File | undefined) {
|
|
if (!file) return;
|
|
setImportFileError(null);
|
|
let exported: unknown;
|
|
try {
|
|
exported = JSON.parse(await file.text());
|
|
} catch {
|
|
importMutation.reset();
|
|
setImportFileError("That file isn't valid JSON. Export a fresh session dump from the extension and try again.");
|
|
return;
|
|
}
|
|
importMutation.mutate(exported);
|
|
}
|
|
|
|
if (statusQuery.isPending) {
|
|
return <section aria-label="Loading access" className="panel panel-loading" role="status"><span className="spinner" />Loading Reddit access...</section>;
|
|
}
|
|
|
|
if (statusQuery.isError) {
|
|
return (
|
|
<section className="panel error-panel" role="alert">
|
|
<strong>Reddit access unavailable</strong><span>{errorMessage(statusQuery.error)}</span>
|
|
<button className="button secondary" onClick={() => void statusQuery.refetch()} type="button">Retry access</button>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const session = statusQuery.data;
|
|
const username = session.ready && 'loggedIn' in session && session.loggedIn ? session.username ?? null : null;
|
|
return (
|
|
<section aria-labelledby="access-heading" className="panel settings-section">
|
|
<div className="panel-heading"><div><span className="eyebrow">Reddit accounts</span><h2 id="access-heading">Access</h2></div><span className={session.ready && 'status' in session && session.loggedIn ? 'pill success' : 'pill warning'}>{sessionLabel(session)}</span></div>
|
|
<p className="settings-copy">Add each Reddit account with its own extension session dump. Content and sync state stay owned by that account in the database. Any account with an expired login pauses while waiting for a fresh session dump; other accounts keep running.</p>
|
|
{username ? <p className="settings-copy">Verified account: <strong><RedditUsernameLink username={username} /></strong>.</p> : null}
|
|
<div aria-label="Reddit accounts" className="reddit-account-list">
|
|
{accountsQuery.isPending ? <p className="settings-copy" role="status"><span className="spinner" /> Loading Reddit accounts…</p> : null}
|
|
{accountsQuery.isError ? (
|
|
<div className="settings-notice error" role="alert">
|
|
<span>Could not load Reddit accounts: {errorMessage(accountsQuery.error)}</span>{' '}
|
|
<button className="button secondary" onClick={() => void accountsQuery.refetch()} type="button">Retry accounts</button>
|
|
</div>
|
|
) : null}
|
|
{(accountsQuery.data ?? []).map((account) => (
|
|
<div className={account.isRunning ? 'reddit-account-row is-active' : 'reddit-account-row'} key={account.accountId}>
|
|
<div><strong><RedditUsernameLink username={account.redditUsername} /></strong><span>{account.isRunning ? 'Running independently' : 'Waiting for a refreshed session'}</span></div>
|
|
<span className={account.isRunning ? 'pill success' : 'pill warning'}>{account.isRunning ? 'Running' : 'Paused'}</span>
|
|
</div>
|
|
))}
|
|
{accountsQuery.isSuccess && accountsQuery.data.length === 0 ? <p className="settings-copy">No Reddit accounts have been added yet.</p> : null}
|
|
</div>
|
|
<div className="access-actions">
|
|
<input
|
|
accept="application/json"
|
|
aria-label="Reddit session export"
|
|
className="sr-only"
|
|
onChange={(event) => {
|
|
const file = event.target.files?.[0];
|
|
// Reset the input so picking the same file again re-triggers change.
|
|
event.target.value = '';
|
|
void importSession(file);
|
|
}}
|
|
ref={importInput}
|
|
type="file"
|
|
/>
|
|
<button className="button primary" disabled={importMutation.isPending} onClick={() => importInput.current?.click()} type="button">{importMutation.isPending ? 'Adding account...' : 'Add or refresh account'}</button>
|
|
<button aria-busy={statusQuery.isFetching ? 'true' : undefined} className="button secondary" disabled={statusQuery.isFetching} onClick={() => void statusQuery.refetch()} type="button">{statusQuery.isFetching ? 'Checking sessions...' : 'Recheck accounts'}</button>
|
|
</div>
|
|
<p className="settings-copy">Open Reddit Chat in Chrome for the account you want to add, wait for the extension badge to show OK, then upload its JSON dump. It contains login credentials; do not share it.</p>
|
|
{importFileError ? <div className="settings-notice error" role="alert">{importFileError}</div> : null}
|
|
{importMutation.isSuccess ? <div className="settings-notice success" role="status"><span>Reddit session imported and verified.</span> <span>Account runtime is running independently.</span></div> : null}
|
|
{importMutation.isError ? <div className="settings-notice error" role="alert">{errorMessage(importMutation.error)}</div> : null}
|
|
</section>
|
|
);
|
|
}
|