Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
6.7 kB · 163 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164// Toolbar state presentation. The pure mappings — a tab's PageState to an// IconState, and an IconState to a native action-badge spec — live here so// they can be unit-tested. The toolbar icon itself is always the bare// substandard sticker (the manifest PNGs, rendered from brand/sticker.svg;// its crooked frame reaches the tile edge, so it is never redrawn or// cropped); state is shown via Chrome's badge layer, which the toolbar// draws on top of the icon and lets overflow the tile corner.
import type { PageState } from './types'
/** * What the user is to a publication the page carries. This axis is * independent of whether the page is the publication's verified home, so it * is named separately: every relation exists in a verified and an unverified * form, and the badge shows both at once rather than picking a winner. */export type Relation = /** Signed in, not subscribed. */ | 'detected' /** Subscription state unknown: signed out, or the list could not be read. */ | 'signedout' /** Subscribed. */ | 'subscribed'
export type IconState = /** Not an http(s) page (chrome://, the web store, ...); detection does not apply. */ | 'idle' /** Navigation started or a re-check is running; result not in yet. */ | 'checking' /** Page checked; no standard.site publication found. */ | 'none' /** Detection could not run because the browser is offline. Not a property * of the page, so it carries no badge — the popup and tooltip explain. */ | 'offline' /** Detection errored for this page, so nothing is known about it. */ | 'failed' /** The publication belongs to an account the signed-in user has blocked. */ | 'blocked' /** A verified publication, plus what the user is to it. */ | Relation /** The same, on a page that failed bidirectional verification. */ | `${Relation}-unverified`
/** Map a tab's detection state to an icon state. */export function iconStateFor(state: PageState | undefined): IconState { if (!state) return 'idle' if (!state.pub) { if (state.error === 'offline') return 'offline' return state.error ? 'failed' : 'none' } // A block is the user's own standing decision about the account behind the // page, so it outranks every other publication state — including a // subscription, where the contradiction is exactly what needs surfacing. if (state.blocked) return 'blocked' const relation: Relation = state.subscriptionRkey ? 'subscribed' : state.subscriptionRkey === null ? 'detected' : 'signedout' // Verification does not compete with the relation, it qualifies it: a // subscription to a publication seen somewhere that is not its home is // both facts at once, and suppressing either one loses something the user // would want to know. return state.pub.verified ? relation : `${relation}-unverified`}
// --- badge mapping -----------------------------------------------------------
const GREEN = '#1a7f37'const GREY = '#8b949e'const AMBER = '#d4a72c'const RED = '#cf222e'const INK = '#24292f'const WHITE = '#ffffff'
export interface BadgeSpec { text: string background: string color: string}
/** * Native badge for a state; null clears the badge. A badge appears only when * the page holds something worth noticing, so its presence is itself the * first signal — `none` and `offline` stay bare like `idle`. The channels * split so no state needs color vision: green means "verified publication", * amber means "needs a look", red means "you blocked this account", and the * glyph carries the user's relationship (+ can subscribe, ? unknown, * ✓ subscribed, X blocked). Glyphs are plain chars that stay crisp at badge * size ("✗"-style dingbats render smudged there, which is why the block badge * is a bare letter). * * A trailing `*` is the one modifier, and it reads as a footnote: the * relation still holds, but this page is not the publication's verified * home, and the popup says why. It rides along with the amber so the caveat * survives without color vision, and it keeps `!` for the other thing amber * used to mean — detection failing outright, where there is no relation to * qualify. */export function badgeFor(state: IconState): BadgeSpec | null { switch (state) { case 'idle': case 'none': case 'offline': return null case 'checking': return { text: '•', background: GREY, color: WHITE } case 'failed': return { text: '!', background: AMBER, color: INK } case 'blocked': return { text: 'X', background: RED, color: WHITE } case 'detected': return { text: '+', background: GREEN, color: WHITE } case 'signedout': return { text: '?', background: GREEN, color: WHITE } case 'subscribed': return { text: '✓', background: GREEN, color: WHITE } case 'detected-unverified': return { text: '+*', background: AMBER, color: INK } case 'signedout-unverified': return { text: '?*', background: AMBER, color: INK } case 'subscribed-unverified': return { text: '✓*', background: AMBER, color: INK } }}
const UNVERIFIED = 'this site is not the verified publisher'
/** * Action tooltip for a state; null restores the manifest default title. * The badge encodes state in color + a glyph, so the tooltip is the only * place the toolbar spells the state out — for hover, and for screen * readers, which announce the action by its title. The `*` states say both * halves, in the order the badge shows them. */export function titleFor(state: IconState): string | null { switch (state) { case 'idle': return null case 'checking': return 'substandard — checking this page' case 'none': return 'substandard — no publication on this page' case 'offline': return 'substandard — offline; publication lookups need a connection' case 'failed': return 'substandard — could not check this page' case 'blocked': return 'substandard — you have blocked the account behind this publication' case 'detected': return 'substandard — publication detected' case 'signedout': return 'substandard — publication detected; subscription state unknown' case 'subscribed': return 'substandard — subscribed to this publication' case 'detected-unverified': return `substandard — publication detected, but ${UNVERIFIED}` case 'signedout-unverified': return `substandard — publication detected, subscription state unknown; ${UNVERIFIED}` case 'subscribed-unverified': return `substandard — subscribed, but ${UNVERIFIED}` }}