/* global LIMITS */ import FileSender from './fileSender'; import FileReceiver from './fileReceiver'; import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils'; import * as metrics from './metrics'; import { bytes } from './utils'; import okDialog from './ui/okDialog'; import copyDialog from './ui/copyDialog'; import signupDialog from './ui/signupDialog'; export default function(state, emitter) { let lastRender = 0; let updateTitle = false; function render() { emitter.emit('render'); } async function checkFiles() { const changes = await state.user.syncFileList(); const rerender = changes.incoming || changes.downloadCount; if (rerender) { render(); } } function updateProgress() { if (updateTitle) { emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio)); } render(); } emitter.on('DOMContentLoaded', () => { document.addEventListener('blur', () => (updateTitle = true)); document.addEventListener('focus', () => { updateTitle = false; emitter.emit('DOMTitleChange', 'Firefox Send'); }); checkFiles(); }); emitter.on('render', () => { lastRender = Date.now(); }); emitter.on('login', email => { state.user.login(email); }); emitter.on('logout', () => { state.user.logout(); metrics.loggedOut({ trigger: 'button' }); emitter.emit('pushState', '/'); }); emitter.on('removeUpload', file => { state.archive.remove(file); render(); }); emitter.on('delete', async ownedFile => { try { metrics.deletedUpload({ size: ownedFile.size, time: ownedFile.time, speed: ownedFile.speed, type: ownedFile.type, ttl: ownedFile.expiresAt - Date.now(), location }); state.storage.remove(ownedFile.id); await ownedFile.del(); } catch (e) { state.raven.captureException(e); } render(); }); emitter.on('cancel', () => { state.transfer.cancel(); }); emitter.on('addFiles', async ({ files }) => { if (files.length < 1) { return; } const maxSize = state.user.maxSize; try { state.archive.addFiles(files, maxSize); } catch (e) { if (e.message === 'fileTooBig' && maxSize < LIMITS.MAX_FILE_SIZE) { return emitter.emit('signup-cta', 'size'); } state.modal = okDialog( state.translate(e.message, { size: bytes(maxSize), count: LIMITS.MAX_FILES_PER_ARCHIVE }) ); } render(); }); emitter.on('signup-cta', source => { state.modal = signupDialog(source); render(); }); /* FIXME choo on Edge double-triggers loaded routes causing 'authenticate' to fire twice which leads to an error. Until that's fixed we have authLocked to prevent the second event from causing the error. Once choo doesn't double-trigger we can remove authLocked. */ let authLocked = false; emitter.on('authenticate', async (code, oauthState) => { if (authLocked) { return; } authLocked = true; try { await state.user.finishLogin(code, oauthState); await state.user.syncFileList(); emitter.emit('replaceState', '/'); } catch (e) { emitter.emit('replaceState', '/error'); setTimeout(render); } authLocked = false; }); emitter.on('upload', async () => { if (state.storage.files.length >= LIMITS.MAX_ARCHIVES_PER_USER) { state.modal = okDialog( state.translate('tooManyArchives', { count: LIMITS.MAX_ARCHIVES_PER_USER }) ); return render(); } const archive = state.archive; const sender = new FileSender(); sender.on('progress', updateProgress); sender.on('encrypting', render); sender.on('complete', render); state.transfer = sender; state.uploading = true; render(); const links = openLinksInNewTab(); await delay(200); const start = Date.now(); try { const ownedFile = await sender.upload(archive, state.user.bearerToken); state.storage.totalUploads += 1; const duration = Date.now() - start; metrics.completedUpload(archive, duration); state.storage.addFile(ownedFile); // TODO integrate password into /upload request if (archive.password) { emitter.emit('password', { password: archive.password, file: ownedFile }); } state.modal = copyDialog(ownedFile.name, ownedFile.url); } catch (err) { if (err.message === '0') { //cancelled. do nothing const duration = Date.now() - start; metrics.cancelledUpload(archive, duration); render(); } else { // eslint-disable-next-line no-console console.error(err); state.raven.captureException(err); metrics.stoppedUpload(archive); emitter.emit('pushState', '/error'); } } finally { openLinksInNewTab(links, false); archive.clear(); state.uploading = false; state.transfer = null; await state.user.syncFileList(); render(); } }); emitter.on('password', async ({ password, file }) => { try { state.settingPassword = true; render(); await file.setPassword(password); state.storage.writeFile(file); await delay(1000); } catch (err) { // eslint-disable-next-line no-console console.error(err); state.passwordSetError = err; } finally { state.settingPassword = false; } render(); }); emitter.on('getMetadata', async () => { const file = state.fileInfo; const receiver = new FileReceiver(file); try { await receiver.getMetadata(); state.transfer = receiver; } catch (e) { if (e.message === '401' || e.message === '404') { file.password = null; if (!file.requiresPassword) { return emitter.emit('pushState', '/404'); } } } render(); }); emitter.on('download', async file => { state.transfer.on('progress', updateProgress); state.transfer.on('decrypting', render); state.transfer.on('complete', render); const links = openLinksInNewTab(); const size = file.size; const start = Date.now(); try { const dl = state.transfer.download({ stream: state.capabilities.streamDownload }); render(); await dl; state.storage.totalDownloads += 1; const duration = Date.now() - start; metrics.completedDownload({ size, duration, password_protected: file.requiresPassword }); } catch (err) { if (err.message === '0') { // download cancelled state.transfer.reset(); render(); } else { // eslint-disable-next-line no-console state.transfer = null; const location = err.message === '404' ? '/404' : '/error'; if (location === '/error') { state.raven.captureException(err); const duration = Date.now() - start; metrics.stoppedDownload({ size, duration, password_protected: file.requiresPassword }); } emitter.emit('pushState', location); } } finally { openLinksInNewTab(links, false); } }); emitter.on('copy', ({ url }) => { copyToClipboard(url); // metrics.copiedLink({ location }); }); setInterval(() => { // poll for updates of the upload list if (state.route === '/') { checkFiles(); } }, 2 * 60 * 1000); setInterval(() => { // poll for rerendering the file list countdown timers if ( state.route === '/' && state.storage.files.length > 0 && Date.now() - lastRender > 30000 ) { render(); } }, 60000); }