Skip to content

feat: improve dialogs UX #919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,40 @@
"yes": "Yes",
"no": "No",
"close": "Close",
"ok": "OK",
"cancel": "Cancel",
"reportTheError": "Report the error",
"restartIpfsDesktop": "Restart IPFS Desktop",
"openLogs": "Open logs",
"anErrorHasOccurred": "An error has occurred",
"anUnexpectedErrorHasOccurred": "An error occurred but it does not prevent the execution of IPFS Desktop. You can either inspect the logs for yourself or report the error to the developers.",
"ipfsCommandLineTools": "IPFS command line tools",
"ipfsCommandLineToolsWindows": "We detected you already have the IPFS command line tools on your PATH. IPFS Desktop can pre-prepend its tools to the PATH and keep them up to date. Do you agree?",
"ipfsCommandLineToolsAlreadyExists": "IPFS Desktop detected you already have IPFS CLI installed. We can replace the existing ipfs command installed at /usr/local/bin/ipfs, and update it when new versions are available. Do you agree?",
"ipfsCommandLineToolsNotExists": "IPFS Desktop can keep the IPFS command-line interface (CLI) up to date. It will be installed at /usr/local/bin/ipfs. Do you agree?",
"polkitNotFound": "IPFS can't be added to /usr/local/bin/ without polkit agent.",
"noPermission": "IPFS couldn't be added to /usr/local/bin/, either because you entered the wrong password, or you denied permission."
"polkitDialog": {
"title": "Polkit not found",
"message": "IPFS can't be added to /usr/local/bin/ without polkit agent."
},
"noPermissionDialog": {
"title": "No permission",
"message": "IPFS couldn't be added to /usr/local/bin/, either because you entered the wrong password, or you denied permission."
},
"ipfsNotRunningDialog": {
"title": "IPFS is not running",
"message": "IPFS needs to be running to perform this action. Do you want to start the daemon?"
},
"recoverableErrorDialog": {
"title": "An error has occurred",
"message": "An error occurred but it does not prevent the execution of IPFS Desktop. You can either inspect the logs for yourself or report the error to the developers."
},
"cannotConnectToApiDialog": {
"title": "Cannot connect to API",
"message": "IPFS Desktop cannot connect to the API address provided: { addr }."
},
"ipfsDesktopHasShutdownDialog": {
"title": "IPFS Desktop has shutdown",
"message": "IPFS Desktop has shutdown because of an error. You can restart the app or report the error to the developers, which requires a GitHub account."
},
"cmdToolsDialog": {
"title": "IPFS command line tools",
"messageWindows": "You already have the IPFS command line tools on your PATH. IPFS Desktop can pre-prepend its tools to the PATH and keep them up to date. Do you agree?",
"messageAlreadyExists": "IPFS Desktop detected you already have IPFS CLI installed. We can replace the existing ipfs command installed at /usr/local/bin/ipfs, and update it when new versions are available. Do you agree?",
"messageNotExists": "IPFS Desktop can keep the IPFS command-line interface (CLI) up to date. It will be installed at /usr/local/bin/ipfs. Do you agree?"
}
}
48 changes: 48 additions & 0 deletions src/dialogs/dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { app, dialog } from 'electron'
import i18n from 'i18next'
import os from 'os'

// NOTE: always send the buttons in the order [OK, Cancel, ...Actions].
// See this post for more interesting information about the topic:
// https://medium.muz.li/ok-key-and-cancel-key-which-one-should-be-set-up-on-the-left-4780e86c16eb
export default function ({ title, message, type = 'info', buttons = [
i18n.t('ok'),
i18n.t('cancel')
], ...opts }) {
let options = {
type: type,
buttons: buttons,
noLink: true,
...opts
}

if (os.platform() === 'darwin') {
options.message = title
options.detail = message
} else {
options.title = title
options.message = message
}

const isInverse = os.platform() === 'windows' ||
os.platform() === 'linux'

if (isInverse) {
options.buttons.reverse()
}

if (buttons.length > 1) {
options.defaultId = isInverse ? buttons.length - 1 : 0
options.cancelId = isInverse ? buttons.length - 2 : 1
}

if (app.dock) app.dock.show()
const selected = dialog.showMessageBox(options)
if (app.dock) app.dock.hide()

if (!isInverse) {
return selected
}

return buttons.length - selected - 1
}
64 changes: 64 additions & 0 deletions src/dialogs/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { app, shell } from 'electron'
import i18n from 'i18next'
import dialog from './dialog'

const issueTemplate = (e) => `Please describe what you were doing when this error happened.

**Specifications**

- **OS**: ${process.platform}
- **IPFS Desktop Version**: ${app.getVersion()}
- **Electron Version**: ${process.versions.electron}
- **Chrome Version**: ${process.versions.chrome}

**Error**

\`\`\`
${e.stack}
\`\`\`
`

let hasErrored = false

export function criticalErrorDialog (e) {
if (hasErrored) return
hasErrored = true

const option = dialog({
title: i18n.t('ipfsDesktopHasShutdownDialog.title'),
message: i18n.t('ipfsDesktopHasShutdownDialog.message'),
type: 'error',
buttons: [
i18n.t('restartIpfsDesktop'),
i18n.t('close'),
i18n.t('reportTheError')
]
})

if (option === 0) {
app.relaunch()
} else if (option === 2) {
shell.openExternal(`https://github.com/ipfs-shipyard/ipfs-desktop/issues/new?body=${encodeURI(issueTemplate(e))}`)
}

app.exit(1)
}

export function recoverableErrorDialog (e) {
const option = dialog({
title: i18n.t('recoverableErrorDialog.title'),
message: i18n.t('recoverableErrorDialog.message'),
type: 'error',
buttons: [
i18n.t('close'),
i18n.t('reportTheError'),
i18n.t('openLogs')
]
})

if (option === 1) {
shell.openExternal(`https://github.com/ipfs-shipyard/ipfs-desktop/issues/new?body=${encodeURI(issueTemplate(e))}`)
} else if (option === 2) {
shell.openItem(app.getPath('userData'))
}
}
10 changes: 10 additions & 0 deletions src/dialogs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import showDialog from './dialog'
import { criticalErrorDialog, recoverableErrorDialog } from './errors'
import ipfsNotRunningDialog from './ipfs-not-running'

export {
showDialog,
criticalErrorDialog,
recoverableErrorDialog,
ipfsNotRunningDialog
}
31 changes: 31 additions & 0 deletions src/dialogs/ipfs-not-running.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import dialog from './dialog'
import i18n from 'i18next'
import { ipcMain } from 'electron'
import { STATUS } from '../late/register-daemon'
import { logger } from '../utils'

export default async function () {
logger.info('[ipfs-not-running] an action needs ipfs to be running')

const option = dialog({
title: i18n.t('ipfsNotRunningDialog.title'),
message: i18n.t('ipfsNotRunningDialog.message'),
buttons: [
i18n.t('start'),
i18n.t('cancel')
]
})

if (option === 0) {
return new Promise(resolve => {
ipcMain.on('ipfsd', (status) => {
if (status === STATUS.STARTING_STARTED) return
resolve(status === STATUS.STARTING_FINISHED)
})

ipcMain.emit('startIpfs')
})
}

return false
}
5 changes: 3 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { app, dialog } from 'electron'
import { showErrorMessage, logger } from './utils'
import { logger } from './utils'
import earlySetup from './early'
import lateSetup from './late'
import { criticalErrorDialog } from './dialogs'

// Hide Dock
if (app.dock) app.dock.hide()
Expand All @@ -22,7 +23,7 @@ app.on('will-finish-launching', () => {

function handleError (e) {
logger.error(e)
showErrorMessage(e)
criticalErrorDialog(e)
}

process.on('uncaughtException', handleError)
Expand Down
2 changes: 1 addition & 1 deletion src/late/download-hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function handler (ctx) {

return async () => {
const text = clipboard.readText().trim()
const ipfsd = getIpfsd()
const ipfsd = await getIpfsd()

if (!ipfsd || !text) {
return
Expand Down
26 changes: 13 additions & 13 deletions src/late/ipfs-on-path/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import sudo from 'sudo-prompt'
import which from 'which'
import { execFile } from 'child_process'
import { createToggler } from '../utils'
import { logger, store, showRecoverableError } from '../../utils'
import { logger, store } from '../../utils'
import { ipcMain, app, dialog } from 'electron'
import { showDialog, recoverableErrorDialog } from '../../dialogs'

const SETTINGS_OPTION = 'ipfsOnPath'

Expand Down Expand Up @@ -42,20 +43,19 @@ function firstTime () {

const suffix = isWindows ? 'Windows' : ipfsExists ? 'AlreadyExists' : 'NotExists'

const option = dialog.showMessageBox({
const option = showDialog({
type: 'info',
message: i18n.t('ipfsCommandLineTools'),
detail: i18n.t('ipfsCommandLineTools' + suffix),
title: i18n.t('cmdToolsDialog.title'),
message: i18n.t('cmdToolsDialog.message' + suffix),
buttons: [
i18n.t('no'),
i18n.t('yes')
],
cancelId: 0
i18n.t('yes'),
i18n.t('no')
]
})

if (app.dock) app.dock.hide()

if (option === 1) {
if (option === 0) {
// Trigger the toggler.
ipcMain.emit('config.toggle', null, SETTINGS_OPTION)
} else {
Expand All @@ -72,7 +72,7 @@ async function runWindows (script) {
], {}, err => {
if (err) {
logger.error(`[ipfs on path] ${err.toString()}`)
showRecoverableError(err)
recoverableErrorDialog(err)
return resolve(false)
}

Expand Down Expand Up @@ -107,11 +107,11 @@ async function run (script) {
logger.error(`[ipfs on path] error: ${str}`)

if (str.includes('No polkit authentication agent found')) {
dialog.showErrorBox(i18n.t('anErrorHasOccurred'), i18n.t('polkitNotFound'))
dialog.showErrorBox(i18n.t('polkitDialog.title'), i18n.t('polkitDialog.message'))
} else if (str.includes('User did not grant permission')) {
dialog.showErrorBox(i18n.t('anErrorHasOccurred'), i18n.t('noPermission'))
dialog.showErrorBox(i18n.t('noPermissionDialog.title'), i18n.t('noPermissionDialog.message'))
} else {
showRecoverableError(err)
recoverableErrorDialog(err)
}

return false
Expand Down
17 changes: 14 additions & 3 deletions src/late/register-daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { store, createDaemon, logger } from '../utils'
import { app, ipcMain } from 'electron'
import fs from 'fs-extra'
import { join } from 'path'
import { ipfsNotRunningDialog } from '../dialogs'

export const STATUS = {
STARTING_STARTED: 1,
Expand All @@ -26,7 +27,17 @@ export default async function (ctx) {
ipcMain.emit('ipfsd', status)
})

ctx.getIpfsd = () => ipfsd
ctx.getIpfsd = async (optional = false) => {
if (optional) {
return ipfsd
}

if (!ipfsd) {
await ipfsNotRunningDialog()
}

return ipfsd
}

const startIpfs = async () => {
if (ipfsd) {
Expand All @@ -50,7 +61,7 @@ export default async function (ctx) {
logger.info('[ipfsd] daemon started')
updateStatus(STATUS.STARTING_FINISHED)
} catch (err) {
logger.error('[ipfsd] %v', err)
logger.error('[ipfsd]', err)
updateStatus(STATUS.STARTING_FAILED)
}
}
Expand All @@ -77,7 +88,7 @@ export default async function (ctx) {
// user wait, and taking longer prevents the update mechanism from working.
ipfsdObj.stop(180, err => {
if (err) {
logger.error('[ipfsd] %v', err)
logger.error('[ipfsd] ', err)
updateStatus(STATUS.STOPPING_FAILED)
return resolve(err)
}
Expand Down
2 changes: 1 addition & 1 deletion src/late/take-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function handleScreenshot (ctx) {
let { getIpfsd, launchWebUI } = ctx

return async (_, output) => {
const ipfsd = getIpfsd()
const ipfsd = await getIpfsd()

if (!ipfsd) {
return
Expand Down
10 changes: 5 additions & 5 deletions src/late/webui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const createWindow = () => {
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
fullscreenWindowTitle: 'true',
fullscreenWindowTitle: true,
width: store.get('window.width', dimensions.width < 1440 ? dimensions.width : 1440),
height: store.get('window.height', dimensions.height < 900 ? dimensions.height : 900),
webPreferences: {
Expand All @@ -25,11 +25,11 @@ const createWindow = () => {
})

window.webContents.on('crashed', event => {
logger.error('[web ui] crashed: %v', event)
logger.error('[web ui] crashed: ', event)
})

window.webContents.on('unresponsive', event => {
logger.warn('[web ui] unresponsive: %v', event)
logger.warn('[web ui] unresponsive: ', event)
})

window.on('resize', () => {
Expand Down Expand Up @@ -65,8 +65,8 @@ export default async function (ctx) {
}
}

ipcMain.on('ipfsd', () => {
const ipfsd = ctx.getIpfsd()
ipcMain.on('ipfsd', async () => {
const ipfsd = await ctx.getIpfsd(true)

if (ipfsd && ipfsd.apiAddr !== apiAddress) {
apiAddress = ipfsd.apiAddr
Expand Down
7 changes: 0 additions & 7 deletions src/utils/add-to-ipfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ export default async function ({ getIpfsd, launchWebUI }, file) {
const ipfsd = await getIpfsd()

if (!ipfsd) {
logger.info('[add to ipfs] daemon is not running')

notify({
title: i18n.t('ipfsNotRunning'),
body: i18n.t('desktopIsStartedButDaemonOffline')
})

return
}

Expand Down
Loading