All files / src/ui connectFronts.ts

100% Statements 42/42
100% Branches 14/14
100% Functions 2/2
100% Lines 42/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143                                                                                          2x 2x 2x 2x                                                       2x   2x         2x 5x 45x                     2x     196x 196x               2x 191x 191x 191x 191x 191x 191x 191x 191x 191x 191x 187x 187x 187x 187x 187x 187x 187x 191x         191x 187x 187x 187x 187x 187x 187x 187x 187x 187x 187x  
/**
 * The places a window can connect to, as data: an id, a name, a mark, a verb.
 *
 * Two doors offer the same four places and drew their own list of them. The
 * Connect dialog (`ui/ConnectCard.ts`) draws them as **tiles** — a place has a
 * name, and the tile is a tab over the form that connects it. The landing page
 * draws them as **buttons** — a reader who has just arrived has nothing to tab
 * between, so what they are offered is the gesture, and a button is named by
 * its verb. The two differ in exactly that word, which is why both are here
 * and why nothing else about a place is.
 *
 * No pane is here. The forms, the sign-ins and the flows behind them are the
 * dialog's, imported on a gesture (`App.openConnectDialog`), and this module
 * is deliberately light enough for the landing page to hold at load: three
 * constants and a list.
 *
 * ## Why the verb is here and not on the button
 *
 * *This machine* is the one place whose gesture differs between browsers:
 * Chromium and the desktop build hold a directory as a **grant** that survives
 * a reload, and Firefox and Safari hand over a **snapshot** of the file list
 * that does not (`browse/local.ts`). §8.5 does not hide the gesture there — it
 * works — so what carries the difference is the label, and the label is now on
 * two screens. Reading it off one list is what keeps the landing page's button
 * and the dialog's pane from promising different things about the same click.
 *
 * ## What `works` is for
 *
 * §8.5's doctrine is that a control which cannot keep its promise is absent
 * rather than disabled, and the two doors part company on the sign-ins. A
 * sign-in needs two things this page does not choose: a **build** that can be
 * registered — Google's OAuth registers an `http`/`https` origin, so the
 * desktop build's `app://viewer` cannot sign in at all (`auth/google.ts`) —
 * and a **site** that was built with the registration, the client id or the
 * app key its maintainer provides (`config/siteDefaults.ts`, §24.7). Either
 * one missing leaves the same press behind the button: an explanation.
 *
 * So `works` is both facts, and the two doors read them their own way. The
 * dialog still offers the tile, because a reader who went looking for Drive is
 * owed the sentence saying why it is not there, and its pane keeps a disabled
 * button beside that sentence so the sentence has a subject (§20). The landing
 * page offers a row of things to press, and a button that leads only to an
 * explanation is not one of them (§24.2).
 */
 
import { DROPBOX_SIGN_IN_AVAILABLE, readAppKey } from '../auth/dropbox'
import { GOOGLE_SIGN_IN_AVAILABLE, readClientId } from '../auth/google'
import { directoriesKept } from '../state/directories'
import { IS_STANDALONE } from '../version'
import type { IconName } from './icons'
 
/** The four places, by the id the tiles, the buttons and the tests name them by. */
export type ProviderId = 'local' | 'bucket' | 'drive' | 'dropbox'
 
export interface ProviderFront {
  id: ProviderId
  /** The tile's name: a place, because a tab is labelled by where it goes. */
  label: string
  /** The button's name: a gesture, because a button is labelled by what it does. */
  verb: string
  mark: IconName
  /** Whether pressing this in *this* build reaches something that can act. See the header. */
  works: boolean
}
 
/**
 * What the door to a folder is called, wherever it is drawn (§20): the landing
 * page's heading, the Files column's button, the palette's command. A button
 * label is lowercase and a heading or a command begins with a capital
 * (§26.4), so the one name is kept in both shapes.
 *
 * *Local or remote* only where there is a remote half. The standalone build
 * reaches this machine and nothing else (§19), and its landing page already said
 * *Connect a folder* while the Files column and the palette went on offering
 * the other half under the same name.
 */
export const CONNECT_FOLDER = IS_STANDALONE ? 'connect a folder' : 'connect a local or remote folder'
/** {@link CONNECT_FOLDER} as a heading or a command's title. */
export const CONNECT_FOLDER_TITLE = IS_STANDALONE ? 'Connect a folder' : 'Connect a local or remote folder'
/**
 * What that door reaches, for the line under it: the places {@link providerFronts}
 * offers in this build, as a phrase that begins a sentence.
 */
export const CONNECT_PLACES = IS_STANDALONE
  ? 'A folder on this machine'
  : 'A folder on this machine, a bucket, a Google Drive folder or a Dropbox'
 
/**
 * What *this machine*'s button says, wherever it is drawn.
 *
 * See the header: a browser with no picker gives a snapshot of the file list
 * for this visit, and this is where a reader is told so — before pressing, on
 * whichever of the two screens they are on. Exported as well as used below,
 * because the dialog's pane draws its button from the label rather than from
 * the front.
 */
export function localVerb(): string {
  // Kept only by the installed app (§20.6): a browser tab holds a directory
  // for the visit, as a browser with no picker holds its snapshot.
  return directoriesKept() ? 'choose a folder…' : 'choose a folder (this visit)…'
}
 
/**
 * The places this build can offer, in the order both doors show them.
 *
 * The standalone build is one HTML file with an opaque origin, so every remote
 * read fails before a byte moves (§19): it has this machine and nothing else.
 */
export function providerFronts(): ProviderFront[] {
  const out: ProviderFront[] = [
    {
      id: 'local',
      label: 'This machine',
      verb: localVerb(),
      mark: 'hard-drive',
      works: true,
    },
  ]
  if (IS_STANDALONE) return out
  out.push({ id: 'bucket', label: 'A bucket', verb: 'connect a bucket…', mark: 'cloud', works: true })
  out.push({
    id: 'drive',
    label: 'Google Drive',
    verb: 'sign in to Google…',
    mark: 'googledrive',
    works: GOOGLE_SIGN_IN_AVAILABLE && readClientId() !== null,
  })
  // The one provider with an account and no key (§24.9). Signed in by a code
  // the reader pastes, which needs no registered address, so the desktop build
  // keeps it where it loses Drive — but it still needs the site's app key, and
  // a site built without one has nothing behind the button either.
  if (DROPBOX_SIGN_IN_AVAILABLE) {
    out.push({
      id: 'dropbox',
      label: 'Dropbox',
      verb: 'sign in to Dropbox…',
      mark: 'dropbox',
      works: readAppKey() !== null,
    })
  }
  return out
}