Files: 4087ed91f7e663172d051c9afe142fa7b7669f1e / lib / context-menu.js
7155 bytesRaw
1 | const { shell, clipboard } = require('electron') |
2 | const { BrowserWindow, ContextMenuParams, ipcMain, MenuItemConstructorOptions, WebContents } = require('electron') |
3 | const contextMenu = require('electron-context-menu') |
4 | const ref = require('ssb-ref') |
5 | |
6 | // used to receive out-of-band information about context-menu events |
7 | // see below, Ctrl-F "context-menu-info" |
8 | var lastClickInfo; |
9 | |
10 | module.exports = function ( |
11 | config, |
12 | serverDevToolsCallback, |
13 | navigateHandler, |
14 | window |
15 | ) { |
16 | ipcMain.handle("context-menu-info", (event, info) => { |
17 | lastClickInfo = info; |
18 | return true; |
19 | }); |
20 | contextMenu({ |
21 | window, |
22 | menu: (defaultActions, parameters, _, dictionarySuggestions) => { |
23 | // elementAtPosition(window, parameters.x, parameters.y) |
24 | const isFileProtocol = parameters.linkURL.startsWith("file:"); |
25 | |
26 | // This is very similar to the boilerplate from electron-context-menu even |
27 | // though we don't use all the options. Some of the options are disabled |
28 | // via "condition && " guards just to clarify where we differ from the |
29 | // boilerplate. |
30 | // See the original menu structure here: |
31 | // https://github.com/sindresorhus/electron-context-menu/blob/621c29a8a133925ac25529e4bea2a738394e8609/index.js#L230 |
32 | // We could probably get away with heavy, heavy modification instead of |
33 | // menu but this seems more understandable, all things considered |
34 | let menuTemplate = [ |
35 | dictionarySuggestions.length > 0 && defaultActions.separator(), |
36 | ...dictionarySuggestions, |
37 | defaultActions.separator(), |
38 | |
39 | defaultActions.learnSpelling(), |
40 | defaultActions.separator(), |
41 | |
42 | defaultActions.lookUpSelection(), |
43 | defaultActions.separator(), |
44 | |
45 | searchwithDDG(parameters), // instead of defaultActions.searchWithGoogle() |
46 | defaultActions.separator(), |
47 | |
48 | defaultActions.cut(), |
49 | defaultActions.copy(), |
50 | defaultActions.paste(), |
51 | defaultActions.separator(), |
52 | |
53 | // We typically don't want to copy links, only external ones |
54 | copyEmbedMd(parameters), |
55 | copyMsgText(window), |
56 | // this and the next one might return the same id. Bit redundant but |
57 | // only if we right-click on the message timestamp or such |
58 | copyMsgKey(), |
59 | copyRef(parameters), |
60 | copyEmail(parameters), |
61 | // We could make our own copyLink() instead which sets |
62 | // visible: !isFileProtocol but this is easier |
63 | !isFileProtocol && defaultActions.copyLink(), |
64 | copyExternalLink(config), |
65 | openOnExternalViewer(config), |
66 | findRefs(parameters, navigateHandler), |
67 | defaultActions.separator(), |
68 | |
69 | openMediaInBrowser(parameters), |
70 | defaultActions.saveImage(), |
71 | defaultActions.saveImageAs(), |
72 | defaultActions.copyImage(), |
73 | defaultActions.copyImageAddress(), |
74 | defaultActions.separator(), |
75 | |
76 | // this could trigger a web request from within patchwork and we don't want that |
77 | false && defaultActions.saveLinkAs(), |
78 | defaultActions.separator(), |
79 | |
80 | defaultActions.inspect(), |
81 | openServerDevTools(serverDevToolsCallback), |
82 | defaultActions.services(), |
83 | defaultActions.separator(), |
84 | |
85 | reloadWindow(), |
86 | ]; |
87 | |
88 | return menuTemplate; |
89 | }, |
90 | }); |
91 | }; |
92 | |
93 | // Every function below here will produce one MenuItemConstructorOptions object |
94 | |
95 | function copyMsgKey() { |
96 | const msgKey = lastClickInfo?.msg?.key |
97 | return { |
98 | label: "Copy Message Reference", |
99 | visible: !!msgKey, |
100 | click: function () { |
101 | clipboard.writeText(msgKey) |
102 | } |
103 | } |
104 | } |
105 | |
106 | function copyMsgText(window) { |
107 | const msgKey = lastClickInfo?.msg?.key |
108 | return { |
109 | label: "Copy Message Text", |
110 | visible: !!msgKey, |
111 | click: function () { |
112 | window.webContents.send('copy-message-text', msgKey) |
113 | }, |
114 | }; |
115 | } |
116 | |
117 | function openOnExternalViewer(config) { |
118 | const msgKey = lastClickInfo?.msg?.key |
119 | return { |
120 | label: 'Open In Online Viewer', |
121 | visible: !!msgKey, |
122 | click: function () { |
123 | const key = msgKey |
124 | const gateway = config.gateway || |
125 | 'https://viewer.scuttlebot.io' |
126 | const url = `${gateway}/${encodeURIComponent(key)}` |
127 | shell.openExternal(url); |
128 | } |
129 | } |
130 | } |
131 | |
132 | function copyExternalLink(config) { |
133 | const msgKey = lastClickInfo?.msg?.key |
134 | return { |
135 | label: 'Copy External Link', |
136 | visible: !!msgKey, |
137 | click: function () { |
138 | const key = msgKey |
139 | const gateway = config.gateway || |
140 | 'https://viewer.scuttlebot.io' |
141 | const url = `${gateway}/${encodeURIComponent(key)}` |
142 | clipboard.writeText(url) |
143 | } |
144 | } |
145 | } |
146 | |
147 | function findRefs(parameters, navigate) { |
148 | const extractedRef = |
149 | parameters.mediaType === "none" |
150 | ? ref.extract(parameters.linkURL) |
151 | : ref.extract(parameters.srcURL); |
152 | const usageOrRef = extractedRef && parameters.mediaType === "none" |
153 | ? 'References To' |
154 | : 'Usages Of' |
155 | const label = !!extractedRef |
156 | ? `Find ${usageOrRef} ${extractedRef.slice(0, 10).replaceAll("&", "&&&")}...` |
157 | : ""; |
158 | return { |
159 | label, |
160 | visible: !!extractedRef, |
161 | click: () => { |
162 | navigate(`?${extractedRef}`); |
163 | }, |
164 | }; |
165 | } |
166 | |
167 | function copyRef(parameters) { |
168 | const extractedRef = |
169 | parameters.mediaType === "none" |
170 | ? ref.extract(parameters.linkURL) |
171 | : ref.extract(parameters.srcURL); |
172 | const label = !!extractedRef |
173 | ? `Copy Reference ${extractedRef.slice(0, 10).replaceAll("&", "&&&")}...` |
174 | : ""; |
175 | return { |
176 | label, |
177 | visible: !!extractedRef, |
178 | click: () => { |
179 | clipboard.writeText(extractedRef); |
180 | }, |
181 | }; |
182 | } |
183 | |
184 | function copyEmail(parameters) { |
185 | return { |
186 | label: "Copy Email Address", |
187 | // FIXME: this fails for "mailto:" links that actually are hand-coded in markdown |
188 | // example: Mail me at my [work address](mailto:daan@business.corp) |
189 | visible: parameters.linkURL.startsWith("mailto:"), |
190 | click: () => { |
191 | // Omit the mailto: portion of the link; we just want the address |
192 | clipboard.writeText(parameters.linkText); |
193 | }, |
194 | }; |
195 | } |
196 | |
197 | function copyEmbedMd(parameters) { |
198 | return { |
199 | label: "Copy Embed Markdown", |
200 | visible: parameters.mediaType !== "none", |
201 | click: () => { |
202 | const extractedRef = ref.extract(parameters.srcURL); |
203 | clipboard.writeText(`![${parameters.titleText}](${extractedRef})`); |
204 | }, |
205 | }; |
206 | } |
207 | |
208 | function openMediaInBrowser(parameters) { |
209 | return { |
210 | label: "Open With Browser", |
211 | visible: parameters.mediaType !== "none", |
212 | click: () => { |
213 | shell.openExternal(parameters.srcURL); |
214 | }, |
215 | }; |
216 | } |
217 | |
218 | function searchwithDDG(parameters) { |
219 | return { |
220 | label: "Search With DuckDuckGo", |
221 | // Only show it when right-clicking text |
222 | visible: parameters.selectionText.trim().length > 0, |
223 | click: () => { |
224 | const url = `https://duckduckgo.com/?q=${encodeURIComponent( |
225 | parameters.selectionText |
226 | )}`; |
227 | shell.openExternal(url); |
228 | }, |
229 | }; |
230 | } |
231 | |
232 | function reloadWindow() { |
233 | return { |
234 | label: "Reload", |
235 | click: function (item, focusedWindow) { |
236 | if (focusedWindow) { |
237 | focusedWindow.reload(); |
238 | } |
239 | }, |
240 | }; |
241 | } |
242 | |
243 | function openServerDevTools(serverDevToolsCallback) { |
244 | return { |
245 | label: "Inspect Server Process", |
246 | click: serverDevToolsCallback, |
247 | }; |
248 | } |
Built with git-ssb-web