import React from 'react'; import { Lbry, formatCredits, normalizeURI, parseURI } from 'lbry-redux'; import { Lbryio } from 'lbryinc'; import { ActivityIndicator, Alert, DeviceEventEmitter, Dimensions, Image, Linking, NativeModules, Platform, ScrollView, StatusBar, StyleSheet, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native'; import { WebView } from 'react-native-webview'; import { NavigationEvents } from 'react-navigation'; import { navigateBack, navigateToUri, formatBytes, formatLbryUrlForWeb } from 'utils/helper'; import Icon from 'react-native-vector-icons/FontAwesome5'; import ImageViewer from 'react-native-image-zoom-viewer'; import Button from 'component/button'; import EmptyStateView from 'component/emptyStateView'; import Tag from 'component/tag'; import ChannelPage from 'page/channel'; import Colors from 'styles/colors'; import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import DateTime from 'component/dateTime'; import FileDownloadButton from 'component/fileDownloadButton'; import FileItemMedia from 'component/fileItemMedia'; import FilePrice from 'component/filePrice'; import FloatingWalletBalance from 'component/floatingWalletBalance'; import Link from 'component/link'; import MediaPlayer from 'component/mediaPlayer'; import ModalTipView from 'component/modalTipView'; import ProgressCircle from 'react-native-progress-circle'; import RelatedContent from 'component/relatedContent'; import SubscribeButton from 'component/subscribeButton'; import SubscribeNotificationButton from 'component/subscribeNotificationButton'; import UriBar from 'component/uriBar'; import Video from 'react-native-video'; import FileRewardsDriver from 'component/fileRewardsDriver'; import filePageStyle from 'styles/filePage'; import uriBarStyle from 'styles/uriBar'; import RNFS from 'react-native-fs'; import showdown from 'showdown'; import _ from 'lodash'; class FilePage extends React.PureComponent { static navigationOptions = { title: '', }; playerBackground = null; scrollView = null; startTime = null; webView = null; converter = null; linkHandlerScript = `(function () { window.onclick = function(evt) { evt.preventDefault(); window.ReactNativeWebView.postMessage(evt.target.href); evt.stopPropagation(); } }());`; constructor(props) { super(props); this.state = { attemptAutoGet: false, autoOpened: false, autoDownloadStarted: false, autoPlayMedia: false, creditsInputFocused: false, downloadButtonShown: false, downloadPressed: false, didSearchRecommended: false, fileViewLogged: false, fullscreenMode: false, fileGetStarted: false, hasCheckedAllResolved: false, imageUrls: null, isLandscape: false, mediaLoaded: false, pageSuspended: false, relatedContentY: 0, sendTipStarted: false, showDescription: false, showImageViewer: false, showWebView: false, showTipView: false, playbackStarted: false, playerBgHeight: 0, playerHeight: 0, uri: null, uriVars: null, showRecommended: false, stopDownloadConfirmed: false, streamingMode: false, viewCountFetched: false, }; } didFocusListener; componentWillMount() { const { navigation } = this.props; // this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused); } onComponentFocused = () => { StatusBar.setHidden(false); NativeModules.Firebase.setCurrentScreen('File').then(result => { const { setPlayerVisible } = this.props; DeviceEventEmitter.addListener('onDownloadAborted', this.handleDownloadAborted); DeviceEventEmitter.addListener('onStoragePermissionGranted', this.handleStoragePermissionGranted); DeviceEventEmitter.addListener('onStoragePermissionRefused', this.handleStoragePermissionRefused); const { claim, fetchMyClaims, fileInfo, isResolvingUri, resolveUri, navigation } = this.props; const { uri, uriVars } = navigation.state.params; this.setState({ uri, uriVars }); setPlayerVisible(true, uri); if (!isResolvingUri && !claim) resolveUri(uri); this.fetchFileInfo(uri, this.props); this.fetchCostInfo(uri, this.props); fetchMyClaims(); NativeModules.Firebase.track('open_file_page', { uri: uri }); NativeModules.UtilityModule.keepAwakeOn(); }); }; componentDidMount() { this.onComponentFocused(); } difference = (object, base) => { function changes(object, base) { return _.transform(object, function(result, value, key) { if (!_.isEqual(value, base[key])) { result[key] = _.isObject(value) && _.isObject(base[key]) ? changes(value, base[key]) : value; } }); } return changes(object, base); }; componentWillReceiveProps(nextProps) { const { claim, currentRoute, failedPurchaseUris: prevFailedPurchaseUris, fetchViewCount, purchasedUris: prevPurchasedUris, purchaseUriErrorMessage: prevPurchaseUriErrorMessage, navigation, contentType, notify, drawerStack: prevDrawerStack, } = this.props; const { currentRoute: prevRoute, failedPurchaseUris, fileInfo, purchasedUris, purchaseUriErrorMessage, streamingUrl, drawerStack, resolveUris, } = nextProps; const uri = this.getPurchaseUrl(); if (Constants.ROUTE_FILE === currentRoute && currentRoute !== prevRoute) { this.onComponentFocused(); } if (failedPurchaseUris.includes(uri) && prevPurchaseUriErrorMessage !== purchaseUriErrorMessage) { if (purchaseUriErrorMessage && purchaseUriErrorMessage.trim().length > 0) { notify({ message: purchaseUriErrorMessage, isError: true }); } this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false, showRecommended: true }); } const mediaType = Lbry.getMediaType(contentType); const isPlayable = mediaType === 'video' || mediaType === 'audio'; if (this.state.fileGetStarted || prevPurchasedUris.length !== purchasedUris.length) { const { permanent_url: permanentUrl, nout, txid } = claim; const outpoint = `${txid}:${nout}`; if (this.state.fileGetStarted) { NativeModules.UtilityModule.queueDownload(outpoint); this.setState({ fileGetStarted: false }); } if (purchasedUris.includes(uri) || purchasedUris.includes(permanentUrl)) { // If the media is playable, file/view will be done in onPlaybackStarted if (!isPlayable && !this.state.fileViewLogged) { this.logFileView(uri, claim); } } NativeModules.UtilityModule.checkDownloads(); } if ((!fileInfo || (fileInfo && !fileInfo.completed)) && !this.state.streamingMode && isPlayable) { if (streamingUrl) { this.setState({ streamingMode: true, currentStreamUrl: streamingUrl }); } else if (fileInfo && fileInfo.streaming_url) { this.setState({ streamingMode: true, currentStreamUrl: fileInfo.streaming_url }); } } if ( prevDrawerStack[prevDrawerStack.length - 1].route === Constants.DRAWER_ROUTE_FILE_VIEW && prevDrawerStack.length !== drawerStack.length ) { this.setState({ downloadPressed: false, showImageViewer: false, showWebView: false, }); } if (claim && !this.state.viewCountFetched) { this.setState({ viewCountFetched: true }, () => fetchViewCount(claim.claim_id)); } } shouldComponentUpdate(nextProps, nextState) { const { fileInfo: prevFileInfo } = this.props; const { fileInfo } = nextProps; const propKeyTriggers = ['balance', 'viewCount', 'isResolvingUri']; const stateKeyTriggers = [ 'downloadPressed', 'fullscreenMode', 'mediaLoaded', 'playerBgHeighht', 'playerHeight', 'relatedY', 'showTipView', 'showImageViewer', 'showWebView', 'showDescription', 'showRecommended', 'uri', ]; for (let i = 0; i < propKeyTriggers.length; i++) { const key = propKeyTriggers[i]; if (this.props[key] !== nextProps[key]) { return true; } } for (let i = 0; i < stateKeyTriggers.length; i++) { const key = stateKeyTriggers[i]; if (this.state[key] !== nextState[key]) { return true; } } if (!prevFileInfo && fileInfo) { return true; } if (prevFileInfo && fileInfo && Object.keys(this.difference(fileInfo, prevFileInfo)).length > 0) { return true; } return false; } componentDidUpdate(prevProps, prevState) { const { claim, contentType, costInfo, fileInfo, isResolvingUri, resolveUri, navigation, title } = this.props; const { uri } = this.state; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); } // Returned to the page. If mediaLoaded, and currentMediaInfo is different, update if (this.state.mediaLoaded && window.currentMediaInfo && window.currentMediaInfo.uri !== this.state.uri) { const { metadata } = this.props; window.currentMediaInfo = { channel: claim ? claim.channel_name : null, title: metadata ? metadata.title : claim.name, uri: this.state.uri, }; } // attempt to retrieve images and html/text automatically once the claim is loaded, and it's free const mediaType = Lbry.getMediaType(contentType); const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isViewable = mediaType === 'image' || mediaType === 'text'; if (claim && costInfo && costInfo.cost === 0 && !this.state.autoGetAttempted && isViewable) { this.setState({ autoGetAttempted: true }, () => this.checkStoragePermissionForDownload()); } if (((costInfo && costInfo.cost > 0) || !isPlayable) && (!fileInfo && !isViewable) && !this.state.showRecommended) { this.setState({ showRecommended: true }); } if ( !fileInfo && !this.state.autoDownloadStarted && claim && costInfo && costInfo.cost === 0 && (isPlayable || (this.state.uriVars && this.state.uriVars.download === 'true')) ) { this.setState({ autoDownloadStarted: true }, () => { if (!isPlayable) { this.checkStoragePermissionForDownload(); } else { this.confirmPurchaseUri(claim.permanent_url, costInfo, !isPlayable); } NativeModules.UtilityModule.checkDownloads(); }); } } fetchFileInfo(uri, props) { if (props.fileInfo === undefined) { props.fetchFileInfo(uri); } } fetchCostInfo(uri, props) { if (props.costInfo === undefined) { props.fetchCostInfo(uri); } } handleFullscreenToggle = isFullscreen => { const { toggleFullscreenMode } = this.props; toggleFullscreenMode(isFullscreen); if (isFullscreen) { // fullscreen, so change orientation to landscape mode NativeModules.ScreenOrientation.lockOrientationLandscape(); // hide the navigation bar (on devices that have the soft navigation bar) NativeModules.UtilityModule.hideNavigationBar(); } else { // Switch back to portrait mode when the media is not fullscreen NativeModules.ScreenOrientation.lockOrientationPortrait(); // show the navigation bar (on devices that have the soft navigation bar) NativeModules.UtilityModule.showNavigationBar(); } this.setState({ fullscreenMode: isFullscreen }); StatusBar.setHidden(isFullscreen); }; onEditPressed = () => { const { claim, navigation } = this.props; navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISH, params: { editMode: true, claimToEdit: claim } }); }; onDeletePressed = () => { const { abandonClaim, claim, deleteFile, deletePurchasedUri, myClaimUris, fileInfo, navigation } = this.props; Alert.alert( __('Delete file'), __('Are you sure you want to remove this file from your device?'), [ { text: __('No') }, { text: __('Yes'), onPress: () => { const { uri } = navigation.state.params; const purchaseUrl = this.getPurchaseUrl(); deleteFile(`${claim.txid}:${claim.nout}`, true); deletePurchasedUri(uri); NativeModules.UtilityModule.deleteDownload(normalizeURI(purchaseUrl)); this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false, stopDownloadConfirmed: false, }); if (claim) { const fullUri = normalizeURI(`${claim.name}#${claim.claim_id}`); const ownedClaim = myClaimUris.includes(fullUri); if (ownedClaim) { const { txid, nout } = claim; abandonClaim(txid, nout); navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISHES }); } } }, }, ], { cancelable: true }, ); }; onStopDownloadPressed = () => { const { deletePurchasedUri, fileInfo, navigation, notify, stopDownload } = this.props; Alert.alert( __('Stop download'), __('Are you sure you want to stop downloading this file?'), [ { text: __('No') }, { text: __('Yes'), onPress: () => { const uri = this.getPurchaseUrl(); stopDownload(uri, fileInfo); deletePurchasedUri(uri); NativeModules.UtilityModule.deleteDownload(normalizeURI(uri)); this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false, stopDownloadConfirmed: true, }); // there can be a bit of lag between the user pressing Yes and the UI being updated // after the file_set_status and file_delete operations, so let the user know notify({ message: __('The download will stop momentarily. You do not need to wait to discover something else.'), }); }, }, ], { cancelable: true }, ); }; componentWillUnmount() { StatusBar.setHidden(false); if (NativeModules.ScreenOrientation) { NativeModules.ScreenOrientation.unlockOrientation(); } if (NativeModules.UtilityModule) { const utility = NativeModules.UtilityModule; utility.keepAwakeOff(); utility.showNavigationBar(); } if (this.didFocusListener) { this.didFocusListener.remove(); } if (window.currentMediaInfo) { window.currentMediaInfo = null; } DeviceEventEmitter.removeListener('onDownloadAborted', this.handleDownloadAborted); DeviceEventEmitter.removeListener('onStoragePermissionGranted', this.handleStoragePermissionGranted); DeviceEventEmitter.removeListener('onStoragePermissionRefused', this.handleStoragePermissionRefused); } handleDownloadAborted = evt => { const { deletePurchasedUri, fileInfo, stopDownload } = this.props; const { uri, outpoint } = evt; const purchaseUrl = normalizeURI(this.getPurchaseUrl()); if (purchaseUrl === uri) { stopDownload(uri, fileInfo); deletePurchasedUri(uri); NativeModules.UtilityModule.deleteDownload(normalizeURI(uri)); this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false, stopDownloadConfirmed: true, }); } }; handleStoragePermissionGranted = () => { // permission was allowed. proceed to download const { notify } = this.props; // update the configured download folder and then download NativeModules.UtilityModule.getDownloadDirectory().then(downloadDirectory => { Lbry.settings_set({ key: 'download_dir', value: downloadDirectory, }) .then(() => this.performDownload()) .catch(() => { notify({ message: __('The file could not be downloaded to the default download directory.'), isError: true }); }); }); }; handleStoragePermissionRefused = () => { const { notify } = this.props; this.setState({ downloadPressed: false }); notify({ message: __('The file could not be downloaded because the permission to write to storage was not granted.'), isError: true, }); }; localUriForFileInfo = fileInfo => { if (!fileInfo) { return null; } return 'file://' + fileInfo.download_path; }; playerUriForFileInfo = fileInfo => { const { streamingUrl } = this.props; if (!this.state.streamingMode && fileInfo && fileInfo.download_path && fileInfo.completed) { // take streamingMode in the state into account because if the download completes while // the media is already streaming, it will restart from the beginning return this.getEncodedDownloadPath(fileInfo); } if (streamingUrl) { return streamingUrl; } if (this.state.currentStreamUrl) { return this.state.currentStreamUrl; } return null; }; getEncodedDownloadPath = fileInfo => { if (this.state.encodedFilePath) { return this.state.encodedFilePath; } const { file_name: fileName } = fileInfo; const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21'); const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName); return encodedFilePath; }; linkify = text => { let linkifiedContent = []; let lines = text.split(/\n/g); linkifiedContent = lines.map((line, i) => { let tokens = line.split(/\s/g); let lineContent = tokens.length === 0 ? '' : tokens.map((token, j) => { let hasSpace = j !== tokens.length - 1; let space = hasSpace ? ' ' : ''; if (token.match(/^(lbry|https?):\/\//g)) { return ( <Link key={j} style={filePageStyle.link} href={token} text={token} effectOnTap={filePageStyle.linkTapped} /> ); } else { return token + space; } }); lineContent.push('\n'); return <Text key={i}>{lineContent}</Text>; }); return linkifiedContent; }; checkOrientation = () => { if (this.state.fullscreenMode) { return; } const screenDimension = Dimensions.get('window'); const screenWidth = screenDimension.width; const screenHeight = screenDimension.height; const isLandscape = screenWidth > screenHeight; this.setState({ isLandscape }); if (!this.playerBackground) { return; } if (isLandscape) { this.playerBackground.setNativeProps({ height: screenHeight - StyleSheet.flatten(uriBarStyle.uriContainer).height, }); } else if (this.state.playerBgHeight > 0) { this.playerBackground.setNativeProps({ height: this.state.playerBgHeight }); } }; onMediaLoaded = (channelName, title, uri) => { this.setState({ mediaLoaded: true }); window.currentMediaInfo = { channel: channelName, title, uri }; }; onPlaybackStarted = () => { let timeToStartMillis, timeToStart; if (this.startTime) { timeToStartMillis = Date.now() - this.startTime; timeToStart = Math.ceil(timeToStartMillis / 1000); this.startTime = null; } const { claim, navigation } = this.props; const { uri } = navigation.state.params; this.logFileView(uri, claim, timeToStartMillis); let payload = { uri: uri }; if (!isNaN(timeToStart)) { payload['time_to_start_seconds'] = timeToStart; payload['time_to_start_ms'] = timeToStartMillis; } NativeModules.Firebase.track('play', payload); // only fetch recommended content after playback has started this.setState({ playbackStarted: true, showRecommended: true }); }; onPlaybackFinished = () => { if (this.scrollView && this.state.relatedContentY) { this.scrollView.scrollTo({ x: 0, y: this.state.relatedContentY, animated: true }); } }; setRelatedContentPosition = evt => { if (!this.state.relatedContentY) { this.setState({ relatedContentY: evt.nativeEvent.layout.y }); } }; logFileView = (uri, claim, timeToStart) => { if (!claim) { return; } const { claimEligibleRewards } = this.props; const { nout, claim_id: claimId, txid } = claim; const outpoint = `${txid}:${nout}`; const params = { uri, outpoint, claim_id: claimId, }; if (!isNaN(timeToStart)) { params.time_to_start = timeToStart; } Lbryio.call('file', 'view', params) .then(() => claimEligibleRewards()) .catch(() => {}); this.setState({ fileViewLogged: true }); }; handleSharePress = () => { const { claim, notify } = this.props; if (claim) { const { canonical_url: canonicalUrl, short_url: shortUrl, permanent_url: permanentUrl } = claim; const url = Constants.SHARE_BASE_URL + formatLbryUrlForWeb(canonicalUrl || shortUrl || permanentUrl); NativeModules.UtilityModule.shareUrl(url); } }; renderTags = tags => { const { navigation } = this.props; return tags.map((tag, i) => ( <Tag style={filePageStyle.tagItem} key={`${tag}-${i}`} name={tag} navigation={navigation} /> )); }; confirmPurchaseUri = (uri, costInfo, download) => { const { notify, purchaseUri, title } = this.props; if (!costInfo) { notify({ message: __('This content cannot be viewed at this time. Please try again in a bit.'), isError: true }); this.setState({ downloadPressed: false }); this.fetchCostInfo(uri, this.props); return; } const { cost } = costInfo; if (costInfo.cost > 0) { Alert.alert( __('Confirm Purchase'), __( cost === 1 ? 'This will purchase "%title%" for %amount% credit' : 'This will purchase "%title%" for %amount% credits', { title, amount: cost }, ), [ { text: __('OK'), onPress: () => purchaseUri(uri, costInfo, download), }, { text: __('Cancel') }, ], ); } else { // Free content. Just call purchaseUri directly. purchaseUri(uri, costInfo, download); } }; onFileDownloadButtonPressed = () => { const { claim, costInfo, contentType, purchaseUri, setPlayerVisible } = this.props; const mediaType = Lbry.getMediaType(contentType); const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isViewable = mediaType === 'image' || mediaType === 'text'; const purchaseUrl = this.getPurchaseUrl(); NativeModules.Firebase.track('purchase_uri', { uri: purchaseUrl }); if (!isPlayable) { this.onDownloadPressed(); } else { this.confirmPurchaseUri(purchaseUrl, costInfo, !isPlayable); } if (isPlayable) { this.startTime = Date.now(); this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false }); } if (isViewable) { this.setState({ downloadPressed: true }); } }; getPurchaseUrl = () => { const { claim, navigation } = this.props; const permanentUrl = claim ? claim.permanent_url : null; let purchaseUrl; if (navigation.state.params) { const { uri, fullUri } = navigation.state.params; purchaseUrl = fullUri || permanentUrl || uri; } if (!purchaseUrl && permanentUrl) { purchaseUrl = permanentUrl; } return purchaseUrl; }; onDownloadPressed = () => { const { claim, title } = this.props; const fileSize = claim && claim.value && claim.value.source ? claim.value.source.size : 0; Alert.alert( __('Download file'), fileSize > 0 ? __('Save "%title%" (%size%) to your device', { title, size: formatBytes(fileSize, 0) }) : __('Save "%title%" to your device', { title }), [ { text: __('No') }, { text: __('Yes'), onPress: () => { this.checkStoragePermissionForDownload(); }, }, ], { cancelable: true }, ); }; checkStoragePermissionForDownload = () => { // check if we the permission to write to external storage has been granted NativeModules.UtilityModule.canReadWriteStorage().then(canReadWrite => { if (!canReadWrite) { // request permission NativeModules.UtilityModule.requestStoragePermission(); } else { this.performDownload(); } }); }; performDownload = () => { const { claim, costInfo, fileGet, fileInfo, purchasedUris } = this.props; this.setState( { downloadPressed: true, autoPlayMedia: false, stopDownloadConfirmed: false, }, () => { const url = this.getPurchaseUrl(); if (fileInfo || purchasedUris.includes(url)) { // file already in library or URI already purchased, use fileGet directly this.setState({ fileGetStarted: true }, () => fileGet(url, true)); } else { this.confirmPurchaseUri(url, costInfo, true); } NativeModules.UtilityModule.checkDownloads(); }, ); }; onBackButtonPressed = () => { const { navigation, drawerStack, popDrawerStack, setPlayerVisible } = this.props; navigateBack(navigation, drawerStack, popDrawerStack, setPlayerVisible); }; onOpenFilePressed = () => { const { contentType, fileInfo, notify } = this.props; const localFileUri = this.localUriForFileInfo(fileInfo); const mediaType = Lbry.getMediaType(contentType); const isViewable = mediaType === 'image' || mediaType === 'text'; const isPlayable = mediaType === 'video' || mediaType === 'audio'; if (isViewable) { this.openFile(localFileUri, mediaType, contentType); } else if (isPlayable) { notify({ message: __('Please press the Play button.') }); } else { notify({ message: __('This file cannot be displayed in the LBRY app.') }); } }; openFile = (localFileUri, mediaType, contentType) => { const { pushDrawerStack } = this.props; const isWebViewable = mediaType === 'text'; if (mediaType === 'image') { // use image viewer if (!this.state.showImageViewer) { this.setState( { imageUrls: [ { url: localFileUri, }, ], showImageViewer: true, showRecommended: true, }, () => pushDrawerStack(Constants.DRAWER_ROUTE_FILE_VIEW), ); } } if (isWebViewable) { // show webview if (!this.state.showWebView) { this.setState( { showWebView: true, showRecommended: true, }, () => { pushDrawerStack(Constants.DRAWER_ROUTE_FILE_VIEW); }, ); } } }; handleWebViewLoad = () => { const { contentType, fileInfo } = this.props; const localFileUri = this.localUriForFileInfo(fileInfo); if (this.webView && ['text/markdown', 'text/md'].includes(contentType)) { RNFS.readFile(localFileUri, 'utf8').then(markdown => { if (this.webView) { if (!this.converter) { this.converter = new showdown.Converter(); } const html = this.converter.makeHtml(markdown); this.webView.injectJavaScript( 'document.getElementById("content").innerHTML = \'' + html.replace(/\n/g, '').replace(/'/g, "\\'") + "'; true;", ); } }); } }; handleWebViewMessage = evt => { const href = evt.nativeEvent.data; if (href && href.startsWith('http')) { Linking.openURL(href); } }; buildWebViewSource = () => { const { contentType, fileInfo } = this.props; const localFileUri = this.localUriForFileInfo(fileInfo); if (['text/markdown', 'text/md'].includes(contentType)) { let fontdecl = ''; if (Platform.OS === 'android') { fontdecl = ` @font-face { font-family: 'Inter'; src: url('file:///android_asset/fonts/Inter-Regular.otf'); font-weight: normal; } @font-face { font-family: 'Inter; src: url('file:///android_asset/fonts/Inter-Bold.otf'); font-weight: bold; } `; } const html = ` <!doctype html> <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, user-scalable=no"/> <style type="text/css"> ${fontdecl} body { font-family: 'Inter', sans-serif; margin: 16px } img { width: 100%; } </style> </head> <body> <div id="content"></div> </body> </html> `; return { html }; } return { uri: localFileUri }; }; render() { const { balance, claim, channels, channelUri, costInfo, fileInfo, metadata, contentType, tab, rewardedContentClaimIds, isPlayerVisible, isResolvingUri, blackListedOutpoints, myClaimUris, navigation, position, purchaseUri, pushDrawerStack, setPlayerVisible, thumbnail, title, viewCount, } = this.props; const { uri, autoplay } = navigation.state.params; const { isChannel } = parseURI(uri); const myChannelUris = channels ? channels.map(channel => channel.permanent_url) : []; const ownedClaim = myClaimUris.includes(uri) || myChannelUris.includes(uri); let innerContent = null; if ((isResolvingUri && !claim) || !claim) { return ( <View style={filePageStyle.pageContainer}> <UriBar value={uri} navigation={navigation} /> {isResolvingUri && ( <View style={filePageStyle.busyContainer}> <ActivityIndicator size="large" color={Colors.NextLbryGreen} /> <Text style={filePageStyle.infoText}>{__('Loading decentralized data...')}</Text> </View> )} {claim === null && !isResolvingUri && ( <View style={filePageStyle.container}> {ownedClaim && ( <EmptyStateView message={ isChannel ? __('It looks like you just created this channel. It will appear in a few minutes.') : __('It looks you just published this content. It will appear in a few minutes.') } /> )} {!ownedClaim && ( <EmptyStateView message={__("There's nothing at this location.")} buttonText={__('Publish something here')} onButtonPress={() => navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISH, params: { vanityUrl: uri.trim() }, }) } /> )} </View> )} <FloatingWalletBalance navigation={navigation} /> </View> ); } let isClaimBlackListed = false; if (blackListedOutpoints) { for (let i = 0; i < blackListedOutpoints.length; i += 1) { const outpoint = blackListedOutpoints[i]; if (outpoint.txid === claim.txid && outpoint.nout === claim.nout) { isClaimBlackListed = true; break; } } } if (isClaimBlackListed) { innerContent = ( <View style={filePageStyle.dmcaContainer}> <Text style={filePageStyle.dmcaText}> {__( 'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.', )} </Text> <Link style={filePageStyle.dmcaLink} href="https://lbry.com/faq/dmca" text={__('Read More')} /> </View> ); } let tags = []; if (claim && claim.value && claim.value.tags) { tags = claim.value.tags; } const completed = fileInfo && fileInfo.completed; const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const description = metadata.description ? metadata.description : null; const mediaType = Lbry.getMediaType(contentType); const isPlayable = mediaType === 'video' || mediaType === 'audio'; const isWebViewable = mediaType === 'text'; const { height, signing_channel: signingChannel, value } = claim; const channelName = signingChannel && signingChannel.name; const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id; const fullUri = `${claim.name}#${claim.claim_id}`; const canEdit = myClaimUris.includes(normalizeURI(fullUri)); const showActions = (canEdit || (fileInfo && fileInfo.download_path)) && !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView; const showFileActions = canEdit || (fileInfo && fileInfo.download_path && (completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes))); const fullChannelUri = channelClaimId && channelClaimId.trim().length > 0 ? normalizeURI(`${channelName}#${channelClaimId}`) : normalizeURI(channelName); const shortChannelUri = signingChannel ? signingChannel.short_url : null; const playerStyle = [ filePageStyle.player, this.state.isLandscape ? filePageStyle.containedPlayerLandscape : this.state.fullscreenMode ? filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer, ]; const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground]; const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground]; // at least 2MB (or the full download) before media can be loaded const canLoadMedia = this.state.streamingMode || (fileInfo && (fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes === fileInfo.total_bytes)); // 2MB = 1024*1024*2 const duration = claim && claim.value && claim.value.video ? claim.value.video.duration : null; const isViewable = mediaType === 'image' || mediaType === 'text'; const canOpen = isViewable && completed; const localFileUri = this.localUriForFileInfo(fileInfo); const unsupported = !isPlayable && !canOpen; if (this.state.downloadPressed && canOpen && !this.state.autoOpened) { // automatically open a web viewable or image file after the download button is pressed this.setState({ autoOpened: true }, () => this.openFile(localFileUri, mediaType, contentType)); } if (isChannel) { return <ChannelPage uri={uri} navigation={navigation} />; } return ( <View style={filePageStyle.pageContainer}> {!this.state.fullscreenMode && <UriBar value={uri} navigation={navigation} />} {innerContent} {this.state.showWebView && isWebViewable && ( <WebView ref={ref => { this.webView = ref; }} allowFileAccess javaScriptEnabled originWhiteList={['*']} source={this.buildWebViewSource()} style={filePageStyle.viewer} onLoad={this.handleWebViewLoad} injectedJavaScript={this.linkHandlerScript} onMessage={this.handleWebViewMessage} /> )} {this.state.showImageViewer && ( <ImageViewer style={StyleSheet.flatten(filePageStyle.viewer)} imageUrls={this.state.imageUrls} renderIndicator={() => null} /> )} {!innerContent && !this.state.showWebView && ( <View style={ this.state.fullscreenMode ? filePageStyle.innerPageContainerFsMode : filePageStyle.innerPageContainer } onLayout={this.checkOrientation} > <TouchableOpacity activeOpacity={0.5} style={filePageStyle.mediaContainer} onPress={this.onFileDownloadButtonPressed} > {(canOpen || (!fileInfo || (isPlayable && !canLoadMedia)) || (!canOpen && fileInfo)) && ( <FileItemMedia duration={duration} style={filePageStyle.thumbnail} title={title} thumbnail={thumbnail} /> )} {!unsupported && (!this.state.downloadButtonShown || this.state.downloadPressed) && !this.state.mediaLoaded && ( <ActivityIndicator size="large" color={Colors.NextLbryGreen} style={filePageStyle.loading} /> )} {unsupported && fileInfo && completed && ( <View style={filePageStyle.unsupportedContent}> <Image style={filePageStyle.unsupportedContentImage} resizeMode={'stretch'} source={require('../../assets/gerbil-happy.png')} /> <View style={filePageStyle.unspportedContentTextContainer}> <Text style={filePageStyle.unsupportedContentTitle}>{__('Unsupported Content')}</Text> <Text style={filePageStyle.unsupportedContentText}> Sorry, we are unable to display this content in the app. You can find the file named{' '} <Text style={filePageStyle.unsupportedContentFilename}>{fileInfo.file_name}</Text> in your downloads folder. </Text> </View> </View> )} {((isPlayable && !completed && !canLoadMedia) || canOpen || (!completed && !this.state.streamingMode)) && ( <FileDownloadButton uri={claim && claim.permanent_url ? claim.permanent_url : uri} style={filePageStyle.downloadButton} openFile={() => this.openFile(localFileUri, mediaType, contentType)} isPlayable={isPlayable} isViewable={isViewable} onFileActionPress={this.onFileDownloadButtonPressed} onButtonLayout={() => this.setState({ downloadButtonShown: true })} /> )} {!fileInfo && ( <FilePrice uri={claim && claim.permanent_url ? claim.permanent_url : uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} iconStyle={filePageStyle.filePriceIcon} /> )} <TouchableOpacity style={filePageStyle.backButton} onPress={this.onBackButtonPressed}> <Icon name={'arrow-left'} size={18} style={filePageStyle.backButtonIcon} /> </TouchableOpacity> </TouchableOpacity> {!innerContent && (this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && ( <View style={playerBgStyle} ref={ref => { this.playerBackground = ref; }} onLayout={evt => { if (!this.state.playerBgHeight) { this.setState({ playerBgHeight: evt.nativeEvent.layout.height }); } }} /> )} {!innerContent && (this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && this.state.fullscreenMode && <View style={fsPlayerBgStyle} />} {isPlayerVisible && !innerContent && (this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && ( <MediaPlayer claim={claim} assignPlayer={ref => { this.player = ref; }} uri={uri} source={this.playerUriForFileInfo(fileInfo)} style={playerStyle} autoPlay onFullscreenToggled={this.handleFullscreenToggle} onLayout={evt => { if (!this.state.playerHeight) { this.setState({ playerHeight: evt.nativeEvent.layout.height }); } }} onMediaLoaded={() => this.onMediaLoaded(channelName, title, uri)} onBackButtonPressed={this.onBackButtonPressed} onPlaybackStarted={this.onPlaybackStarted} onPlaybackFinished={this.onPlaybackFinished} thumbnail={thumbnail} position={position} /> )} {!innerContent && ( <ScrollView style={filePageStyle.scrollContainer} contentContainerstyle={showActions ? null : filePageStyle.scrollContent} keyboardShouldPersistTaps={'handled'} ref={ref => { this.scrollView = ref; }} > <TouchableWithoutFeedback style={filePageStyle.titleTouch} onPress={() => this.setState({ showDescription: !this.state.showDescription })} > <View style={filePageStyle.titleArea}> <View style={filePageStyle.titleRow}> <Text style={filePageStyle.title} selectable> {title} </Text> {isRewardContent && <Icon name="award" style={filePageStyle.rewardIcon} size={16} />} <View style={filePageStyle.descriptionToggle}> <Icon name={this.state.showDescription ? 'caret-up' : 'caret-down'} size={24} /> </View> </View> <Text style={filePageStyle.viewCount}> {viewCount === 1 && __('%view% view', { view: viewCount })} {viewCount > 1 && __('%view% views', { view: viewCount })} </Text> </View> </TouchableWithoutFeedback> <View style={filePageStyle.largeButtonsRow}> <TouchableOpacity style={filePageStyle.largeButton} onPress={this.handleSharePress}> <Icon name={'share-alt'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Share')}</Text> </TouchableOpacity> <TouchableOpacity style={filePageStyle.largeButton} onPress={() => this.setState({ showTipView: true })} > <Icon name={'gift'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Tip')}</Text> </TouchableOpacity> {!canEdit && ( <View style={filePageStyle.sharedLargeButton}> {!this.state.downloadPressed && (!fileInfo || !fileInfo.download_path || (fileInfo.written_bytes <= 0 && !completed)) && ( <TouchableOpacity style={filePageStyle.innerLargeButton} onPress={this.onDownloadPressed}> <Icon name={'download'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Download')}</Text> </TouchableOpacity> )} {this.state.downloadPressed && (!fileInfo || fileInfo.written_bytes === 0) && ( <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} /> )} {!completed && fileInfo && fileInfo.written_bytes > 0 && fileInfo.written_bytes < fileInfo.total_bytes && !this.state.stopDownloadConfirmed && ( <TouchableOpacity style={filePageStyle.innerLargeButton} onPress={this.onStopDownloadPressed}> <ProgressCircle percent={(fileInfo.written_bytes / fileInfo.total_bytes) * 100} radius={9} borderWidth={2} shadowColor={Colors.ActionGrey} color={Colors.NextLbryGreen} > <Icon name={'stop'} size={6} style={filePageStyle.largeButtonIcon} /> </ProgressCircle> <Text style={filePageStyle.largeButtonText}>{__('Stop')}</Text> </TouchableOpacity> )} {completed && fileInfo && ( <TouchableOpacity style={filePageStyle.innerLargeButton} onPress={this.onOpenFilePressed}> <Icon name={'folder-open'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Open')}</Text> </TouchableOpacity> )} </View> )} {!canEdit && ( <TouchableOpacity style={filePageStyle.largeButton} onPress={() => Linking.openURL(`https://lbry.com/dmca/${claim.claim_id}`)} > <Icon name={'flag'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Report')}</Text> </TouchableOpacity> )} {canEdit && ( <TouchableOpacity style={filePageStyle.largeButton} onPress={this.onEditPressed}> <Icon name={'edit'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Edit')}</Text> </TouchableOpacity> )} {(completed || canEdit) && ( <TouchableOpacity style={filePageStyle.largeButton} onPress={this.onDeletePressed}> <Icon name={'trash-alt'} size={16} style={filePageStyle.largeButtonIcon} /> <Text style={filePageStyle.largeButtonText}>{__('Delete')}</Text> </TouchableOpacity> )} </View> <View style={filePageStyle.channelRow}> <View style={filePageStyle.publishInfo}> {channelName && ( <Link style={filePageStyle.channelName} selectable text={channelName} numberOfLines={1} ellipsizeMode={'tail'} onPress={() => { navigateToUri( navigation, normalizeURI(shortChannelUri || fullChannelUri), null, false, fullChannelUri, setPlayerVisible, ); }} /> )} {!channelName && ( <Text style={filePageStyle.anonChannelName} selectable ellipsizeMode={'tail'}> {__('Anonymous')} </Text> )} <DateTime style={filePageStyle.publishDate} textStyle={filePageStyle.publishDateText} uri={fullUri} formatOptions={{ day: 'numeric', month: 'long', year: 'numeric' }} show={DateTime.SHOW_DATE} /> </View> <View style={filePageStyle.subscriptionRow}> {channelName && ( <SubscribeButton style={filePageStyle.actionButton} uri={fullChannelUri} name={channelName} hideText={false} /> )} {false && channelName && ( <SubscribeNotificationButton style={[filePageStyle.actionButton, filePageStyle.bellButton]} uri={fullChannelUri} name={channelName} /> )} </View> </View> {this.state.showDescription && description && description.length > 0 && ( <View style={filePageStyle.divider} /> )} {this.state.showDescription && description && ( <View> <Text style={filePageStyle.description} selectable> {this.linkify(description)} </Text> {tags && tags.length > 0 && ( <View style={filePageStyle.tagContainer}> <Text style={filePageStyle.tagTitle}>{__('Tags')}</Text> <View style={filePageStyle.tagList}>{this.renderTags(tags)}</View> </View> )} </View> )} {costInfo && parseFloat(costInfo.cost) > balance && !fileInfo && ( <FileRewardsDriver navigation={navigation} /> )} <View onLayout={this.setRelatedContentPosition} /> {this.state.showRecommended && ( <RelatedContent navigation={navigation} claimId={claim.claim_id} title={title} uri={fullUri} fullUri={fullUri} /> )} </ScrollView> )} </View> )} {this.state.showTipView && ( <ModalTipView claim={claim} channelName={channelName} contentName={title} onCancelPress={() => this.setState({ showTipView: false })} onOverlayPress={() => this.setState({ showTipView: false })} onSendTipSuccessful={() => this.setState({ showTipView: false })} /> )} {!this.state.fullscreenMode && !this.state.showTipView && !this.state.showImageViewer && !this.state.showWebView && <FloatingWalletBalance navigation={navigation} />} </View> ); } } export default FilePage;