diff --git a/static/app-strings.json b/static/app-strings.json index b0ce17374..f35901ba2 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1215,4 +1215,4 @@ "%duration% minute ago": "%duration% minute ago", "%duration% seconds ago": "%duration% seconds ago", "%duration% second ago": "%duration% second ago" -} \ No newline at end of file +} diff --git a/ui/component/viewers/comicBookViewer.jsx b/ui/component/viewers/comicBookViewer.jsx index fbe21e05e..05ca41c87 100644 --- a/ui/component/viewers/comicBookViewer.jsx +++ b/ui/component/viewers/comicBookViewer.jsx @@ -4,11 +4,11 @@ import Villain from 'villain-react'; import LoadingScreen from 'component/common/loading-screen'; // @if TARGET='web' -import useStream from 'effects/use-stream' +import useStream from 'effects/use-stream'; // @endif // @if TARGET='app' -import useFileStream from 'effects/use-stream-file' +import useFileStream from 'effects/use-stream-file'; // @endif // Import default styles for Villain @@ -29,39 +29,39 @@ if (process.env.NODE_ENV !== 'production') { workerUrl = `/${workerUrl}`; } -const ComicBookViewer = ( props: Props) => { - const { source, theme } = props - const { stream, file } = source +const ComicBookViewer = (props: Props) => { + const { source, theme } = props; + let finalSource; - // @if TARGET='web' - const finalSource = useStream(stream) - // @endif + // @if TARGET='web' + finalSource = useStream(source.stream); + // @endif - // @if TARGET='app' - const finalSource = useFileStream(file) - // @endif + // @if TARGET='app' + finalSource = useFileStream(source.file); + // @endif - const { error, loading, content } = finalSource - - const ready = content !== null && !loading + // Villain options + const opts = { + theme: theme === 'dark' ? 'Dark' : 'Light', + allowFullScreen: true, + autoHideControls: false, + allowGlobalShortcuts: true, + }; - // Villain options - const opts = { - theme: theme === 'dark' ? 'Dark' : 'Light', - allowFullScreen: true, - autoHideControls: false, - allowGlobalShortcuts: true, - }; + const { error, loading, content } = finalSource; + const ready = content !== null && !loading; + const errorMessage = __("Sorry, looks like we can't load the archive."); - const errorMessage = __("Sorry, looks like we can't load the archive."); - - return ( -
- { loading && } - { ready && } - { error && } -
- ); - } + return ( +
+ {loading && } + {ready && ( + + )} + {error && } +
+ ); +}; export default ComicBookViewer; diff --git a/ui/effects/use-is-mounted.js b/ui/effects/use-is-mounted.js new file mode 100644 index 000000000..0337e2b36 --- /dev/null +++ b/ui/effects/use-is-mounted.js @@ -0,0 +1,17 @@ +import React from 'react'; + +// Check if component is mounted, useful to prevent state updates after component unmounted +function useIsMounted() { + const isMounted = React.useRef(true); + + React.useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + // Returning "isMounted.current" wouldn't work because we would return unmutable primitive + return isMounted; +} + +export default useIsMounted; diff --git a/ui/effects/use-stream-file.js b/ui/effects/use-stream-file.js index 0c18fbc27..f86c8fa56 100644 --- a/ui/effects/use-stream-file.js +++ b/ui/effects/use-stream-file.js @@ -1,36 +1,46 @@ -// @flow import React from 'react'; +import useIsMounted from 'effects/use-is-mounted'; // Returns a blob from the download path -export default function useFileStream(fileStream: (?string) => any) { +export default function useFileStream(fileStream) { + const isMounted = useIsMounted(); const [state, setState] = React.useState({ error: false, - content: null, loading: true, + content: null, }); React.useEffect(() => { - if (fileStream) { - let chunks = [] + if (fileStream && isMounted.current) { + let chunks = []; const stream = fileStream(); - + // Handle steam chunk recived stream.on('data', chunk => { - chunks.push(chunk) + if (isMounted.current) { + chunks.push(chunk); + } else { + // Cancel stream if component is not mounted: + // The user has left the viewer page + stream.destroy(); + } }); - + // Handle stream ended stream.on('end', () => { - const buffer = Buffer.concat(chunks) - const blob = new Blob([buffer]) - setState({ content: blob, loading: false }); + if (isMounted.current) { + const buffer = Buffer.concat(chunks); + const blob = new Blob([buffer]); + setState({ content: blob, loading: false }); + } }); - + // Handle stream error stream.on('error', () => { - setState({ error: true, loading: false }); - + if (isMounted.current) { + setState({ error: true, loading: false }); + } }); } - }, []); + }, [fileStream, isMounted]); return state; } diff --git a/ui/effects/use-stream.js b/ui/effects/use-stream.js index d317a2aab..c32fdcaf3 100644 --- a/ui/effects/use-stream.js +++ b/ui/effects/use-stream.js @@ -1,45 +1,56 @@ -// @flow import React from 'react'; import https from 'https'; +import useIsMounted from 'effects/use-is-mounted'; // Returns web blob from the streaming url -export default function useStream(url: (?string) => any) { +export default function useStream(url) { + const isMounted = useIsMounted(); const [state, setState] = React.useState({ error: false, + loading: true, content: null, - loading: false, }); React.useEffect(() => { - if (url) { - let chunks = []; - - // Start loading state - setState({loading: true}) - - https.get( - url, - response => { - if (response.statusCode >= 200 && response.statusCode < 300) { - let chunks = [] - response.on('data', function(chunk) { + if (url && isMounted.current) { + https.get(url, response => { + if (isMounted && response.statusCode >= 200 && response.statusCode < 300) { + let chunks = []; + // Handle stream chunk recived + response.on('data', function(chunk) { + if (isMounted.current) { chunks.push(chunk); - }); - response.on('end', () => { - const buffer = Buffer.concat(chunks) - const blob = new Blob([buffer]) - console.info(response) + } else { + // Cancel stream if component is not mounted: + // The user has left the viewer page + response.destroy(); + } + }); + // Handle stream ended + response.on('end', () => { + if (isMounted.current) { + const buffer = Buffer.concat(chunks); + const blob = new Blob([buffer]); + console.info(response); setState({ content: blob, loading: false }); - }); - } else { - console.info(response) + } + }); + // Handle stream error + response.on('error', () => { + if (isMounted.current) { + setState({ error: true, loading: false }); + } + }); + } else { + // Handle network error + if (isMounted.current) { setState({ error: true, loading: false }); } } - ); + }); } - }, []); + }, [url, isMounted]); return state; }