Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/ext/popup.js

5.0 KiB, 1 run

created by r2519314175:887, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// The user's own window onto the extension: what mode it is in, what page it is
2// holding, and which sites it may touch. Every grant here can be withdrawn here.
3
4'use strict';
5
6(() => {
7
8 const $ = (id) => document.getElementById(id);
9 const I = globalThis.DaimondExtI18n;
10 const t = (...a) => I.t(...a);
11
12 /// A match pattern, said the way a person would say it.
13 function plain(pat) {
14 if (pat === '<all_urls>') return t('popup_any_tab');
15 // Not an origin, and deliberately not spelled like one: what was granted
16 // is the machine. But it was granted to ONE page, and the page is named,
17 // because a user looking at this list is auditing what they gave away and
18 // "commands on this computer" alone does not say to whom.
19 if (pat === 'machine-hand') return t('popup_machine_hand');
20 if (pat.indexOf('machine-hand:') === 0) return t('popup_machine_hand_origin', pat.slice('machine-hand:'.length));
21 const m = /^\*:\/\/\*\.([^/]+)\/\*$/.exec(pat);
22 if (m) return t('popup_subdomains', m[1]);
23 const n = /^\*:\/\/([^/]+)\/\*$/.exec(pat);
24 return n ? n[1] : pat;
25 }
26
27 /// A line of state: a sentence, and the small print under it.
28 function says(el, head, fine) {
29 el.textContent = '';
30 const h = document.createElement('div');
31 const f = document.createElement('small');
32 h.textContent = head;
33 f.textContent = fine;
34 el.append(h, f);
35 }
36
37 /// Paints the current state.
38 function paint(s) {
39 $('ver').textContent = `v${s.version}`;
40
41 // A question is waiting. Its own window is in front, but a window can be
42 // covered, minimised or lost behind the app -- and then this icon is the
43 // only place left that knows. Say what is being asked and offer the way
44 // back to it. This RAISES that window; it never opens a second one.
45 const ask = $('ask');
46 if (s.pending) {
47 ask.hidden = false;
48 if (s.pending.kind === 'mirror') {
49 says(ask, t('popup_ask_mirror'), t('popup_ask_mirror_fine'));
50 } else if (s.pending.kind === 'hand') {
51 says(ask, t('popup_ask_hand'), t('popup_ask_hand_fine'));
52 } else {
53 says(ask, t('popup_ask_site', s.pending.host), t('popup_ask_site_fine', s.pending.host));
54 }
55 $('raise').hidden = false;
56 } else {
57 ask.hidden = true;
58 $('raise').hidden = true;
59 }
60
61 const mode = $('mode');
62 mode.className = 'mode ' + s.mode;
63 if (s.mode === 'user') {
64 says(mode, t('popup_mode_user'), t('popup_mode_user_fine'));
65 $('take').hidden = false;
66 } else if (s.mode === 'agent') {
67 says(mode, t('popup_mode_agent'), t('popup_mode_agent_fine'));
68 $('take').hidden = true;
69 } else {
70 says(mode, t('popup_mode_idle'), t('popup_mode_idle_fine'));
71 $('take').hidden = true;
72 }
73
74 $('url').textContent = s.url || '';
75 // One question at a time. Offering the live view -- the broadest permission
76 // Chrome has -- beside a site question the user has not answered is how a
77 // clear flow turns back into three surprises.
78 $('mirror').hidden = (s.granted || []).includes('<all_urls>') || !!s.pending;
79
80 const ul = $('granted');
81 ul.textContent = '';
82 if (!s.granted || !s.granted.length) {
83 const li = document.createElement('li');
84 li.className = 'none';
85 li.textContent = t('popup_none');
86 ul.append(li);
87 return;
88 }
89 for (const pat of s.granted) {
90 const li = document.createElement('li');
91 const span = document.createElement('span');
92 const btn = document.createElement('button');
93 // The pattern is what was granted; the plain form is what it MEANS.
94 // A user auditing what they have given away should not have to read
95 // a match pattern to find out that it covers subdomains.
96 span.textContent = plain(pat);
97 span.title = pat;
98 btn.textContent = t('popup_revoke');
99 btn.addEventListener('click', async () => {
100 const res = await chrome.runtime.sendMessage({ type: 'revoke', pattern: pat });
101 if (res && res.ok) refresh();
102 });
103 li.append(span, btn);
104 ul.append(li);
105 }
106 }
107
108 async function refresh() {
109 const s = await chrome.runtime.sendMessage({ type: 'panel' });
110 if (s && s.ok) paint(s);
111 }
112
113 // Bring the waiting question back to the front. It is the same window, raised
114 // -- opening another would be the popup flood the live-view guard exists to
115 // stop.
116 $('raise').addEventListener('click', async () => {
117 const r = await chrome.runtime.sendMessage({ type: 'raise' });
118 if (r && r.ok) window.close();
119 else refresh();
120 });
121
122 $('take').addEventListener('click', async () => {
123 await chrome.runtime.sendMessage({ type: 'takeover' });
124 refresh();
125 });
126
127 // A click in the popup is a user gesture, which is what Chrome insists on
128 // before it will widen a permission. So the request is made from here.
129 $('mirror').addEventListener('click', async () => {
130 try {
131 await chrome.permissions.request({ origins: ['<all_urls>'] });
132 } catch (e) {
133 // The user said no. Nothing changes.
134 }
135 refresh();
136 });
137
138 // The marked nodes are painted the moment the table is in, and the state is
139 // asked for after -- so the window never draws a translated question around
140 // English furniture.
141 I.ready().then(() => { I.paint(); refresh(); });
142
143})();