1const NIGHTLY_SERVER_VERSION_PATTERN = /^[^-+]+-(?:nightly|preview)\.\d{8}\.\d+$/;
2
3export function formatAppDisplayName(input: {
4 readonly baseName: string;
5 readonly stageLabel: string;
6}): string {
7 if (input.stageLabel.trim().toLowerCase() === "latest") {
8 return input.baseName;
9 }
10
11 return `${input.baseName} (${input.stageLabel})`;
12}
13
14export function resolveHostedAppChannelLabel(channel: string | null | undefined): string | null {
15 const normalized = channel?.trim().toLowerCase();
16 return normalized === "nightly" ? "Nightly" : normalized === "latest" ? "Latest" : null;
17}
18
19/** Stage label of a web build. vite.config.ts stamps it into index.html's title too. */
20export function resolveWebAppStageLabel(input: {
21 readonly hostedAppChannel: string | null | undefined;
22 readonly development: boolean;
23}): string {
24 return (
25 resolveHostedAppChannelLabel(input.hostedAppChannel) ?? (input.development ? "Dev" : "Beta")
26 );
27}
28
29export function resolveServerBackedAppStageLabel(input: {
30 readonly primaryServerVersion: string | null | undefined;
31 readonly fallbackStageLabel: string;
32}): string {
33 return input.primaryServerVersion &&
34 NIGHTLY_SERVER_VERSION_PATTERN.test(input.primaryServerVersion)
35 ? "Nightly"
36 : input.fallbackStageLabel;
37}
38
39export function resolveServerBackedAppDisplayName(input: {
40 readonly baseName: string;
41 readonly fallbackDisplayName: string;
42 readonly fallbackStageLabel: string;
43 readonly primaryServerVersion: string | null | undefined;
44}): string {
45 const stageLabel = resolveServerBackedAppStageLabel({
46 primaryServerVersion: input.primaryServerVersion,
47 fallbackStageLabel: input.fallbackStageLabel,
48 });
49
50 return stageLabel === input.fallbackStageLabel
51 ? input.fallbackDisplayName
52 : formatAppDisplayName({ baseName: input.baseName, stageLabel });
53}
54
55/**
56 * Names the open thread: `project / thread` on desktop, where the OS already
57 * shows the app, with the app name appended in browser tabs.
58 */
59export function resolveWindowTitle(input: {
60 readonly appDisplayName: string;
61 readonly projectTitle: string | null;
62 readonly threadTitle: string | null;
63 /** The screen reader title: status, then thread, then project, since a screen reader reads from the start. */
64 readonly screenReader?: { readonly threadStatus: string | null };
65 readonly desktop: boolean;
66}): string {
67 const threadTitle = input.threadTitle?.trim() ?? "";
68 const status = input.screenReader?.threadStatus;
69 if (threadTitle === "") {
70 return status ? `${status}: ${input.appDisplayName}` : input.appDisplayName;
71 }
72
73 const projectTitle = input.projectTitle?.trim() ?? "";
74 const context =
75 projectTitle === ""
76 ? threadTitle
77 : input.screenReader
78 ? `${threadTitle} - ${projectTitle}`
79 : `${projectTitle} / ${threadTitle}`;
80 const title = status ? `${status}: ${context}` : context;
81 return input.desktop ? title : `${title} - ${input.appDisplayName}`;
82}