diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index 6c14c05f6..f186046dc 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -52,19 +52,19 @@ "description": "A label tooltip in Node status section of Browser Action pop-up (panel_statusSwarmPeersTitle)" }, "panel_quickImport": { - "message": "Quick Import/Share…", + "message": "Import", "description": "A menu item in Browser Action pop-up (panel_quickImport)" }, "panel_quickImportTooltip": { - "message": "Import files to IPFS and copy a publicly shareable link to your clipboard", + "message": "Import files to IPFS and copy a publicly shareable link to your clipboard.", "description": "A menu item tooltip in Browser Action pop-up (panel_quickImportTooltip)" }, "panel_openWebui": { - "message": "Go to My Node…", + "message": "My Node", "description": "A menu item in Browser Action pop-up (panel_openWebui)" }, "panel_openWebuiTooltip": { - "message": "Open your IPFS node's controls in your browser", + "message": "Open your IPFS node's controls in your browser.", "description": "A menu item in Browser Action pop-up (panel_openWebuiTooltip)" }, "panel_openPreferences": { @@ -76,11 +76,11 @@ "description": "A menu item in Browser Action pop-up (panel_activeTabSiteRedirectEnable)" }, "panel_activeTabSiteIntegrationsToggle": { - "message": "Enable on $1", + "message": "Enable for $1", "description": "A menu item in Browser Action pop-up (panel_activeTabSiteIntegrationsToggle)" }, "panel_activeTabSiteIntegrationsToggleTooltip": { - "message": "Enable/disable all IPFS integrations on $1", + "message": "Enable/disable all IPFS integrations on $1.", "description": "A menu item tooltip in Browser Action pop-up (panel_activeTabSiteIntegrationsToggleTooltip)" }, "panel_pinCurrentIpfsAddress": { @@ -91,12 +91,20 @@ "message": "Pin this page's IPFS resources to your node to have a local copy that's available offline and never thrown away.", "description": "A menu item tooltip in Browser Action pop-up (panel_pinCurrentIpfsAddressTooltip)" }, + "panelCopy_currentIpnsAddress": { + "message": "Copy IPNS Path", + "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpnsAddress)" + }, + "panelCopy_currentIpnsAddressTooltip": { + "message": "Use this content path with IPFS tools and gateways to reach the most recently updated version of this tab's content.", + "description": "A menu item tooltip in Browser Action pop-up (panelCopy_currentIpnsAddressTooltip)" + }, "panelCopy_currentIpfsAddress": { - "message": "Copy IPFS Content Path", + "message": "Copy IPFS Path", "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_currentIpfsAddress)" }, "panelCopy_currentIpfsAddressTooltip": { - "message": "A canonical content path that you can use with IPFS tools and gateways", + "message": "Use this content path with IPFS tools and gateways to reach the content in this tab at this moment in time. This snapshot won't change, even if content changes later.", "description": "A menu item tooltip in Browser Action pop-up (panelCopy_currentIpfsAddressTooltip)" }, "panelCopy_copyRawCid": { @@ -104,21 +112,29 @@ "description": "A menu item in Browser Action pop-up and right-click context menu (panelCopy_copyRawCid)" }, "panelCopy_copyRawCidTooltip": { - "message": "The unique IPFS content identifier for this page", + "message": "The unique IPFS content identifier for this tab at this moment in time. If content changes later, the CID will change too.", "description": "A menu item tooltip in Browser Action pop-up (panelCopy_copyRawCidTooltip)" }, - "panelCopy_copyRawCidNotReadyHint": { + "panelCopy_notReadyHint": { "message": "(waiting for DAG data)", - "description": "A hint in menu item in Browser Action pop-up to indicate CID is still being resolved (panelCopy_copyRawCidNotReadyHint)" + "description": "A hint in menu item in Browser Action pop-up to indicate value is still being resolved (panelCopy_notReadyHint)" }, "panel_copyCurrentPublicGwUrl": { "message": "Copy Shareable Link", "description": "A menu item in Browser Action pop-up and right-click context menu (panel_copyCurrentPublicGwUrl)" }, "panel_copyCurrentPublicGwUrlTooltip": { - "message": "This link works for anyone, even if they don't use IPFS", + "message": "A shareable link to this tab that works for anyone, even if they don't use IPFS.", "description": "A menu item tooltip in Browser Action pop-up (panel_copyCurrentPublicGwUrlTooltip)" }, + "panel_copyCurrentPermalink": { + "message": "Copy Snapshot Link", + "description": "A menu item in Browser Action pop-up (panel_copyCurrentPermalink)" + }, + "panel_copyCurrentPermalinkTooltip": { + "message": "A link to a snapshot of this tab at this moment in time; it won't change, even if content changes later. This link works for anyone, even if they don't use IPFS.", + "description": "A menu item tooltip in Browser Action pop-up (panel_copyCurrentPermalinkTooltip)" + }, "panel_contextMenuViewOnGateway": { "message": "View on Gateway", "description": "A menu item in Browser Action pop-up and right-click context menu (panel_contextMenuViewOnGateway)" diff --git a/add-on/src/lib/context-menus.js b/add-on/src/lib/context-menus.js index b38291d23..cfa9aa46e 100644 --- a/add-on/src/lib/context-menus.js +++ b/add-on/src/lib/context-menus.js @@ -55,14 +55,18 @@ const contextMenuImportToIpfs = 'contextMenu_importToIpfs' // Add X to IPFS const contextMenuImportToIpfsSelection = 'contextMenu_importToIpfsSelection' // Copy X -const contextMenuCopyCanonicalAddress = 'panelCopy_currentIpfsAddress' +const contextMenuCopyCidAddress = 'panelCopy_currentIpfsAddress' +const contextMenuCopyCanonicalAddress = 'panelCopy_currentIpnsAddress' const contextMenuCopyRawCid = 'panelCopy_copyRawCid' const contextMenuCopyAddressAtPublicGw = 'panel_copyCurrentPublicGwUrl' const contextMenuViewOnGateway = 'panel_contextMenuViewOnGateway' +const contextMenuCopyPermalink = 'panel_copyCurrentPermalink' +module.exports.contextMenuCopyCidAddress = contextMenuCopyCidAddress module.exports.contextMenuCopyCanonicalAddress = contextMenuCopyCanonicalAddress module.exports.contextMenuCopyRawCid = contextMenuCopyRawCid module.exports.contextMenuCopyAddressAtPublicGw = contextMenuCopyAddressAtPublicGw module.exports.contextMenuViewOnGateway = contextMenuViewOnGateway +module.exports.contextMenuCopyPermalink = contextMenuCopyPermalink // menu items that are enabled only when API is online const apiMenuItems = new Set() diff --git a/add-on/src/lib/copier.js b/add-on/src/lib/copier.js index cfe6d224c..c1f4f2d85 100644 --- a/add-on/src/lib/copier.js +++ b/add-on/src/lib/copier.js @@ -43,6 +43,12 @@ function createCopier (notify, ipfsPathValidator) { await copyTextToClipboard(ipfsPath, notify) }, + async copyCidAddress (context, contextType) { + const url = await findValueForContext(context, contextType) + const ipfsPath = await ipfsPathValidator.resolveToImmutableIpfsPath(url) + await copyTextToClipboard(ipfsPath, notify) + }, + async copyRawCid (context, contextType) { const url = await findValueForContext(context, contextType) try { @@ -68,6 +74,12 @@ function createCopier (notify, ipfsPathValidator) { const url = await findValueForContext(context, contextType) const publicUrl = ipfsPathValidator.resolveToPublicUrl(url) await copyTextToClipboard(publicUrl, notify) + }, + + async copyPermalink (context, contextType) { + const url = await findValueForContext(context, contextType) + const permalink = await ipfsPathValidator.resolveToPermalink(url) + await copyTextToClipboard(permalink, notify) } } } diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 7bac99fab..217dd7e51 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -21,7 +21,7 @@ const createNotifier = require('./notifier') const createCopier = require('./copier') const createInspector = require('./inspector') const { createRuntimeChecks } = require('./runtime-checks') -const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway } = require('./context-menus') +const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } = require('./context-menus') const createIpfsProxy = require('./ipfs-proxy') const { registerSubdomainProxy } = require('./http-proxy') const { showPendingLandingPages } = require('./on-installed') @@ -217,7 +217,7 @@ module.exports = async function init () { // Cache for async URL2CID resolution used by browser action // (resolution happens off-band so UI render is not blocked with sometimes expensive DHT traversal) - const url2cidCache = new LRU({ max: 10, maxAge: 1000 * 30 }) + const resolveCache = new LRU({ max: 10, maxAge: 1000 * 30 }) var browserActionPort @@ -235,8 +235,10 @@ module.exports = async function init () { notification: (message) => notify(message.title, message.message), [contextMenuViewOnGateway]: inspector.viewOnGateway, [contextMenuCopyCanonicalAddress]: copier.copyCanonicalAddress, + [contextMenuCopyCidAddress]: copier.copyCidAddress, [contextMenuCopyRawCid]: copier.copyRawCid, - [contextMenuCopyAddressAtPublicGw]: copier.copyAddressAtPublicGw + [contextMenuCopyAddressAtPublicGw]: copier.copyAddressAtPublicGw, + [contextMenuCopyPermalink]: copier.copyPermalink } function handleMessageFromBrowserAction (message) { @@ -279,13 +281,22 @@ module.exports = async function init () { if (info.isIpfsContext) { info.currentTabPublicUrl = ipfsPathValidator.resolveToPublicUrl(url) info.currentTabContentPath = ipfsPathValidator.resolveToIpfsPath(url) - if (!url2cidCache.has(url)) { - // run async resolution in the next event loop + if (resolveCache.has(url)) { + const [immutableIpfsPath, permalink, cid] = resolveCache.get(url) + info.currentTabImmutablePath = immutableIpfsPath + info.currentTabPermalink = permalink + info.currentTabCid = cid + } else { + // run async resolution in the next event loop so it does not block the UI setImmediate(async () => { - url2cidCache.set(url, await ipfsPathValidator.resolveToCid(url)) + resolveCache.set(url, [ + await ipfsPathValidator.resolveToImmutableIpfsPath(url), + await ipfsPathValidator.resolveToPermalink(url), + await ipfsPathValidator.resolveToCid(url) + ]) + await sendStatusUpdateToBrowserAction() }) } - info.currentTabCid = url2cidCache.get(url) } info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url) info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index c9b01a0c2..deead4264 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -6,7 +6,7 @@ const isIPFS = require('is-ipfs') const isFQDN = require('is-fqdn') // For how long more expensive lookups (DAG traversal etc) should be cached -const RESULT_TTL_MS = 30 * 1000 +const RESULT_TTL_MS = 300000 // 5 minutes // Turns URL or URIencoded path into a content path function ipfsContentPath (urlOrPath, opts) { @@ -254,6 +254,20 @@ function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) { return null }, + // Resolve URL or path to HTTP URL with CID: + // - IPFS paths are attached to HTTP Gateway root + // - URL of DNSLinked websties are resolved to CIDs + // The purpose of this resolver is to always return a meaningful, publicly + // accessible URL that can be accessed without the need of an IPFS client + // and that never changes. + async resolveToPermalink (urlOrPath, optionalGatewayUrl) { + const input = urlOrPath + const ipfsPath = await this.resolveToImmutableIpfsPath(input) + const gateway = optionalGatewayUrl || getState().pubGwURLString + if (ipfsPath) return pathAtHttpGateway(ipfsPath, gateway) + return input.startsWith('http') ? input : null + }, + // Resolve URL or path to IPFS Path: // - The path can be /ipfs/ or /ipns/ // - Keeps pathname + ?search + #hash from original URL diff --git a/add-on/src/popup/browser-action/browser-action.css b/add-on/src/popup/browser-action/browser-action.css index 0a45b9328..11189702b 100644 --- a/add-on/src/popup/browser-action/browser-action.css +++ b/add-on/src/popup/browser-action/browser-action.css @@ -9,7 +9,7 @@ .header-icon:active { color: #edf0f4; - transform: translateY(4px); + transform: translateY(2px); } .header-icon[disabled], .header-icon[disabled]:active { diff --git a/add-on/src/popup/browser-action/context-actions.js b/add-on/src/popup/browser-action/context-actions.js index e3aa786bb..2f3dcb500 100644 --- a/add-on/src/popup/browser-action/context-actions.js +++ b/add-on/src/popup/browser-action/context-actions.js @@ -9,10 +9,14 @@ const { sameGateway } = require('../../lib/ipfs-path') const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, + contextMenuCopyPermalink, contextMenuCopyRawCid, - contextMenuCopyCanonicalAddress + contextMenuCopyCanonicalAddress, + contextMenuCopyCidAddress } = require('../../lib/context-menus') +const notReady = browser.i18n.getMessage('panelCopy_notReadyHint') + // Context Actions are displayed in Browser Action and Page Action (FF only) function contextActions ({ active, @@ -24,9 +28,11 @@ function contextActions ({ currentFqdn, currentDnslinkFqdn, currentTabIntegrationsOptOut, - currentTabContentPath, - currentTabCid, - currentTabPublicUrl, + currentTabContentPath = notReady, + currentTabImmutablePath = notReady, + currentTabCid = notReady, + currentTabPublicUrl = notReady, + currentTabPermalink = notReady, ipfsNodeType, isIpfsContext, isPinning, @@ -42,6 +48,7 @@ function contextActions ({ }) { const activeCidResolver = active && isIpfsOnline && isApiAvailable && currentTabCid const activePinControls = active && isIpfsOnline && isApiAvailable + const isMutable = currentTabContentPath.startsWith('/ipns/') const activeViewOnGateway = (currentTab) => { if (!currentTab) return false const { url } = currentTab @@ -52,27 +59,43 @@ function contextActions ({ if (!isIpfsContext) return return html`
${activeViewOnGateway(currentTab) - ? navItem({ - text: browser.i18n.getMessage(contextMenuViewOnGateway), - onClick: () => onViewOnGateway(contextMenuViewOnGateway) - }) - : null} + ? navItem({ + text: browser.i18n.getMessage(contextMenuViewOnGateway), + onClick: () => onViewOnGateway(contextMenuViewOnGateway) + }) + : null} ${navItem({ text: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw), title: browser.i18n.getMessage('panel_copyCurrentPublicGwUrlTooltip'), helperText: currentTabPublicUrl, onClick: () => onCopy(contextMenuCopyAddressAtPublicGw) })} + ${isMutable + ? navItem({ + text: browser.i18n.getMessage(contextMenuCopyPermalink), + title: browser.i18n.getMessage('panel_copyCurrentPermalinkTooltip'), + helperText: currentTabPermalink, + onClick: () => onCopy(contextMenuCopyPermalink) + }) + : ''} + ${isMutable + ? navItem({ + text: browser.i18n.getMessage(contextMenuCopyCanonicalAddress), + title: browser.i18n.getMessage('panelCopy_currentIpnsAddressTooltip'), + helperText: currentTabContentPath, + onClick: () => onCopy(contextMenuCopyCanonicalAddress) + }) + : ''} ${navItem({ - text: browser.i18n.getMessage(contextMenuCopyCanonicalAddress), + text: browser.i18n.getMessage(contextMenuCopyCidAddress), title: browser.i18n.getMessage('panelCopy_currentIpfsAddressTooltip'), - helperText: currentTabContentPath, - onClick: () => onCopy(contextMenuCopyCanonicalAddress) + helperText: currentTabImmutablePath, + onClick: () => onCopy(contextMenuCopyCidAddress) })} ${navItem({ text: browser.i18n.getMessage(contextMenuCopyRawCid), title: browser.i18n.getMessage('panelCopy_copyRawCidTooltip'), - helperText: (currentTabCid || browser.i18n.getMessage('panelCopy_copyRawCidNotReadyHint')), + helperText: currentTabCid, disabled: !activeCidResolver, onClick: () => onCopy(contextMenuCopyRawCid) })} @@ -114,7 +137,7 @@ function activeTabActions (state) { const showActiveTabSection = (state.isRedirectContext) || state.isIpfsContext if (!showActiveTabSection) return return html` -
+
${navHeader('panel_activeTabSectionHeader')}
${contextActions(state)}
diff --git a/add-on/src/popup/browser-action/gateway-status.js b/add-on/src/popup/browser-action/gateway-status.js index 2711294bb..a14532e84 100644 --- a/add-on/src/popup/browser-action/gateway-status.js +++ b/add-on/src/popup/browser-action/gateway-status.js @@ -26,7 +26,7 @@ module.exports = function gatewayStatus ({ }) { const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl return html` -
    +
      ${statusEntry({ label: 'panel_statusSwarmPeers', labelLegend: 'panel_statusSwarmPeersTitle', diff --git a/add-on/src/popup/browser-action/header.js b/add-on/src/popup/browser-action/header.js index 1ce5d53b7..c53280e3d 100644 --- a/add-on/src/popup/browser-action/header.js +++ b/add-on/src/popup/browser-action/header.js @@ -12,7 +12,7 @@ const gatewayStatus = require('./gateway-status') module.exports = function header (props) { const { ipfsNodeType, active, onToggleActive, onOpenPrefs, onOpenReleaseNotes, isIpfsOnline, onOpenWelcomePage, showUpdateIndicator } = props return html` -
      +
      +
      ${browser.i18n.getMessage(label)}
      ` diff --git a/add-on/src/popup/browser-action/page.js b/add-on/src/popup/browser-action/page.js index a84e58003..4b8a46f89 100644 --- a/add-on/src/popup/browser-action/page.js +++ b/add-on/src/popup/browser-action/page.js @@ -30,8 +30,10 @@ module.exports = function browserActionPage (state, emit) { return html`
      - ${header(headerProps)} - ${tools(opsProps)} +
      + ${header(headerProps)} + ${tools(opsProps)} +
      ${activeTabActions(activeTabActionsProps)}
      ` diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index 800e22307..3ba3df38e 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -6,7 +6,7 @@ const isIPFS = require('is-ipfs') const all = require('it-all') const { trimHashAndSearch, ipfsContentPath } = require('../../lib/ipfs-path') const { welcomePage } = require('../../lib/on-installed') -const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress } = require('../../lib/context-menus') +const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, contextMenuCopyPermalink, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress } = require('../../lib/context-menus') // The store contains and mutates the state for the app module.exports = (state, emitter) => { @@ -76,12 +76,18 @@ module.exports = (state, emitter) => { case contextMenuCopyCanonicalAddress: port.postMessage({ event: contextMenuCopyCanonicalAddress }) break + case contextMenuCopyCidAddress: + port.postMessage({ event: contextMenuCopyCidAddress }) + break case contextMenuCopyRawCid: port.postMessage({ event: contextMenuCopyRawCid }) break case contextMenuCopyAddressAtPublicGw: port.postMessage({ event: contextMenuCopyAddressAtPublicGw }) break + case contextMenuCopyPermalink: + port.postMessage({ event: contextMenuCopyPermalink }) + break } window.close() }) @@ -307,7 +313,7 @@ module.exports = (state, emitter) => { if (state.isPinning || state.isUnPinning) return try { const currentPath = await resolveToPinPath(ipfs, status.currentTab.url) - const response = await all(ipfs.pin.ls(currentPath, { type: 'recursive', quiet: true })) + const response = await all(ipfs.pin.ls({ paths: [currentPath], type: 'recursive' })) console.log(`positive ipfs.pin.ls for ${currentPath}: ${JSON.stringify(response)}`) state.isPinned = true } catch (error) { diff --git a/add-on/src/popup/browser-action/tools-button.js b/add-on/src/popup/browser-action/tools-button.js new file mode 100644 index 000000000..4f77d3aa4 --- /dev/null +++ b/add-on/src/popup/browser-action/tools-button.js @@ -0,0 +1,29 @@ +'use strict' +/* eslint-env browser, webextensions */ + +const html = require('choo/html') + +function toolsButton ({ iconD, iconSize, text, title, disabled, style, onClick }) { + let buttonStyle = 'header-icon fade-in w-50 ba bw1 snow b--snow bg-transparent f7 ph1 pv0 br4 ma1 flex justify-center items-center truncate' + if (disabled) { + buttonStyle += ' o-60' + } else { + buttonStyle += ' pointer' + } + if (style) { + buttonStyle += ` ${style}` + } + if (disabled) { + title = '' + } + + return html` + +
      + +
      ${text}
      +
      + ` +} + +module.exports = toolsButton diff --git a/add-on/src/popup/browser-action/tools.js b/add-on/src/popup/browser-action/tools.js index 82378eceb..55ed426fe 100644 --- a/add-on/src/popup/browser-action/tools.js +++ b/add-on/src/popup/browser-action/tools.js @@ -3,7 +3,7 @@ const browser = require('webextension-polyfill') const html = require('choo/html') -const navItem = require('./nav-item') +const toolsButton = require('./tools-button') module.exports = function tools ({ active, @@ -17,18 +17,22 @@ module.exports = function tools ({ const activeWebUI = active && isIpfsOnline && ipfsNodeType !== 'embedded' return html` -
      - ${navItem({ +
      + ${toolsButton({ text: browser.i18n.getMessage('panel_quickImport'), title: browser.i18n.getMessage('panel_quickImportTooltip'), disabled: !activeQuickImport, - onClick: onQuickImport + onClick: onQuickImport, + iconSize: 20, + iconD: 'M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V52.74H33.53v-5.48h13.73V33.53h5.48v13.73h13.73v5.48H52.74z' })} - ${navItem({ + ${toolsButton({ text: browser.i18n.getMessage('panel_openWebui'), title: browser.i18n.getMessage('panel_openWebuiTooltip'), disabled: !activeWebUI, - onClick: onOpenWebUi + onClick: onOpenWebUi, + iconSize: 18, + iconD: 'M69.69 20.57c-.51-.51-1.06-1-1.62-1.47l-.16-.1c-.56-.46-1.15-.9-1.76-1.32l-.5-.35c-.25-.17-.52-.32-.79-.48A28.27 28.27 0 0050 12.23h-.69a28.33 28.33 0 00-27.52 28.36c0 13.54 19.06 37.68 26 46a3.21 3.21 0 005 0c6.82-8.32 25.46-32.25 25.46-45.84a28.13 28.13 0 00-8.56-20.18zM51.07 49.51a9.12 9.12 0 119.13-9.12 9.12 9.12 0 01-9.13 9.12z' })}
      ` diff --git a/package.json b/package.json index f76e934e7..a7c48840c 100644 --- a/package.json +++ b/package.json @@ -165,5 +165,10 @@ "engines": { "node": ">=10.0.0", "npm": ">=6.0.0" + }, + "husky": { + "hooks": { + "pre-commit": "npm run fix:lint:standard" + } } }