diff --git a/add-on/manifest.json b/add-on/manifest.json index 4f9be7a60..78383c332 100644 --- a/add-on/manifest.json +++ b/add-on/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "IPFS Companion (Development Channel)", "short_name": "IPFS Companion", - "version" : "2.0.6", + "version" : "2.0.7", "description": "Access IPFS resources via custom HTTP2IPFS gateway", "homepage_url": "https://github.com/ipfs/ipfs-companion", diff --git a/add-on/src/lib/common.js b/add-on/src/lib/common.js index d7d6b74cd..8569ba25f 100644 --- a/add-on/src/lib/common.js +++ b/add-on/src/lib/common.js @@ -14,8 +14,8 @@ async function init () { const options = await browser.storage.local.get(optionDefaults) ipfs = initIpfsApi(options.ipfsApiUrl) initStates(options) - restartAlarms(options) registerListeners() + setApiStatusUpdateInterval(options.ipfsApiPollMs) await storeMissingOptions(options, optionDefaults) } catch (error) { console.error('Unable to initialize addon due to error', error) @@ -38,13 +38,6 @@ async function initStates (options) { state.linkify = options.linkify state.dnslink = options.dnslink state.dnslinkCache = /* global LRUMap */ new LRUMap(1000) - await getSwarmPeerCount() - .then(updatePeerCountState) - .then(updateBrowserActionBadge) -} - -function updatePeerCountState (count) { - state.peerCount = count } function registerListeners () { @@ -54,21 +47,6 @@ function registerListeners () { browser.tabs.onUpdated.addListener(onUpdatedTab) } -/* -function smokeTestLibs () { - // is-ipfs - console.info('is-ipfs library test (should be true) --> ' + window.IsIpfs.multihash('QmUqRvxzQyYWNY6cD1Hf168fXeqDTQWwZpyXjU5RUExciZ')) - // ipfs-api: execute test request :-) - ipfs.id() - .then(id => { - console.info('ipfs-api .id() test --> Node ID is: ', id) - }) - .catch(err => { - console.info('ipfs-api .id() test --> Failed to read Node info: ', err) - }) -} -*/ - // REDIRECT // =================================================================== @@ -221,48 +199,52 @@ function readDnslinkTxtRecordFromApi (fqdn) { }) } -// ALARMS +// PORTS // =================================================================== +// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect +// Make a connection between different contexts inside the add-on, +// e.g. signalling between browser action popup and background page that works +// in everywhere, even in private contexts (https://github.com/ipfs/ipfs-companion/issues/243) + +const browserActionPortName = 'browser-action-port' +var browserActionPort + +browser.runtime.onConnect.addListener(port => { + // console.log('onConnect', port) + if (port.name === browserActionPortName) { + browserActionPort = port + browserActionPort.onMessage.addListener(handleMessageFromBrowserAction) + browserActionPort.onDisconnect.addListener(() => { browserActionPort = null }) + sendStatusUpdateToBrowserAction() + } +}) -const ipfsApiStatusUpdateAlarm = 'ipfs-api-status-update' -const ipfsRedirectUpdateAlarm = 'ipfs-redirect-update' - -function handleAlarm (alarm) { - // avoid making expensive updates when IDLE - if (alarm.name === ipfsApiStatusUpdateAlarm) { - getSwarmPeerCount() - .then(updatePeerCountState) - .then(updateAutomaticModeRedirectState) - .then(updateBrowserActionBadge) - .then(updateContextMenus) +function handleMessageFromBrowserAction (message) { + // console.log('In background script, received message from browser action', message) + if (message.event === 'notification') { + notify(message.title, message.message) } } -/* -const idleInSecs = 60 -function runIfNotIdle (action) { - browser.idle.queryState(idleInSecs) - .then(state => { - if (state === 'active') { - return action() +async function sendStatusUpdateToBrowserAction () { + if (browserActionPort) { + const info = { + peerCount: state.peerCount, + gwURLString: state.gwURLString, + currentTab: await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) + } + try { + let v = await ipfs.version() + if (v) { + info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version } - }) - .catch(error => { - console.error(`Unable to read idle state due to ${error}`) - }) -} -*/ - -// API HELPERS -// =================================================================== - -async function getSwarmPeerCount () { - try { - const peerInfos = await ipfs.swarm.peers() - return peerInfos.length - } catch (error) { - // console.error(`Error while ipfs.swarm.peers: ${err}`) - return -1 + } catch (error) { + info.gatewayVersion = null + } + if (info.currentTab) { + info.ipfsPageActionsContext = isIpfsPageActionsContext(info.currentTab.url) + } + browserActionPort.postMessage({statusUpdate: info}) } } @@ -380,23 +362,60 @@ async function onUpdatedTab (tabId, changeInfo, tab) { } } -// browserAction +// API STATUS UPDATES // ------------------------------------------------------------------- +// API is polled for peer count every ipfsApiPollMs + +const offlinePeerCount = -1 +const idleInSecs = 5 * 60 -async function restartAlarms (options) { - await browser.alarms.clearAll() - if (!browser.alarms.onAlarm.hasListener(handleAlarm)) { - browser.alarms.onAlarm.addListener(handleAlarm) +var apiStatusUpdateInterval + +function setApiStatusUpdateInterval (ipfsApiPollMs) { + if (apiStatusUpdateInterval) { + clearInterval(apiStatusUpdateInterval) } - await createIpfsApiStatusUpdateAlarm(options.ipfsApiPollMs) + apiStatusUpdateInterval = setInterval(() => runIfNotIdle(apiStatusUpdate), ipfsApiPollMs) + apiStatusUpdate() +} + +async function apiStatusUpdate () { + state.peerCount = await getSwarmPeerCount() + updatePeerCountDependentStates() + sendStatusUpdateToBrowserAction() } -function createIpfsApiStatusUpdateAlarm (ipfsApiPollMs) { - const periodInMinutes = ipfsApiPollMs / 60000 - const when = Date.now() + 500 - return browser.alarms.create(ipfsApiStatusUpdateAlarm, { when, periodInMinutes }) +function updatePeerCountDependentStates () { + updateAutomaticModeRedirectState() + updateBrowserActionBadge() + updateContextMenus() +} + +async function getSwarmPeerCount () { + try { + const peerInfos = await ipfs.swarm.peers() + return peerInfos.length + } catch (error) { + // console.error(`Error while ipfs.swarm.peers: ${err}`) + return offlinePeerCount + } } +async function runIfNotIdle (action) { + try { + const state = await browser.idle.queryState(idleInSecs) + if (state === 'active') { + return action() + } + } catch (error) { + console.error('Unable to read idle state, executing action without idle check', error) + return action() + } +} + +// browserAction +// ------------------------------------------------------------------- + async function updateBrowserActionBadge () { let badgeText, badgeColor, badgeIcon badgeText = state.peerCount.toString() @@ -518,17 +537,14 @@ function onStorageChange (changes, area) { // eslint-disable-line no-unused-vars state.apiURL = new URL(change.newValue) state.apiURLString = state.apiURL.toString() ipfs = initIpfsApi(state.apiURLString) - browser.alarms.create(ipfsApiStatusUpdateAlarm, {}) + apiStatusUpdate() } else if (key === 'ipfsApiPollMs') { - browser.alarms.clear(ipfsApiStatusUpdateAlarm).then(() => { - createIpfsApiStatusUpdateAlarm(change.newValue) - }) + setApiStatusUpdateInterval(change.newValue) } else if (key === 'customGatewayUrl') { state.gwURL = new URL(change.newValue) state.gwURLString = state.gwURL.toString() } else if (key === 'useCustomGateway') { state.redirect = change.newValue - browser.alarms.create(ipfsRedirectUpdateAlarm, {}) } else if (key === 'linkify') { state.linkify = change.newValue } else if (key === 'automaticMode') { diff --git a/add-on/src/popup/browser-action.js b/add-on/src/popup/browser-action.js index aa73fa298..aa8be9c0d 100644 --- a/add-on/src/popup/browser-action.js +++ b/add-on/src/popup/browser-action.js @@ -18,6 +18,9 @@ const ipfsIconOn = '../../icons/ipfs-logo-on.svg' const ipfsIconOff = '../../icons/ipfs-logo-off.svg' const offline = 'offline' +var port +var state + function resolv (element) { // lookup DOM if element is just a string ID if (Object.prototype.toString.call(element) === '[object String]') { @@ -49,28 +52,24 @@ function getBackgroundPage () { return browser.runtime.getBackgroundPage() } -function getCurrentTab () { - return browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) +function notify (title, message) { + port.postMessage({event: 'notification', title: title, message: message}) } // Ipfs Context Page Actions // =================================================================== async function copyCurrentPublicGwAddress () { - const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - const publicGwAddress = new URL(currentTab.url.replace(bg.state.gwURLString, 'https://ipfs.io/')).toString() + const publicGwAddress = new URL(state.currentTab.url.replace(state.gwURLString, 'https://ipfs.io/')).toString() copyTextToClipboard(publicGwAddress) - bg.notify('notify_copiedPublicURLTitle', publicGwAddress) + notify('notify_copiedPublicURLTitle', publicGwAddress) window.close() } async function copyCurrentCanonicalAddress () { - const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - const rawIpfsAddress = currentTab.url.replace(/^.+(\/ip(f|n)s\/.+)/, '$1') + const rawIpfsAddress = state.currentTab.url.replace(/^.+(\/ip(f|n)s\/.+)/, '$1') copyTextToClipboard(rawIpfsAddress) - bg.notify('notify_copiedCanonicalAddressTitle', rawIpfsAddress) + notify('notify_copiedCanonicalAddressTitle', rawIpfsAddress) window.close() } @@ -89,11 +88,10 @@ async function pinCurrentResource () { deactivatePinButton() try { const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - const currentPath = await resolveToIPFS(new URL(currentTab.url).pathname) + const currentPath = await resolveToIPFS(new URL(state.currentTab.url).pathname) const pinResult = await bg.ipfs.pin.add(currentPath, { recursive: true }) console.log('ipfs.pin.add result', pinResult) - bg.notify('notify_pinnedIpfsResourceTitle', currentPath) + notify('notify_pinnedIpfsResourceTitle', currentPath) } catch (error) { handlePinError('notify_pinErrorTitle', error) } @@ -104,11 +102,10 @@ async function unpinCurrentResource () { deactivatePinButton() try { const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - const currentPath = await resolveToIPFS(new URL(currentTab.url).pathname) + const currentPath = await resolveToIPFS(new URL(state.currentTab.url).pathname) const result = await bg.ipfs.pin.rm(currentPath, {recursive: true}) console.log('ipfs.pin.rm result', result) - bg.notify('notify_unpinnedIpfsResourceTitle', currentPath) + notify('notify_unpinnedIpfsResourceTitle', currentPath) } catch (error) { handlePinError('notify_unpinErrorTitle', error) } @@ -140,8 +137,7 @@ async function handlePinError (errorMessageKey, error) { console.error(browser.i18n.getMessage(errorMessageKey), error) deactivatePinButton() try { - const bg = await getBackgroundPage() - bg.notify(errorMessageKey, error.message) + notify(errorMessageKey, error.message) } catch (error) { console.error('unable to access background page', error) } @@ -159,8 +155,7 @@ async function resolveToIPFS (path) { async function activatePinButton () { try { const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - const currentPath = await resolveToIPFS(new URL(currentTab.url).pathname) + const currentPath = await resolveToIPFS(new URL(state.currentTab.url).pathname) const response = await bg.ipfs.pin.ls(currentPath, {quiet: true}) console.log(`positive ipfs.pin.ls for ${currentPath}: ${JSON.stringify(response)}`) activateUnpinning() @@ -176,21 +171,27 @@ async function activatePinButton () { } async function updatePageActions () { - // console.log('running updatePageActions()') - try { - const bg = await getBackgroundPage() - const currentTab = await getCurrentTab() - if (bg.isIpfsPageActionsContext(currentTab.url)) { - deactivatePinButton() - show(ipfsContextActions) - copyPublicGwAddressButton.onclick = copyCurrentPublicGwAddress - copyIpfsAddressButton.onclick = copyCurrentCanonicalAddress + // IPFS contexts require access to background page + // which is denied in Private Browsing mode + const bg = await getBackgroundPage() + + // Check if current page is an IPFS one + const ipfsContext = bg && state && state.ipfsPageActionsContext + + // There is no point in displaying actions that require API interaction if API is down + const apiIsUp = state && state.peerCount >= 0 + + if (ipfsContext) { + show(ipfsContextActions) + copyPublicGwAddressButton.onclick = copyCurrentPublicGwAddress + copyIpfsAddressButton.onclick = copyCurrentCanonicalAddress + if (apiIsUp) { activatePinButton() } else { - hide(ipfsContextActions) + deactivatePinButton() } - } catch (error) { - console.error(`Error while setting up pageAction: ${error}`) + } else { + hide(ipfsContextActions) } } @@ -223,6 +224,7 @@ openPreferences.onclick = () => { } async function updateBrowserActionPopup () { + updatePageActions() // update redirect status const options = await browser.storage.local.get() try { @@ -247,46 +249,45 @@ async function updateBrowserActionPopup () { set('gateway-address-val', '???') } - try { - const background = await browser.runtime.getBackgroundPage() - if (background.ipfs) { - // update swarm peer count - try { - const peerCount = background.state.peerCount - set('swarm-peers-val', peerCount < 0 ? offline : peerCount) - ipfsIcon.src = peerCount > 0 ? ipfsIconOn : ipfsIconOff - if (peerCount > 0) { // API is online & there are peers - show('quick-upload') - } else { - hide('quick-upload') - } - if (peerCount < 0) { // API is offline - hide('open-webui') - } else { - show('open-webui') - } - } catch (error) { - console.error(`Unable update peer count due to ${error}`) - } - // update gateway version - try { - const v = await background.ipfs.version() - set('gateway-version-val', (v.commit ? v.version + '/' + v.commit : v.version)) - } catch (error) { - set('gateway-version-val', offline) - } + if (state) { + // update swarm peer count + const peerCount = state.peerCount + set('swarm-peers-val', peerCount < 0 ? offline : peerCount) + ipfsIcon.src = peerCount > 0 ? ipfsIconOn : ipfsIconOff + if (peerCount > 0) { // API is online & there are peers + show('quick-upload') + } else { + hide('quick-upload') } - } catch (error) { - console.error(`Error while accessing background page: ${error}`) + if (peerCount < 0) { // API is offline + hide('open-webui') + } else { + show('open-webui') + } + // update gateway version + set('gateway-version-val', state.gatewayVersion ? state.gatewayVersion : offline) } } // hide things that cause ugly reflow if removed later +deactivatePinButton() hide(ipfsContextActions) hide('quick-upload') hide('open-webui') -// listen to any changes and update diagnostics -browser.alarms.onAlarm.addListener(updateBrowserActionPopup) -document.addEventListener('DOMContentLoaded', updatePageActions) -document.addEventListener('DOMContentLoaded', updateBrowserActionPopup) +function onDOMContentLoaded () { + // set up initial layout that will remain if there are no peers + updateBrowserActionPopup() + // initialize connection to the background script which will trigger UI updates + port = browser.runtime.connect({name: 'browser-action-port'}) + port.onMessage.addListener((message) => { + if (message.statusUpdate) { + // console.log('In browser action, received message from background:', message) + state = message.statusUpdate + updateBrowserActionPopup() + } + }) +} + +// init +document.addEventListener('DOMContentLoaded', onDOMContentLoaded)