Skip to content

Commit d257ebc

Browse files
authored
Merge pull request #500 from ipfs-shipyard/feat/off-switch
Feature: ON / OFF Toggle This PR delivers an initial part of #491: - Moves status fields back to the space-header, resulting in two clear sections instead of three vague ones - Replaces node type toggle with a global On/Off switch that enables/disables gateway redirect and all active API integrations. - Perfect for users who don't want to run js-ipfs all the time to save battery, want to disable redirect or temporarily block access to API `window.ipfs` - Things that don't require access to API will still work in OFF state: linkify, copy IPFS Path, custom protocol handlers (they just redirect to public gw) - Moves switch for changing between _External_ (js-ipfs-api) and _Embedded_ (js-ipfs) to _Preferences_ screen - Hides unused options when embedded node is active (_Preferences_ screen) - Changes fonts and icons to ones from ipfs-css
2 parents 032ebdd + 19ba4ac commit d257ebc

28 files changed

+372
-360
lines changed

add-on/_locales/en/messages.json

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,16 @@
44
"description": "A pop-up title when user hovers on Browser Action button (browserAction_title)"
55
},
66
"panel_headerIpfsNodeIconLabel": {
7-
"message": "IPFS node",
7+
"message": "IPFS Companion",
88
"description": "Label for IPFS icon (panel_headerIpfsNodeIconLabel)"
99
},
10-
"panel_headerIpfsNodeEmbedded": {
11-
"message": "Embedded",
12-
"description": "Label for an embedded IPFS node (panel_headerIpfsNodeEmbedded)"
13-
},
14-
"panel_headerIpfsNodeEmbeddedTitle": {
15-
"message": "Experimental: Use IPFS embedded in your browser via js-ipfs",
16-
"description": "Label for an embedded IPFS node (panel_headerIpfsNodeEmbeddedTitle)"
10+
"panel_headerActiveToggleTitle": {
11+
"message": "Global toggle: activate or suspend all IPFS integrations",
12+
"description": "Label for an embedded IPFS node (panel_headerActiveToggleTitle)"
1713
},
18-
"panel_headerIpfsNodeExternal": {
19-
"message": "External",
20-
"description": "Label for an external IPFS node (panel_headerIpfsNodeExternal)"
21-
},
22-
"panel_headerIpfsNodeExternalTitle": {
23-
"message": "Connect to an IPFS node over HTTP",
24-
"description": "Label for an external IPFS node (panel_headerIpfsNodeExternalTitle)"
14+
"panel_statusOffline": {
15+
"message": "offline",
16+
"description": "A label in Node status section of Browser Action pop-up (panel_statusOffline)"
2517
},
2618
"panel_statusGatewayAddress": {
2719
"message": "Gateway",
@@ -52,7 +44,7 @@
5244
"description": "A menu item in Browser Action pop-up (panel_openWebui)"
5345
},
5446
"panel_openPreferences": {
55-
"message": "Open Preferences",
47+
"message": "Open Preferences of Browser Extension",
5648
"description": "A menu item in Browser Action pop-up (panel_openPreferences)"
5749
},
5850
"panel_switchToCustomGateway": {
@@ -189,8 +181,12 @@
189181
"message": "IPFS Node Type",
190182
"description": "An option title on the Preferences screen (option_ipfsNodeType_title)"
191183
},
192-
"option_ipfsNodeType_description": {
193-
"message": "External: Connect to an IPFS daemon over HTTP. \n\n Embedded: Run an IPFS node in your browser via ipfs-js *Experimental*",
184+
"option_ipfsNodeType_external_description": {
185+
"message": "External (suggested): connect to an IPFS daemon over HTTP.",
186+
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
187+
},
188+
"option_ipfsNodeType_embedded_description": {
189+
"message": "Embedded (experimental): run a js-ipfs node in your browser (YMMV, may drain your battery etc).",
194190
"description": "An option description on the Preferences screen (option_ipfsNodeType_description)"
195191
},
196192
"option_ipfsNodeConfig_title": {

add-on/src/lib/ipfs-companion.js

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ module.exports = async function init () {
4040
state = initState(options)
4141
notify = createNotifier(getState)
4242

43-
// It's ok for this to fail, node might be unavailable or mis-configured
44-
try {
45-
ipfs = await initIpfsClient(state)
46-
} catch (err) {
47-
console.error('[ipfs-companion] Failed to init IPFS client', err)
48-
notify(
49-
'notify_startIpfsNodeErrorTitle',
50-
err.name === 'ValidationError' ? err.details[0].message : err.message
51-
)
43+
if (state.active) {
44+
// It's ok for this to fail, node might be unavailable or mis-configured
45+
try {
46+
ipfs = await initIpfsClient(state)
47+
} catch (err) {
48+
console.error('[ipfs-companion] Failed to init IPFS client', err)
49+
notify(
50+
'notify_startIpfsNodeErrorTitle',
51+
err.name === 'ValidationError' ? err.details[0].message : err.message
52+
)
53+
}
5254
}
5355

5456
copier = createCopier(getState, notify)
@@ -60,7 +62,7 @@ module.exports = async function init () {
6062
onCopyAddressAtPublicGw: () => copier.copyAddressAtPublicGw()
6163
})
6264
modifyRequest = createRequestModifier(getState, dnsLink, ipfsPathValidator, runtime)
63-
ipfsProxy = createIpfsProxy(() => ipfs, getState)
65+
ipfsProxy = createIpfsProxy(getIpfs, getState)
6466
ipfsProxyContentScript = await registerIpfsProxyContentScript()
6567
registerListeners()
6668
await setApiStatusUpdateInterval(options.ipfsApiPollMs)
@@ -79,6 +81,12 @@ module.exports = async function init () {
7981
return state
8082
}
8183

84+
function getIpfs () {
85+
if (state.active && ipfs) return ipfs
86+
console.error('[ipfs-companion] Refused access to IPFS API client, check if extension is enabled')
87+
throw new Error('IPFS Companion: API client is disabled')
88+
}
89+
8290
function registerListeners () {
8391
browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ['<all_urls>']}, ['blocking', 'requestHeaders'])
8492
browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ['<all_urls>']}, ['blocking'])
@@ -107,8 +115,8 @@ module.exports = async function init () {
107115
if (previousHandle) {
108116
previousHandle.unregister()
109117
}
110-
if (!state.ipfsProxy || !browser.contentScripts) {
111-
// no-op if window.ipfs is disabled in Preferences
118+
if (!state.active || !state.ipfsProxy || !browser.contentScripts) {
119+
// no-op if global toggle is off, window.ipfs is disabled in Preferences
112120
// or if runtime has no contentScript API
113121
// (Chrome loads content script via manifest)
114122
return
@@ -200,6 +208,7 @@ module.exports = async function init () {
200208
async function sendStatusUpdateToBrowserAction () {
201209
if (!browserActionPort) return
202210
const info = {
211+
active: state.active,
203212
ipfsNodeType: state.ipfsNodeType,
204213
peerCount: state.peerCount,
205214
gwURLString: state.gwURLString,
@@ -353,6 +362,7 @@ module.exports = async function init () {
353362
}
354363

355364
async function onUpdatedTab (tabId, changeInfo, tab) {
365+
if (!state.active) return // skip content script injection when off
356366
if (changeInfo.status && changeInfo.status === 'complete' && tab.url && tab.url.startsWith('http')) {
357367
if (state.linkify) {
358368
console.info(`[ipfs-companion] Running linkfyDOM for ${tab.url}`)
@@ -549,6 +559,11 @@ module.exports = async function init () {
549559
// debug info
550560
// console.info(`Storage key "${key}" in namespace "${area}" changed. Old value was "${change.oldValue}", new value is "${change.newValue}".`)
551561
switch (key) {
562+
case 'active':
563+
state[key] = change.newValue
564+
ipfsProxyContentScript = await registerIpfsProxyContentScript()
565+
shouldRestartIpfsClient = true
566+
break
552567
case 'ipfsNodeType':
553568
case 'ipfsNodeConfig':
554569
shouldRestartIpfsClient = true
@@ -598,6 +613,8 @@ module.exports = async function init () {
598613
ipfs = null
599614
}
600615

616+
if (!state.active) return
617+
601618
try {
602619
ipfs = await initIpfsClient(state)
603620
} catch (err) {

add-on/src/lib/ipfs-request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function createRequestModifier (getState, dnsLink, ipfsPathValidator, runtime) {
4747
}
4848

4949
// handle redirects to custom gateway
50-
if (state.redirect) {
50+
if (state.active && state.redirect) {
5151
// Ignore preload requests
5252
if (request.method === 'HEAD') {
5353
return

add-on/src/lib/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use strict'
22

33
const optionDefaults = Object.freeze({
4-
ipfsNodeType: 'external',
4+
active: true, // global ON/OFF switch, overrides everything else
5+
ipfsNodeType: 'external', // or 'embedded'
56
ipfsNodeConfig: JSON.stringify({
67
config: {
78
Addresses: {

add-on/src/lib/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function initState (options) {
77
const state = {}
88
// we store the most used values in optimized form
99
// to minimize performance impact on overall browsing experience
10+
state.active = options.active
1011
state.peerCount = offlinePeerCount
1112
state.ipfsNodeType = options.ipfsNodeType
1213
state.ipfsNodeConfig = options.ipfsNodeConfig

add-on/src/options/forms/gateways-form.js

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const html = require('choo/html')
66
const { normalizeGatewayURL } = require('../../lib/options')
77

88
function gatewaysForm ({
9+
ipfsNodeType,
910
customGatewayUrl,
1011
useCustomGateway,
1112
publicGatewayUrl,
@@ -19,37 +20,41 @@ function gatewaysForm ({
1920
<form>
2021
<fieldset>
2122
<legend>${browser.i18n.getMessage('option_header_gateways')}</legend>
22-
<div>
23-
<label for="customGatewayUrl">
24-
<dl>
25-
<dt>${browser.i18n.getMessage('option_customGatewayUrl_title')}</dt>
26-
<dd>${browser.i18n.getMessage('option_customGatewayUrl_description')}</dd>
27-
</dl>
28-
</label>
29-
<input
30-
id="customGatewayUrl"
31-
type="url"
32-
inputmode="url"
33-
required
34-
pattern="^https?://[^/]+$"
35-
spellcheck="false"
36-
title="Enter URL without any sub-path"
37-
onchange=${onCustomGatewayUrlChange}
38-
value=${customGatewayUrl} />
39-
</div>
40-
<div>
41-
<label for="useCustomGateway">
42-
<dl>
43-
<dt>${browser.i18n.getMessage('option_useCustomGateway_title')}</dt>
44-
<dd>${browser.i18n.getMessage('option_useCustomGateway_description')}</dd>
45-
</dl>
46-
</label>
47-
<input
48-
id="useCustomGateway"
49-
type="checkbox"
50-
onchange=${onUseCustomGatewayChange}
51-
checked=${useCustomGateway} />
52-
</div>
23+
${ipfsNodeType === 'external' ? html`
24+
<div>
25+
<label for="customGatewayUrl">
26+
<dl>
27+
<dt>${browser.i18n.getMessage('option_customGatewayUrl_title')}</dt>
28+
<dd>${browser.i18n.getMessage('option_customGatewayUrl_description')}</dd>
29+
</dl>
30+
</label>
31+
<input
32+
id="customGatewayUrl"
33+
type="url"
34+
inputmode="url"
35+
required
36+
pattern="^https?://[^/]+$"
37+
spellcheck="false"
38+
title="Enter URL without any sub-path"
39+
onchange=${onCustomGatewayUrlChange}
40+
value=${customGatewayUrl} />
41+
</div>
42+
` : null}
43+
${ipfsNodeType === 'external' ? html`
44+
<div>
45+
<label for="useCustomGateway">
46+
<dl>
47+
<dt>${browser.i18n.getMessage('option_useCustomGateway_title')}</dt>
48+
<dd>${browser.i18n.getMessage('option_useCustomGateway_description')}</dd>
49+
</dl>
50+
</label>
51+
<input
52+
id="useCustomGateway"
53+
type="checkbox"
54+
onchange=${onUseCustomGatewayChange}
55+
checked=${useCustomGateway} />
56+
</div>
57+
` : null}
5358
<div>
5459
<label for="publicGatewayUrl">
5560
<dl>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
/* eslint-env browser, webextensions */
3+
4+
const browser = require('webextension-polyfill')
5+
const html = require('choo/html')
6+
7+
function globalToggleForm ({ active, onOptionChange }) {
8+
const toggle = onOptionChange('active')
9+
return html`
10+
<form class="dib mb3">
11+
<label for="active" class="dib pa3 pointer ${!active ? 'bg-aqua-muted br2' : ''}">
12+
<input class="mr2 pointer" id="active" type="checkbox" onchange=${toggle} checked=${active} />
13+
${browser.i18n.getMessage('panel_headerActiveToggleTitle')}
14+
</label>
15+
</form>
16+
`
17+
}
18+
19+
module.exports = globalToggleForm

add-on/src/options/forms/ipfs-node-form.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
1717
<dl>
1818
<dt>${browser.i18n.getMessage('option_ipfsNodeType_title')}</dt>
1919
<dd>
20-
${browser.i18n.getMessage('option_ipfsNodeType_description')}
20+
<p>${browser.i18n.getMessage('option_ipfsNodeType_external_description')}</p>
21+
<p>${browser.i18n.getMessage('option_ipfsNodeType_embedded_description')}</p>
2122
<p><a href="https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/node-types.md" target="_blank">
2223
${browser.i18n.getMessage('option_legend_readMore')}
2324
</a></p>

add-on/src/options/options.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
html.is-chrome {
1+
@import url('/ui-kit/tachyons.css');
2+
@import url('/ui-kit/ipfs.css');
3+
html {
24
overflow: hidden;
35
}
46
.is-chrome body {

add-on/src/options/page.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-env browser, webextensions */
33

44
const html = require('choo/html')
5+
const globalToggleForm = require('./forms/global-toggle-form')
56
const ipfsNodeForm = require('./forms/ipfs-node-form')
67
const gatewaysForm = require('./forms/gateways-form')
78
const apiForm = require('./forms/api-form')
@@ -28,25 +29,42 @@ module.exports = function optionsPage (state, emit) {
2829
emit('optionsReset')
2930
}
3031

32+
if (!state.options.active) {
33+
// we don't want to confuse users by showing "active" checkboxes
34+
// when global toggle is in "suspended" state
35+
return html`
36+
<div class="sans-serif">
37+
${globalToggleForm({
38+
active: state.options.active,
39+
onOptionChange
40+
})}
41+
</div>
42+
`
43+
}
3144
return html`
32-
<div>
45+
<div class="sans-serif">
46+
${globalToggleForm({
47+
active: state.options.active,
48+
onOptionChange
49+
})}
3350
${ipfsNodeForm({
3451
ipfsNodeType: state.options.ipfsNodeType,
3552
ipfsNodeConfig: state.options.ipfsNodeConfig,
3653
onOptionChange
3754
})}
3855
${gatewaysForm({
56+
ipfsNodeType: state.options.ipfsNodeType,
3957
customGatewayUrl: state.options.customGatewayUrl,
4058
useCustomGateway: state.options.useCustomGateway,
4159
publicGatewayUrl: state.options.publicGatewayUrl,
4260
onOptionChange
4361
})}
44-
${apiForm({
62+
${state.options.ipfsNodeType === 'external' ? apiForm({
4563
ipfsApiUrl: state.options.ipfsApiUrl,
4664
ipfsApiPollMs: state.options.ipfsApiPollMs,
4765
automaticMode: state.options.automaticMode,
4866
onOptionChange
49-
})}
67+
}) : null}
5068
${experimentsForm({
5169
displayNotifications: state.options.displayNotifications,
5270
preloadAtPublicGateway: state.options.preloadAtPublicGateway,

add-on/src/popup/browser-action/browser-action.css

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
@import url('../../../ui-kit/tachyons.css');
1+
@import url('/ui-kit/tachyons.css');
2+
@import url('/ui-kit/ipfs.css');
23
@import url('../heartbeat.css');
3-
@import url('mdc.switch.css');
44

55
.bg-near-white--hover:hover {
66
background-color: #F4F4F4;
@@ -10,6 +10,20 @@
1010
outline: 0;
1111
}
1212

13+
.no-user-select {
14+
-webkit-user-select: none; /* Old Chrome, Safari */
15+
-moz-user-select: none; /* Firefox */
16+
-ms-user-select: none; /* Internet Explorer/Edge */
17+
user-select: none; /* Non-prefixed version, Chrome and Opera */
18+
}
19+
20+
.force-select-all {
21+
-webkit-user-select: all; /* Chrome 49+ */
22+
-moz-user-select: all; /* Firefox 43+ */
23+
-ms-user-select: all; /* No support yet */
24+
user-select: all; /* Likely future */
25+
}
26+
1327
.fade-in {
1428
animation: fade-in 600ms;
1529
}

0 commit comments

Comments
 (0)