Skip to content
This repository has been archived by the owner on Jul 7, 2024. It is now read-only.
/ Orion Public archive

Shows the activity window when adding a new file #142

Merged
merged 8 commits into from
Jul 2, 2018
60 changes: 55 additions & 5 deletions app/api.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { remote } from 'electron'
import byteSize from 'byte-size'
import ipfsAPI from 'ipfs-api'
import { join } from 'path'
import { createWriteStream, mkdirSync } from 'fs'
import { join, parse } from 'path'
import { createWriteStream, mkdirSync, statSync } from 'fs'
import multiaddr from 'multiaddr'
import request from 'request-promise-native'
import pjson from '../package.json'
Expand All @@ -11,7 +10,9 @@ import { trackEvent } from './stats'

import Settings from 'electron-settings'
import { reportAndReject } from './lib/report/util'
import uuidv4 from 'uuid/v4'

export const ACTIVITIES_WINDOW_THRESHOLD = 16 * 1000 * 1000 // 16 MB
export const ERROR_IPFS_UNAVAILABLE = 'IPFS NOT AVAILABLE'
export const ERROR_IPFS_TIMEOUT = 'TIMEOUT'
let IPFS_CLIENT = null
Expand All @@ -22,6 +23,9 @@ export function setClientInstance (client) {
IPFS_CLIENT = client
}

const electron = require('electron')
const remote = electron.remote
const app = remote ? remote.app : electron.app
/**
* initIPFSClient will set up a new ipfs-api instance. It will try to get
* the configuration (api endpoint) from global vars
Expand Down Expand Up @@ -106,8 +110,54 @@ export function addFilesFromFSPath (filePaths, _queryGateways = queryGateways) {
if (!IPFS_CLIENT) return Promise.reject(ERROR_IPFS_UNAVAILABLE)
trackEvent('addFilesFromFSPath', { count: filePaths.length })

const options = { recursive: true }
const promises = filePaths.map(path => IPFS_CLIENT.util.addFromFs(path, options))
const promises = filePaths.map(path => {
const size = statSync(path).size
const filename = parse(path).base
const uuid = uuidv4()

// when uploading big files we want to show the progress in the activities window
if (size >= ACTIVITIES_WINDOW_THRESHOLD) {
app.emit('show-activities-window')
}

/**
* The fuction we pass under `progress` will be called with the byte length of chunks
* as they are added to IPFS
*/
const options = {
recursive: true,
progress: (progress) => {
app.emit('patch-activity', {
uuid,
/**
* {
* bytes: 2200,
* value: 2.2,
* unit: 'kB'
* }
*/
progress: {
bytes: progress,
...byteSize(progress)
}
})
}
}

app.emit('new-activity', {
uuid,
path,
filename,
type: 'add',
size: {
bytes: size,
...byteSize(size)
},
progress: 0
})

return IPFS_CLIENT.util.addFromFs(path, options)
})

return Promise.all(promises)
.then(fileUploadResults => {
Expand Down
25 changes: 20 additions & 5 deletions app/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@ import * as api from './api'
import ipfsApi from 'ipfs-api'
import multiaddr from 'multiaddr'
import request from 'request-promise-native'
// import gateways from './gateways'
import pjson from '../package'
// import Settings from 'electron-settings'

jest.mock('fs', () => {
return {
statSync: jest.fn().mockReturnValue({ size: 13 })
}
})

jest.mock('electron', () => {
return {
app: {
emit: jest.fn()
}
}
})

jest.mock('ipfs-api', () => {
return jest.fn().mockReturnValue('new-instance')
Expand All @@ -16,12 +28,12 @@ jest.mock('./gateways', () => {

jest.mock('./stats', () => {
return {
trackEvent: jest.fn().mockReturnValue(Promise.resolve())
trackEvent: jest.fn().mockResolvedValue()
}
})

jest.mock('request-promise-native', () => {
return jest.fn().mockReturnValue(Promise.resolve())
return jest.fn().mockResolvedValue()
})

jest.mock('electron-settings', () => {
Expand Down Expand Up @@ -237,7 +249,10 @@ describe('api.js', () => {
return api.addFilesFromFSPath(['./textfiles'], queryGatewaysMock)
.then(result => {
// assert
expect(addFromFsMock).toHaveBeenCalledWith('./textfiles', { recursive: true })
expect(addFromFsMock).toHaveBeenCalledWith('./textfiles', {
recursive: true,
progress: expect.any(Function)
})
expect(objectPutMock).toHaveBeenCalledWith({
Data: Buffer.from('\u0008\u0001'),
Links: [{
Expand Down
50 changes: 49 additions & 1 deletion app/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, dialog, shell } from 'electron'
import { app, dialog, shell, ipcMain } from 'electron'
import { autoUpdater } from 'electron-updater'
import { join as pathJoin } from 'path'
import pjson from '../package.json'
Expand Down Expand Up @@ -27,10 +27,16 @@ import {
import LoadingWindow from './windows/Loading/window'
import StorageWindow from './windows/Storage/window'
import WelcomeWindow from './windows/Welcome/window'
import ActivitiesWindow from './windows/Activities/window'

// Let's create the main window
app.mainWindow = null

// activities window
let activitiesWindow = null
const activitiesById = []
const activities = {}

// A little space for IPFS processes
global.IPFS_PROCESS = null

Expand Down Expand Up @@ -286,6 +292,48 @@ app.on('start-orion', () => {
startWelcome().then(startOrion)
})

/**
* This will create a new Activities window or bring to focus the existing one.
* We use this method instead of creating the window ourselves to ensure it's a singleton.
*/
app.on('show-activities-window', () => {
if (!activitiesWindow) {
activitiesWindow = ActivitiesWindow.create(app)
activitiesWindow.on('closed', () => {
activitiesWindow = null
})
} else {
activitiesWindow.show()
}
})

app.on('new-activity', (event) => {
activitiesById.push(event.uuid)
activities[event.uuid] = event

updateActivitiesWindow()
})

app.on('patch-activity', (event) => {
let activity = activities[event.uuid]
const patched = Object.assign({}, activity, event)
activities[event.uuid] = patched

updateActivitiesWindow()
})

// after activities window is mounted, it will emit this event
ipcMain.on('update-activities', () => {
updateActivitiesWindow()
})

function updateActivitiesWindow () {
// update the activitiesWindow
if (activitiesWindow) {
activitiesWindow.webContents.send('update', { activities, activitiesById })
}
}

app.on('ready', () => {
startWelcome().then(startOrion)
})
Expand Down
75 changes: 75 additions & 0 deletions app/windows/Activities/Components/Activity.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react'
import ProgressBar from '../../../components/ProgressBar'
import styled from 'styled-components'

const NameAndPath = styled.div`
display: flex;
flex-direction: column;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

h4 {
margin: 0px;
color: rgb(95,95,95);
}

span {
color: rgb(127,127,127);
}
`
const Progress = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;

div {
margin-top: 20px;
}

span {
color: rgb(95,95,95);
}
`
const ActivityWrapper = styled.div`
margin-top: 5px;
display: flex;
width: 100%;
padding: 5px;
border-radius: 5px;
background-color: rgb(239,239,239);
align-items: center;

.icon {
transform: scale(2.5);
padding: 0 25px;
color: rgb(95,95,95);
}
`

const Activity = ({ activity }) => {
const finished = activity.progress.bytes === activity.size.bytes

return (
<ActivityWrapper>
<span className="icon icon-upload"></span>
<NameAndPath>
<h4>{activity.filename}</h4>
<span>{activity.path}</span>
</NameAndPath>
<Progress>
{
!finished && <ProgressBar percentage={activity.progress.bytes / activity.size.bytes * 100} />
}
{
finished ? <span>{activity.size.value} {activity.size.unit}</span>
: <span>{activity.progress.value} {activity.progress.unit} of {activity.size.value} {activity.size.unit}</span>
}
</Progress>
</ActivityWrapper>
)
}

export default Activity
20 changes: 20 additions & 0 deletions app/windows/Activities/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>

<head>
<style>
.window-content {
padding: 24px !important;
display: initial !important;
background-color: rgb(236, 236, 236) !important;
}
</style>
</head>

<body>
<div id="host"></div>
</body>

<!-- Electron Javascript -->
<script src="loader.js" charset="utf-8"></script>
</html>
7 changes: 7 additions & 0 deletions app/windows/Activities/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Spinner from 'spin.js'

var target = document.getElementById('host')
new Spinner().spin(target)

// After the spinner is rendered, we require the actual component
setTimeout(() => require('./renderer.jsx'), 0)
59 changes: 59 additions & 0 deletions app/windows/Activities/renderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react'
import ReactDom from 'react-dom'
import { Window } from 'react-photonkit'
import { ipcRenderer } from 'electron'
import styled from 'styled-components'
import Activity from './Components/Activity'

const ActivityList = styled.div`
display: flex;
flex-direction: column;
width: 100%;
overflow-y: auto;
`

// This will store the loop's timeout ID
window.loopTimeoutID = null

class ActivitiesWindow extends React.Component {
data = {
activitiesById: [],
activities: {}
}

componentDidMount () {
ipcRenderer.on('update', (event, data) => {
this.data = data
})

ipcRenderer.send('update-activities')
this.startUpdateLoop()
}

startUpdateLoop = () => {
this.forceUpdate()
// update with 50fps
window.loopTimeoutID = setTimeout(this.startUpdateLoop, 20)
}

componentWillUnmount () {
ipcRenderer.removeAllListeners('update')
clearTimeout(window.loopTimeoutID)
}

render () {
const { activities, activitiesById } = this.data

return (
<Window>
<ActivityList>
{
activitiesById.map(id => <Activity key={id} activity={activities[id]} />)
}
</ActivityList>
</Window>
)
}
}

ReactDom.render(<ActivitiesWindow />, document.querySelector('#host'))
Loading