Skip to content
Source
Sign inSign up
Open History to see the last commit

externalLinkContextMenu.ts

124 lines4.1 KB

1import type { ContextMenuItem } from "@t3tools/contracts";
2​
3type ExternalLinkContextMenuAction =
4 | "open-in-preview"
5 | "open-external"
6 | "copy-link"
7 | "link-to-thread"
8 | "unlink-from-thread";
9​
10type ExternalLinkContextMenuFailureOperation =
11 | "show-link-context-menu"
12 | "open-link-in-preview"
13 | "open-link-external"
14 | "copy-link"
15 | "link-pull-request-to-thread"
16 | "unlink-pull-request-from-thread";
17​
18const FAILURE_OPERATION_BY_ACTION = {
19 "open-in-preview": "open-link-in-preview",
20 "open-external": "open-link-external",
21 "copy-link": "copy-link",
22 "link-to-thread": "link-pull-request-to-thread",
23 "unlink-from-thread": "unlink-pull-request-from-thread",
24} as const satisfies Record<ExternalLinkContextMenuAction, ExternalLinkContextMenuFailureOperation>;
25​
26const EXTERNAL_LINK_CONTEXT_MENU_ITEMS = [
27 { id: "open-in-preview", label: "Open in integrated browser" },
28 { id: "open-external", label: "Open in system browser" },
29 { id: "copy-link", label: "Copy Link" },
30] as const satisfies readonly ContextMenuItem<ExternalLinkContextMenuAction>[];
31​
32function externalLinkContextMenuItems(options: {
33 readonly canOpenInPreview: boolean;
34 readonly threadLinkAction?: "link-to-thread" | "unlink-from-thread" | undefined;
35}): readonly ContextMenuItem<ExternalLinkContextMenuAction>[] {
36 const items = options.canOpenInPreview
37 ? EXTERNAL_LINK_CONTEXT_MENU_ITEMS
38 : EXTERNAL_LINK_CONTEXT_MENU_ITEMS.filter((item) => item.id !== "open-in-preview");
39 if (options.threadLinkAction === undefined) return items;
40 return [
41 {
42 id: options.threadLinkAction,
43 label:
44 options.threadLinkAction === "link-to-thread" ? "Link to thread" : "Unlink from thread",
45 },
46 ...items,
47 ];
49​
50interface ShowExternalLinkContextMenuOptions {
51 readonly href: string;
52 readonly position: { readonly x: number; readonly y: number };
53 readonly canOpenInPreview?: boolean;
54 readonly threadLinkAction?: "link-to-thread" | "unlink-from-thread" | undefined;
55 readonly showContextMenu: (
56 items: readonly ContextMenuItem<ExternalLinkContextMenuAction>[],
57 position: { readonly x: number; readonly y: number },
58 ) => Promise<ExternalLinkContextMenuAction | null>;
59 readonly openInPreview: (href: string) => Promise<void>;
60 readonly openExternal: (href: string) => Promise<void>;
61 readonly copyLink: (href: string) => Promise<unknown>;
62 readonly updateThreadLink?: (href: string, linked: boolean) => Promise<void>;
63 readonly reportFailure: (
64 operation: ExternalLinkContextMenuFailureOperation,
65 cause: unknown,
66 ) => void;
68​
69function resolveExternalWebLink(href: string | null | undefined): URL | null {
70 if (!href) return null;
71 try {
72 const url = new URL(href.startsWith("//") ? `https:${href}` : href);
73 if (url.protocol !== "http:" && url.protocol !== "https:") return null;
74 return url;
75 } catch {
76 return null;
77 }
79​
80export function resolveExternalWebLinkHref(href: string | null | undefined): string | null {
81 return resolveExternalWebLink(href)?.href ?? null;
83​
84export function resolveExternalWebLinkHost(href: string | undefined): string | null {
85 return resolveExternalWebLink(href)?.hostname || null;
87​
88export async function showExternalLinkContextMenu({
89 href,
90 position,
91 canOpenInPreview = true,
92 threadLinkAction,
93 showContextMenu,
94 openInPreview,
95 openExternal,
96 copyLink,
97 updateThreadLink,
98 reportFailure,
99}: ShowExternalLinkContextMenuOptions): Promise<void> {
100 let action: ExternalLinkContextMenuAction | null;
101 try {
102 action = await showContextMenu(
103 externalLinkContextMenuItems({ canOpenInPreview, threadLinkAction }),
104 position,
105 );
106 } catch (cause) {
107 reportFailure("show-link-context-menu", cause);
108 return;
109 }
110​
111 try {
112 if (action === "open-in-preview") {
113 await openInPreview(href);
114 } else if (action === "open-external") {
115 await openExternal(href);
116 } else if (action === "copy-link") {
117 await copyLink(href);
118 } else if (action === "link-to-thread" || action === "unlink-from-thread") {
119 await updateThreadLink?.(href, action === "link-to-thread");
120 }
121 } catch (cause) {
122 if (action) reportFailure(FAILURE_OPERATION_BY_ACTION[action], cause);
123 }