Skip to content

Commit

Permalink
Merge pull request #1754 from cozy/feat/viewer-mobile
Browse files Browse the repository at this point in the history
Add PdfMobileViewer on Viewer
  • Loading branch information
JF-Cozy authored Mar 3, 2021
2 parents c780a01 + e025386 commit cbf87c5
Show file tree
Hide file tree
Showing 20 changed files with 427 additions and 63 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"commitlint": "7.6.1",
"commitlint-config-cozy": "0.4.3",
"copyfiles": "2.1.1",
"cozy-client": "^13.4.1",
"cozy-client": "^18.1.2",
"cozy-device-helper": "1.10.0",
"cozy-doctypes": "^1.69.0",
"css-loader": "0.28.11",
Expand Down Expand Up @@ -158,7 +158,7 @@
},
"peerDependencies": {
"@material-ui/core": "4",
"cozy-client": "^13.4.0",
"cozy-client": "^18.1.2",
"cozy-device-helper": "1.10.0",
"cozy-doctypes": "^1.69.0",
"piwik-react-router": "^0.8.2",
Expand Down
14 changes: 7 additions & 7 deletions react/Viewer/ImageLoader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const LOADING_FALLBACK = 'LOADING_FALLBACK'
const LOADED = 'LOADED'
const FAILED = 'FAILED'

class ImageLoader extends React.Component {
export class ImageLoader extends React.Component {
state = {
status: PENDING,
src: null
Expand Down Expand Up @@ -82,13 +82,13 @@ class ImageLoader extends React.Component {

async loadLink() {
this.setState({ status: LOADING_LINK })
const { file, size, client } = this.props
const { file, linkType, client } = this.props

try {
const links = await this.getFileLinks(file, size)
const link = links[size]
const links = await this.getFileLinks(file, linkType)
const link = links[linkType]

if (!link) throw new Error(`${size} link is not available`)
if (!link) throw new Error(`${linkType} link is not available`)

const src = client.getStackClient().uri + link
await this.checkImageSource(src)
Expand Down Expand Up @@ -141,13 +141,13 @@ class ImageLoader extends React.Component {
ImageLoader.propTypes = {
file: PropTypes.object.isRequired,
render: PropTypes.func.isRequired,
size: PropTypes.oneOf(['small', 'medium', 'large']),
linkType: PropTypes.oneOf(['small', 'medium', 'large', 'preview']),
onError: PropTypes.func,
renderFallback: PropTypes.func
}

ImageLoader.defaultProps = {
size: 'small',
linkType: 'small',
onError: () => {}
}

Expand Down
2 changes: 1 addition & 1 deletion react/Viewer/ImageViewer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class ImageViewer extends Component {
{file && (
<ImageLoader
file={file}
size="large"
linkType="large"
onError={this.onImageError}
key={file.id}
render={src => (
Expand Down
2 changes: 1 addition & 1 deletion react/Viewer/NoNetworkViewer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import styles from './styles.styl'
const NoNetworkViewer = ({ t, onReload }) => (
<div className={styles['viewer-canceled']}>
<Icon icon={CloudBrokenIcon} width={160} height={140} />
<h2>{t('Viewer.error')}</h2>
<h2>{t('Viewer.error.network')}</h2>
<Button onClick={onReload} label={t('Viewer.retry')} />
</div>
)
Expand Down
25 changes: 17 additions & 8 deletions react/Viewer/NoViewer/FileIcon.jsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import React from 'react'

import Icon from '../../Icon'
import FileTypeBinIcon from '../../Icons/FileTypeBin'
import FileTypeCodeIcon from '../../Icons/FileTypeCode'
import FileTypeSheetIcon from '../../Icons/FileTypeSheet'
import FileTypeSlideIcon from '../../Icons/FileTypeSlide'
import FileTypeTextIcon from '../../Icons/FileTypeText'
import FileTypeZipIcon from '../../Icons/FileTypeZip'
import FileTypePdfIcon from '../../Icons/FileTypePdf'
import FileTypeFilesIcon from '../../Icons/FileTypeFiles'

const FileIcon = ({ type }) => {
let icon

switch (type) {
case 'bin':
icon = 'file-type-bin'
icon = FileTypeBinIcon
break
case 'code':
icon = 'file-type-code'
icon = FileTypeCodeIcon
break
case 'spreadsheet':
icon = 'file-type-spreadsheet'
icon = FileTypeSheetIcon
break
case 'slide':
icon = 'file-type-slide'
icon = FileTypeSlideIcon
break
case 'text':
icon = 'file-type-text'
icon = FileTypeTextIcon
break
case 'zip':
icon = 'file-type-zip'
icon = FileTypeZipIcon
break
case 'pdf':
icon = 'file-type-pdf'
icon = FileTypePdfIcon
break
default:
icon = 'file-type-files'
icon = FileTypeFilesIcon
break
}

Expand Down
116 changes: 116 additions & 0 deletions react/Viewer/PdfMobileViewer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useState, useEffect, useRef } from 'react'
import has from 'lodash/get'

import { useClient } from 'cozy-client'
import { openFileWith } from 'cozy-client/dist/models/fsnative'
import { isMobileApp } from 'cozy-device-helper'

import Alerter from '../Alerter'
import Spinner from '../Spinner'
import Button from '../Button'

import { withViewerLocales } from './withViewerLocales'
import DownloadButton from './NoViewer/DownloadButton'
import ImageLoader from './ImageLoader'
import NoViewer from './NoViewer'
import NoNetworkViewer from './NoNetworkViewer'

import styles from './styles.styl'

export const PdfMobileViewer = ({ file, t, gestures }) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const imgRef = useRef(null)

const client = useClient()

const reload = () => {
setLoading(true)
setError(false)
}

const onImageError = () => {
setLoading(false)
setError(true)
}

const onImageLoad = () => {
setLoading(false)
}

const onFileOpen = async file => {
try {
await openFileWith(client, file)
} catch (error) {
Alerter.info(`Viewer.error.${error}`, { fileMime: file.mime })
}
}

const handleOnClick = file => {
!isMobileApp() && client.collection('io.cozy.files').download(file)
}

useEffect(() => {
if (gestures && isMobileApp()) {
gestures.get('pinch').set({ enable: true })
gestures.on('tap doubletap pinchend', e => {
if (e.target === imgRef.current) {
onFileOpen(file)
}
})

return () => {
gestures.off('tap doubletap pinchend')
}
}
}, [gestures])

if (error) {
return <NoNetworkViewer onReload={reload} />
}

if (!has(file, 'links.preview')) {
return (
<NoViewer
file={file}
renderFallbackExtraContent={
isMobileApp()
? file => (
<Button
className={styles['viewer-noviewer-download']}
onClick={() => onFileOpen(file)}
label={t('Viewer.openWith')}
/>
)
: file => <DownloadButton file={file} />
}
/>
)
}

return (
<div className={styles['viewer-pdfMobile']}>
{loading && <Spinner size="xxlarge" middle noMargin />}
{file && (
<ImageLoader
file={file}
linkType="preview"
onError={onImageError}
key={file.id}
render={src => (
<img
ref={imgRef}
className={styles['viewer-pdfMobile--image']}
alt={file.name}
src={src}
onLoad={onImageLoad}
onClick={() => handleOnClick(file)}
/>
)}
/>
)}
</div>
)
}

export default withViewerLocales(PdfMobileViewer)
105 changes: 105 additions & 0 deletions react/Viewer/PdfMobileViewer.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react'
import { render, wait } from '@testing-library/react'

import { CozyProvider, createMockClient } from 'cozy-client'
import { isMobileApp } from 'cozy-device-helper'

import { I18n } from '../I18n'

import { PdfMobileViewer } from './PdfMobileViewer'
import { ImageLoader } from './ImageLoader'

jest.mock('cozy-device-helper', () => ({
...jest.requireActual('cozy-device-helper'),
isMobileApp: jest.fn()
}))

const client = createMockClient({})
client.collection = jest.fn(() => ({
getDownloadLinkById: jest.fn()
}))

const file = {
_id: 'pdf',
class: 'pdf',
name: 'Demo.pdf',
mime: 'application/pdf',
links: {
preview: 'https://viewerdemo.cozycloud.cc/IMG_0062.PNG'
}
}

const setup = ({ file }) => {
const root = render(
<CozyProvider client={client}>
<I18n lang="en" dictRequire={() => ''}>
<PdfMobileViewer file={file} t={x => x} />
</I18n>
</CozyProvider>
)

return { root }
}

describe('PdfMobileViewer', () => {
it('should show a spinner if image is not loaded', () => {
const { root } = setup({ file })
const { getByRole } = root

expect(getByRole('progressbar'))
})

describe('error when downloading file', () => {
it('should show network error message on native app and browser', async () => {
ImageLoader.prototype.checkImageSource = jest.fn().mockRejectedValue() // fail to load image

const { root } = setup({ file })
const { queryByRole, getByText } = root

const isMobileAppValues = [true, false]

for (const isMobileAppValue of isMobileAppValues) {
isMobileApp.mockReturnValue(isMobileAppValue)

await wait()

expect(queryByRole('progressbar')).toBeFalsy()
expect(
getByText(
'This file could not be loaded. Do you have a working internet connection right now?'
)
)
}
})
})

describe('error if file as no preview', () => {
let fileWithoutLinks = file

beforeAll(() => {
fileWithoutLinks.links = {}
})

it('should show "download" button on browser', () => {
isMobileApp.mockReturnValue(false)

const { root } = setup({ file: fileWithoutLinks })
const { getByText, queryByRole } = root

expect(queryByRole('progressbar')).toBeFalsy()
expect(getByText('Download'))
expect(getByText(file.name))
})

it('should show "open with" button on native app', () => {
isMobileApp.mockReturnValue(true)

const { root } = setup({ file: fileWithoutLinks })
const { getByText, queryByRole } = root

expect(queryByRole('progressbar')).toBeFalsy()
expect(getByText('Viewer.openWith'))
expect(getByText(file.name))
})
})
})
9 changes: 9 additions & 0 deletions react/Viewer/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ const files = [
name: 'Sample.mp3',
mime: 'audio/mp3'
},
{
_id: 'pdf',
class: 'pdf',
name: 'Demo.pdf',
mime: 'application/pdf',
links: {
preview: 'https://viewerdemo.cozycloud.cc/IMG_0062.PNG'
}
},
{
_id: 'pdf',
class: 'pdf',
Expand Down
Loading

0 comments on commit cbf87c5

Please sign in to comment.