import { useEffect, useRef, useState } from 'react';
import type {
AccountEstablishmentSummary,
RedditAccountSummary,
} from '../../../../../packages/api-contracts/src/index.js';
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
import {
useImportRedditSessionMutation,
useLoginStatusQuery,
useRedditAccountsQuery,
useRemoveRedditAccountMutation,
} from '../settings/settings-queries.js';
import './accounts.css';
const CONFIRM_TIMEOUT_MS = 8_000;
function errorMessage(error: Error | null): string {
return error?.message ?? 'Something went wrong.';
}
function karmaLabel(value: number | null): string {
return value === null ? 'unknown' : value.toLocaleString();
}
function ageLabel(days: number | null): string {
if (days === null) return 'unknown';
if (days < 1) return 'less than a day';
return `${days.toLocaleString()} day${days === 1 ? '' : 's'}`;
}
function retryLabel(retryAt: number | null): string {
if (retryAt === null) return 'Will be re-tested on the next profile check.';
const minutes = Math.round((retryAt - Date.now()) / 60_000);
if (minutes <= 0) return 'Due for a re-test now.';
if (minutes < 60) return `Next re-test in ${minutes} minute${minutes === 1 ? '' : 's'}.`;
const hours = Math.round(minutes / 60);
return `Next re-test in about ${hours} hour${hours === 1 ? '' : 's'}.`;
}
/**
* Two different situations share this panel: Reddit actively refusing
* invitations from the account (sending is on hold), and the account merely
* sitting below our own configured thresholds (nothing is blocked). Conflating
* them reads as a broken account when it is only a soft signal.
*/
function EstablishmentDetail({ establishment }: { establishment: AccountEstablishmentSummary }) {
const belowThresholds = establishment.state === 'unestablished' && !establishment.sendingHeld;
return (
{establishment.sendingHeld ? (
<>
{establishment.lastProbeFailure
?? 'Reddit rejected an invitation from this account as unestablished.'}
Outbound sending is on hold. {retryLabel(establishment.retryAt)}
>
) : null}
{belowThresholds ? (
<>
{establishment.reasons.join('; ')}
Still sending normally — these are our own configured thresholds, not limits Reddit
publishes. The account is only held back if Reddit actually rejects an invitation.
);
}
function establishmentPill(establishment: AccountEstablishmentSummary | null | undefined) {
if (!establishment || establishment.state === 'unknown') return null;
if (establishment.sendingHeld) return Sending on hold;
if (establishment.state === 'unestablished') return Below thresholds;
return Established;
}
/**
* Removal is destructive — the account's messages, activity and sync state go
* with it — so the button arms a confirmation that disarms itself if ignored.
*/
function AccountRow({ account }: { account: RedditAccountSummary }) {
const removeMutation = useRemoveRedditAccountMutation();
const [confirmRemove, setConfirmRemove] = useState(false);
const isRemoved = account.status === 'removed';
const isSuspended = account.status === 'suspended';
useEffect(() => {
if (!confirmRemove) return;
const timer = globalThis.setTimeout(() => setConfirmRemove(false), CONFIRM_TIMEOUT_MS);
return () => globalThis.clearTimeout(timer);
}, [confirmRemove]);
const rowClass = isRemoved
? 'account-row is-removed'
: isSuspended ? 'account-row is-suspended'
: account.isRunning ? 'account-row is-active' : 'account-row';
return (
{isRemoved
? 'Removed — its conversations and media are kept, read-only'
: isSuspended
? 'Suspended by Reddit — all bot work is paused'
: account.isRunning ? 'Running independently' : 'Waiting for a refreshed session'}
{isRemoved ? (
Import a session for this account to bring it back.
) : confirmRemove ? (
<>
Stops u/{account.redditUsername} and deletes its stored session. Conversations,
messages and media it collected are kept, but become read-only.
>
) : (
)}
Account suspendedReddit or Matrix confirmed this account is suspended. It cannot send, scan, or sync until you review it and import a newly verified session.
{pausedCount === 1 ? 'One account is paused.' : `${pausedCount} accounts are paused.`}
A paused account is waiting for a fresh session dump. Other accounts keep running normally.
) : null}
{suspendedAccounts.length > 0 ? (
{suspendedAccounts.length === 1
? `u/${suspendedAccounts[0].redditUsername} is suspended by Reddit.`
: `${suspendedAccounts.length} accounts are suspended by Reddit.`}
All work is paused for these accounts. Review the suspension before importing any new session.
) : null}
{heldAccounts.length > 0 ? (
{heldAccounts.length === 1
? `u/${heldAccounts[0].redditUsername} cannot start chats yet.`
: `${heldAccounts.length} accounts cannot start chats yet.`}
Reddit rejected an invitation from a new account, so outbound sending is on hold for it.
The bot re-tests periodically and resumes on its own.