Skip to content
135 linesCodeBlameRaw
1// The desktop bridge: what the site finds at `window.g1tDesktop`, run at
2// the start of every page the window loads (bridge.rs puts it there). The
3// site's lib/notify-client.ts describes the contract from its side; when
4// the bridge is there, notifications go to the system, the badge to the
5// icon, links out to the browser, and g1t:// links arrive as `onNavigate`.
6// Each call is a command in bridge.rs, which only answers the server's
7// pages; on any other page the calls fail quietly.
8(function () {
9 if (window.g1tDesktop) return;
10 var VERSION = "__VERSION__";
11 var PLATFORM = "__PLATFORM__";
12 // The site's pages for reading about g1t, not using it, which open in
13 // the browser (navigation.rs); one ending in "/" is every page under it.
14 var OUTSIDE = "__OUTSIDE__";
15
16 function invoke(command, args) {
17 var core = window.__TAURI__ && window.__TAURI__.core;
18 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
19 var internals = window.__TAURI_INTERNALS__;
20 if (internals && typeof internals.invoke === "function") return internals.invoke(command, args || {});
21 return Promise.reject(new Error("g1t desktop: this page cannot reach the app"));
22 }
23
24 function quietly(promise) {
25 promise.catch(function (error) {
26 console.warn("g1t desktop:", error && error.message ? error.message : error);
27 });
28 }
29
30 var listeners = [];
31 var listening = false;
32 function listen() {
33 if (listening) return;
34 listening = true;
35 var events = window.__TAURI__ && window.__TAURI__.event;
36 if (!events || typeof events.listen !== "function") return;
37 events
38 .listen("g1t-navigate", function (event) {
39 var href = event && event.payload;
40 if (typeof href !== "string") return;
41 listeners.slice().forEach(function (listener) {
42 try {
43 listener(href);
44 } catch (error) {
45 console.warn("g1t desktop: a navigate listener failed", error);
46 }
47 });
48 })
49 .catch(function () {});
50 }
51
52 var bridge = {
53 version: VERSION,
54 platform: PLATFORM,
55 // A native notification, for a message to the person while the window is behind another.
56 notify: function (notification) {
57 quietly(invoke("notify", { title: String(notification.title || "g1t"), body: String(notification.body || "") }));
58 },
59 // What needs the person, on the icon and the tray; 0 clears it.
60 setBadge: function (count) {
61 quietly(invoke("set_badge", { count: Math.max(0, Math.floor(Number(count) || 0)) }));
62 },
63 // A link to somewhere else, in the browser the system uses.
64 openUrl: function (url) {
65 quietly(invoke("open_url", { url: String(url) }));
66 },
67 // g1t:// links, as site paths such as "/acme/-/chat/dm/chn_1".
68 onNavigate: function (listener) {
69 listeners.push(listener);
70 listen();
71 return function () {
72 var at = listeners.indexOf(listener);
73 if (at >= 0) listeners.splice(at, 1);
74 };
75 },
76 // Looks for an update now: "current", "ready:<version>", or rejects with why.
77 checkForUpdates: function () {
78 return invoke("check_for_updates");
79 },
80 // The app's own settings, in a window of their own.
81 openSettings: function () {
82 quietly(invoke("open_settings"));
83 },
84 };
85 Object.defineProperty(window, "g1tDesktop", { value: Object.freeze(bridge), writable: false, configurable: false });
86
87 // Links out. A link to another site, or to one of the site's pages for
88 // reading about g1t, opens in the system's browser, and the window stays
89 // where it is; a link that asks for a new tab, or a click
90 // with Ctrl or Cmd held, goes on in this window, which has no tabs.
91 function outside(url) {
92 var path = url.pathname.length > 1 ? url.pathname.replace(/\/+$/, "") : url.pathname;
93 return OUTSIDE.some(function (page) {
94 return page.charAt(page.length - 1) === "/" ? path.indexOf(page) === 0 : path === page;
95 });
96 }
97 function destination(href) {
98 try {
99 var url = new URL(href, location.href);
100 return /^https?:$/.test(url.protocol) ? url : null;
101 } catch (error) {
102 return null;
103 }
104 }
105 document.addEventListener(
106 "click",
107 function (event) {
108 if (event.defaultPrevented || event.button !== 0) return;
109 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
110 if (!anchor || anchor.hasAttribute("download")) return;
111 var url = destination(anchor.getAttribute("href"));
112 if (!url) return;
113 if (url.origin !== location.origin || outside(url)) {
114 event.preventDefault();
115 event.stopImmediatePropagation();
116 bridge.openUrl(url.href);
117 } else if (anchor.target === "_blank" || event.metaKey || event.ctrlKey) {
118 event.preventDefault();
119 event.stopImmediatePropagation();
120 location.assign(url.href);
121 }
122 },
123 true,
124 );
125 var open = window.open;
126 window.open = function (href, target, features) {
127 var url = destination(href);
128 if (url) {
129 if (url.origin === location.origin && !outside(url)) location.assign(url.href);
130 else bridge.openUrl(url.href);
131 return null;
132 }
133 return typeof open === "function" ? open.call(window, href, target, features) : null;
134 };
135})();