Files: 06ded70b47c2b7b6dbd436a59f532f6caff0e67c / lib / context-menu.js
7121 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 | } |
102 | } |
103 | } |
104 | |
105 | function copyMsgText(window) { |
106 | const msgKey = lastClickInfo?.msg?.key |
107 | return { |
108 | label: "Copy Message Text", |
109 | visible: !!msgKey, |
110 | click: function () { |
111 | window.webContents.send('copy-message-text', msgKey) |
112 | }, |
113 | }; |
114 | } |
115 | |
116 | function openOnExternalViewer(config) { |
117 | const msgKey = lastClickInfo?.msg?.key |
118 | return { |
119 | label: 'Open In Online Viewer', |
120 | visible: !!msgKey, |
121 | click: function () { |
122 | const key = msgKey |
123 | const gateway = config.gateway || |
124 | 'https://viewer.scuttlebot.io' |
125 | const url = `${gateway}/${encodeURIComponent(key)}` |
126 | shell.openExternal(url); |
127 | } |
128 | } |
129 | } |
130 | |
131 | function copyExternalLink(config) { |
132 | const msgKey = lastClickInfo?.msg?.key |
133 | return { |
134 | label: 'Copy External Link', |
135 | visible: !!msgKey, |
136 | click: function () { |
137 | const key = msgKey |
138 | const gateway = config.gateway || |
139 | 'https://viewer.scuttlebot.io' |
140 | const url = `${gateway}/${encodeURIComponent(key)}` |
141 | clipboard.writeText(url) |
142 | } |
143 | } |
144 | } |
145 | |
146 | function findRefs(parameters, navigate) { |
147 | const extractedRef = |
148 | parameters.mediaType === "none" |
149 | ? ref.extract(parameters.linkURL) |
150 | : ref.extract(parameters.srcURL); |
151 | const usageOrRef = extractedRef && parameters.mediaType === "none" |
152 | ? 'References To' |
153 | : 'Usages Of' |
154 | const label = !!extractedRef |
155 | ? `Find ${usageOrRef} ${extractedRef.slice(0, 10).replaceAll("&", "&&&")}...` |
156 | : ""; |
157 | return { |
158 | label, |
159 | visible: !!extractedRef, |
160 | click: () => { |
161 | navigate(`?${extractedRef}`); |
162 | }, |
163 | }; |
164 | } |
165 | |
166 | function copyRef(parameters) { |
167 | const extractedRef = |
168 | parameters.mediaType === "none" |
169 | ? ref.extract(parameters.linkURL) |
170 | : ref.extract(parameters.srcURL); |
171 | const label = !!extractedRef |
172 | ? `Copy Reference ${extractedRef.slice(0, 10).replaceAll("&", "&&&")}...` |
173 | : ""; |
174 | return { |
175 | label, |
176 | visible: !!extractedRef, |
177 | click: () => { |
178 | clipboard.writeText(extractedRef); |
179 | }, |
180 | }; |
181 | } |
182 | |
183 | function copyEmail(parameters) { |
184 | return { |
185 | label: "Copy Email Address", |
186 | // FIXME: this fails for "mailto:" links that actually are hand-coded in markdown |
187 | // example: Mail me at my [work address](mailto:daan@business.corp) |
188 | visible: parameters.linkURL.startsWith("mailto:"), |
189 | click: () => { |
190 | // Omit the mailto: portion of the link; we just want the address |
191 | clipboard.writeText(parameters.linkText); |
192 | }, |
193 | }; |
194 | } |
195 | |
196 | function copyEmbedMd(parameters) { |
197 | return { |
198 | label: "Copy Embed Markdown", |
199 | visible: parameters.mediaType !== "none", |
200 | click: () => { |
201 | const extractedRef = ref.extract(parameters.srcURL); |
202 | clipboard.writeText(`![${parameters.titleText}](${extractedRef})`); |
203 | }, |
204 | }; |
205 | } |
206 | |
207 | function openMediaInBrowser(parameters) { |
208 | return { |
209 | label: "Open With Browser", |
210 | visible: parameters.mediaType !== "none", |
211 | click: () => { |
212 | shell.openExternal(parameters.srcURL); |
213 | }, |
214 | }; |
215 | } |
216 | |
217 | function searchwithDDG(parameters) { |
218 | return { |
219 | label: "Search With DuckDuckGo", |
220 | // Only show it when right-clicking text |
221 | visible: parameters.selectionText.trim().length > 0, |
222 | click: () => { |
223 | const url = `https://duckduckgo.com/?q=${encodeURIComponent( |
224 | parameters.selectionText |
225 | )}`; |
226 | shell.openExternal(url); |
227 | }, |
228 | }; |
229 | } |
230 | |
231 | function reloadWindow() { |
232 | return { |
233 | label: "Reload", |
234 | click: function (item, focusedWindow) { |
235 | if (focusedWindow) { |
236 | focusedWindow.reload(); |
237 | } |
238 | }, |
239 | }; |
240 | } |
241 | |
242 | function openServerDevTools(serverDevToolsCallback) { |
243 | return { |
244 | label: "Inspect Server Process", |
245 | click: serverDevToolsCallback, |
246 | }; |
247 | } |
Built with git-ssb-web