import React from 'react'; import PropTypes from 'prop-types'; import { View, Text, FlatList, Keyboard, BackHandler } from 'react-native'; import { SafeAreaView } from 'react-navigation'; import ShareExtension from 'rn-extensions-share'; import { connect } from 'react-redux'; import RNFetchBlob from 'rn-fetch-blob'; import * as mime from 'react-native-mime-types'; import { isEqual, orderBy } from 'lodash'; import { Q } from '@nozbe/watermelondb'; import Navigation from '../../lib/ShareNavigation'; import database from '../../lib/database'; import { isIOS, isAndroid } from '../../utils/deviceInfo'; import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; import log from '../../utils/log'; import { canUploadFile } from '../../utils/media'; import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem'; import ServerItem from '../../presentation/ServerItem'; import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import ShareListHeader from './Header'; import ActivityIndicator from '../../containers/ActivityIndicator'; import styles from './styles'; import StatusBar from '../../containers/StatusBar'; import { themes } from '../../constants/colors'; import { animateNextTransition } from '../../utils/layoutAnimation'; import { withTheme } from '../../theme'; import { themedHeader } from '../../utils/navigation'; const LIMIT = 50; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const keyExtractor = item => item.rid; class ShareListView extends React.Component { static navigationOptions = ({ navigation, screenProps }) => { const searching = navigation.getParam('searching'); const initSearch = navigation.getParam('initSearch', () => {}); const cancelSearch = navigation.getParam('cancelSearch', () => {}); const search = navigation.getParam('search', () => {}); if (isIOS) { return { headerStyle: { backgroundColor: themes[screenProps.theme].headerBackground }, headerTitle: ( <ShareListHeader searching={searching} initSearch={initSearch} cancelSearch={cancelSearch} search={search} theme={screenProps.theme} /> ) }; } return { ...themedHeader(screenProps.theme), headerLeft: searching ? ( <CustomHeaderButtons left> <Item title='cancel' iconName='cross' onPress={cancelSearch} /> </CustomHeaderButtons> ) : ( <CloseShareExtensionButton onPress={ShareExtension.close} testID='share-extension-close' /> ), headerTitle: <ShareListHeader searching={searching} search={search} theme={screenProps.theme} />, headerRight: ( searching ? null : ( <CustomHeaderButtons> {isAndroid ? <Item title='search' iconName='magnifier' onPress={initSearch} /> : null} </CustomHeaderButtons> ) ) }; } static propTypes = { navigation: PropTypes.object, server: PropTypes.string, token: PropTypes.string, userId: PropTypes.string, theme: PropTypes.string } constructor(props) { super(props); this.data = []; this.state = { showError: false, searching: false, searchText: '', value: '', isMedia: false, mediaLoading: false, fileInfo: null, searchResults: [], chats: [], servers: [], loading: true, serverInfo: null }; this.didFocusListener = props.navigation.addListener('didFocus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)); this.willBlurListener = props.navigation.addListener('willBlur', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)); } componentDidMount() { const { navigation, server } = this.props; navigation.setParams({ initSearch: this.initSearch, cancelSearch: this.cancelSearch, search: this.search }); setTimeout(async() => { try { const { value, type } = await ShareExtension.data(); let fileInfo = null; const isMedia = (type === 'media'); if (isMedia) { this.setState({ mediaLoading: true }); const data = await RNFetchBlob.fs.stat(this.uriToPath(value)); fileInfo = { name: data.filename, description: '', size: data.size, mime: mime.lookup(data.path), path: isIOS ? data.path : `file://${ data.path }` }; } this.setState({ value, fileInfo, isMedia, mediaLoading: false }); } catch (e) { log(e); this.setState({ mediaLoading: false }); } this.getSubscriptions(server); }, 500); } componentWillReceiveProps(nextProps) { const { server } = this.props; if (nextProps.server !== server) { this.getSubscriptions(nextProps.server); } } shouldComponentUpdate(nextProps, nextState) { const { searching } = this.state; if (nextState.searching !== searching) { return true; } const { isMedia } = this.state; if (nextState.isMedia !== isMedia) { this.getSubscriptions(nextProps.server, nextState.fileInfo); return true; } const { server, theme } = this.props; if (server !== nextProps.server) { return true; } if (theme !== nextProps.theme) { return true; } const { searchResults } = this.state; if (!isEqual(nextState.searchResults, searchResults)) { return true; } return false; } // eslint-disable-next-line react/sort-comp internalSetState = (...args) => { const { navigation } = this.props; if (navigation.isFocused()) { animateNextTransition(); } this.setState(...args); } getSubscriptions = async(server, fileInfo) => { const { fileInfo: fileData } = this.state; const db = database.active; const serversDB = database.servers; if (server) { this.data = await db.collections .get('subscriptions') .query( Q.where('archived', false), Q.where('open', true) ).fetch(); this.data = orderBy(this.data, ['roomUpdatedAt'], ['desc']); const serversCollection = serversDB.collections.get('servers'); this.servers = await serversCollection.query().fetch(); this.chats = this.data.slice(0, LIMIT); let serverInfo = {}; try { serverInfo = await serversCollection.find(server); } catch (error) { // Do nothing } const canUploadFileResult = canUploadFile(fileInfo || fileData, serverInfo); this.internalSetState({ chats: this.chats ? this.chats.slice() : [], servers: this.servers ? this.servers.slice() : [], loading: false, showError: !canUploadFileResult.success, error: canUploadFileResult.error, serverInfo }); this.forceUpdate(); } }; uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); getRoomTitle = (item) => { const { serverInfo } = this.state; const { useRealName } = serverInfo; return ((item.prid || useRealName) && item.fname) || item.name; } shareMessage = (item) => { const { value, isMedia, fileInfo } = this.state; const { navigation } = this.props; navigation.navigate('ShareView', { rid: item.rid, value, isMedia, fileInfo, name: this.getRoomTitle(item) }); } search = (text) => { const result = this.data.filter(item => item.name.includes(text)) || []; this.internalSetState({ searchResults: result.slice(0, LIMIT), searchText: text }); } initSearch = () => { const { chats } = this.state; const { navigation } = this.props; this.setState({ searching: true, searchResults: chats }); navigation.setParams({ searching: true }); } cancelSearch = () => { const { navigation } = this.props; this.internalSetState({ searching: false, searchResults: [], searchText: '' }); navigation.setParams({ searching: false }); Keyboard.dismiss(); } handleBackPress = () => { const { searching } = this.state; if (searching) { this.cancelSearch(); return true; } return false; } renderSectionHeader = (header) => { const { searching } = this.state; const { theme } = this.props; if (searching) { return null; } return ( <View style={[styles.headerContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> <Text style={[styles.headerText, { color: themes[theme].titleText }]}> {I18n.t(header)} </Text> </View> ); } renderItem = ({ item }) => { const { userId, token, server, theme } = this.props; return ( <DirectoryItem user={{ id: userId, token }} title={this.getRoomTitle(item)} baseUrl={server} avatar={this.getRoomTitle(item)} description={ item.t === 'c' ? (item.topic || item.description) : item.fname } type={item.t} onPress={() => this.shareMessage(item)} testID={`share-extension-item-${ item.name }`} theme={theme} /> ); } renderSeparator = () => { const { theme } = this.props; return <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />; } renderBorderBottom = () => { const { theme } = this.props; return <View style={[styles.borderBottom, { borderColor: themes[theme].separatorColor }]} />; } renderSelectServer = () => { const { servers } = this.state; const { server, theme } = this.props; const currentServer = servers.find(serverFiltered => serverFiltered.id === server); return currentServer ? ( <> {this.renderSectionHeader('Select_Server')} <View style={[ styles.bordered, { borderColor: themes[theme].separatorColor, backgroundColor: themes[theme].auxiliaryBackground } ]} > <ServerItem server={server} onPress={() => Navigation.navigate('SelectServerView', { servers: this.servers })} item={currentServer} theme={theme} /> </View> </> ) : null; } renderEmptyComponent = () => { const { theme } = this.props; return ( <View style={[styles.container, styles.emptyContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text> </View> ); } renderHeader = () => { const { searching } = this.state; return ( <> { !searching ? ( <> {this.renderSelectServer()} {this.renderSectionHeader('Chats')} </> ) : null } </> ); } renderContent = () => { const { chats, mediaLoading, loading, searchResults, searching, searchText } = this.state; const { theme } = this.props; if (mediaLoading || loading) { return <ActivityIndicator theme={theme} />; } return ( <FlatList data={searching ? searchResults : chats} keyExtractor={keyExtractor} style={[styles.flatlist, { backgroundColor: themes[theme].auxiliaryBackground }]} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} renderItem={this.renderItem} getItemLayout={getItemLayout} ItemSeparatorComponent={this.renderSeparator} ListHeaderComponent={this.renderHeader} ListFooterComponent={!searching && this.renderBorderBottom} ListHeaderComponentStyle={!searching ? { ...styles.borderBottom, borderColor: themes[theme].separatorColor } : {}} ListEmptyComponent={searching && searchText ? this.renderEmptyComponent : null} enableEmptySections removeClippedSubviews keyboardShouldPersistTaps='always' initialNumToRender={12} windowSize={20} /> ); } renderError = () => { const { fileInfo: file, loading, searching, error } = this.state; const { theme } = this.props; if (loading) { return <ActivityIndicator theme={theme} />; } return ( <View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}> { !searching ? ( <> {this.renderSelectServer()} </> ) : null } <View style={[styles.container, styles.centered, { backgroundColor: themes[theme].auxiliaryBackground }]}> <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t(error)}</Text> <CustomIcon name='circle-cross' size={120} color={themes[theme].dangerColor} /> <Text style={[styles.fileMime, { color: themes[theme].titleText }]}>{ file.mime }</Text> </View> </View> ); } render() { const { showError } = this.state; const { theme } = this.props; return ( <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]} forceInset={{ vertical: 'never' }}> <StatusBar theme={theme} /> { showError ? this.renderError() : this.renderContent() } </SafeAreaView> ); } } const mapStateToProps = (({ share }) => ({ userId: share.user && share.user.id, token: share.user && share.user.token, server: share.server })); export default connect(mapStateToProps)(withTheme(ShareListView));