import React from 'react'; import PropTypes from 'prop-types'; import { View, Text, LayoutAnimation, InteractionManager, FlatList, ScrollView, ActivityIndicator, Keyboard } 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 } from 'lodash'; import Navigation from '../../lib/Navigation'; import database, { safeAddListener } from '../../lib/realm'; import debounce from '../../utils/debounce'; import { isIOS, isAndroid } from '../../utils/deviceInfo'; import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; import log from '../../utils/log'; import { openSearchHeader as openSearchHeaderAction, closeSearchHeader as closeSearchHeaderAction } from '../../actions/rooms'; import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem'; import ServerItem, { ROW_HEIGHT as ROW_HEIGHT_SERVER } from '../../presentation/ServerItem'; import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import SearchBar from '../RoomsListView/ListHeader/SearchBar'; import ShareListHeader from './Header'; import styles from './styles'; const SCROLL_OFFSET = 56; const getItemLayoutChannel = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const getItemLayoutServer = (data, index) => ({ length: ROW_HEIGHT_SERVER, offset: ROW_HEIGHT_SERVER * index, index }); const keyExtractor = item => item.rid; @connect(state => ({ userId: state.login.user && state.login.user.id, token: state.login.user && state.login.user.token, useRealName: state.settings.UI_Use_Real_Name, searchText: state.rooms.searchText, server: state.server.server, loading: state.server.loading, FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, baseUrl: state.settings.baseUrl || state.server ? state.server.server : '', sortBy: state.sortPreferences.sortBy, groupByType: state.sortPreferences.groupByType, showFavorites: state.sortPreferences.showFavorites }), dispatch => ({ openSearchHeader: () => dispatch(openSearchHeaderAction()), closeSearchHeader: () => dispatch(closeSearchHeaderAction()) })) /** @extends React.Component */ export default class ShareListView extends React.Component { static navigationOptions = ({ navigation }) => { const searching = navigation.getParam('searching'); const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid'); const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {}); return { headerBackTitle: isIOS ? I18n.t('Back') : null, headerLeft: searching ? ( ) : ( ), headerTitle: , headerRight: ( searching ? null : ( {isAndroid ? : null} ) ) }; } static propTypes = { navigation: PropTypes.object, server: PropTypes.string, useRealName: PropTypes.bool, searchText: PropTypes.string, FileUpload_MediaTypeWhiteList: PropTypes.string, FileUpload_MaxFileSize: PropTypes.number, openSearchHeader: PropTypes.func, closeSearchHeader: PropTypes.func, baseUrl: PropTypes.string, token: PropTypes.string, userId: PropTypes.string, sortBy: PropTypes.string, groupByType: PropTypes.bool, showFavorites: PropTypes.bool, loading: PropTypes.bool } constructor(props) { super(props); this.data = []; this.state = { searching: false, value: '', isMedia: false, mediaLoading: false, loading: true, fileInfo: null, search: [], discussions: [], channels: [], favorites: [], chats: [], privateGroup: [], direct: [], livechat: [], servers: [] }; } async componentDidMount() { this.getSubscriptions(); const { navigation } = this.props; navigation.setParams({ initSearchingAndroid: this.initSearchingAndroid, cancelSearchingAndroid: this.cancelSearchingAndroid }); 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, type: mime.lookup(data.path), store: 'Uploads', path: isIOS ? data.path : `file://${ data.path }` }; } this.setState({ value, fileInfo, isMedia, mediaLoading: false }); } catch (e) { log('err_process_media_share_extension', e); this.setState({ mediaLoading: false }); } } componentWillReceiveProps(nextProps) { const { searchText, loading } = this.props; if (nextProps.server && loading !== nextProps.loading) { if (nextProps.loading) { this.internalSetState({ loading: true }); } else { this.getSubscriptions(); } } else if (searchText !== nextProps.searchText) { this.search(nextProps.searchText); } } shouldComponentUpdate(nextProps, nextState) { const { loading, searching } = this.state; if (nextState.loading !== loading) { return true; } if (nextState.searching !== searching) { return true; } const { search } = this.state; if (!isEqual(nextState.search, search)) { return true; } return false; } // eslint-disable-next-line react/sort-comp internalSetState = (...args) => { const { navigation } = this.props; if (isIOS && navigation.isFocused()) { LayoutAnimation.easeInEaseOut(); } this.setState(...args); } getSubscriptions = debounce(() => { if (this.data && this.data.removeAllListeners) { this.data.removeAllListeners(); } const { server, sortBy, showFavorites, groupByType } = this.props; const { serversDB } = database.databases; if (server) { this.data = database.objects('subscriptions').filtered('archived != true && open == true'); if (sortBy === 'alphabetical') { this.data = this.data.sorted('name', false); } else { this.data = this.data.sorted('roomUpdatedAt', true); } // servers this.servers = serversDB.objects('servers'); // favorites if (showFavorites) { this.favorites = this.data.filtered('f == true'); } else { this.favorites = []; } // type if (groupByType) { this.discussions = this.data.filtered('prid != null'); this.channels = this.data.filtered('t == $0 AND prid == null', 'c'); this.privateGroup = this.data.filtered('t == $0 AND prid == null', 'p'); this.direct = this.data.filtered('t == $0 AND prid == null', 'd'); this.livechat = this.data.filtered('t == $0 AND prid == null', 'l'); } else { this.chats = this.data; } safeAddListener(this.data, this.updateState); } }, 300); uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); // eslint-disable-next-line react/sort-comp updateState = debounce(() => { this.updateStateInteraction = InteractionManager.runAfterInteractions(() => { this.internalSetState({ chats: this.chats ? this.chats.slice() : [], favorites: this.favorites ? this.favorites.slice() : [], discussions: this.discussions ? this.discussions.slice() : [], channels: this.channels ? this.channels.slice() : [], privateGroup: this.privateGroup ? this.privateGroup.slice() : [], direct: this.direct ? this.direct.slice() : [], livechat: this.livechat ? this.livechat.slice() : [], servers: this.servers ? this.servers.slice() : [], loading: false }); this.forceUpdate(); }); }, 300); getRoomTitle = (item) => { const { useRealName } = this.props; 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) }); } canUploadFile = () => { const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props; const { fileInfo: file, mediaLoading, loading } = this.state; if (loading || mediaLoading) { return true; } if (!(file && file.path)) { return true; } if (file.size > FileUpload_MaxFileSize) { return false; } if (!FileUpload_MediaTypeWhiteList) { return false; } const allowedMime = FileUpload_MediaTypeWhiteList.split(','); if (allowedMime.includes(file.type)) { return true; } const wildCardGlob = '/*'; const wildCards = allowedMime.filter(item => item.indexOf(wildCardGlob) > 0); if (wildCards.includes(file.type.replace(/(\/.*)$/, wildCardGlob))) { return true; } return false; } search = (text) => { const result = database.objects('subscriptions').filtered('name CONTAINS[c] $0', text); const subscriptions = database.objects('subscriptions'); const data = result.length !== subscriptions.length ? result : []; this.internalSetState({ search: data }); } initSearchingAndroid = () => { const { openSearchHeader, navigation } = this.props; this.setState({ searching: true }); navigation.setParams({ searching: true }); openSearchHeader(); } cancelSearchingAndroid = () => { if (isAndroid) { const { closeSearchHeader, navigation } = this.props; this.setState({ searching: false }); navigation.setParams({ searching: false }); closeSearchHeader(); this.internalSetState({ search: [] }); Keyboard.dismiss(); } } renderListHeader = () => ; renderSectionHeader = header => ( {I18n.t(header)} ) renderItem = ({ item }) => { const { userId, token, baseUrl } = this.props; return ( this.shareMessage(item)} testID={`share-extension-item-${ item.name }`} /> ); } renderSeparator = () => ; renderSection = (data, header) => { if (data && data.length > 0) { return ( {this.renderSectionHeader(header)} ); } return null; } renderServerSelector = () => { const { servers } = this.state; const { server } = this.props; const currentServer = servers.find(serverFiltered => serverFiltered.id === server); return currentServer ? ( {this.renderSectionHeader('Select_Server')} Navigation.navigate('SelectServerView')} item={currentServer} /> ) : null; } renderContent = () => { const { discussions, channels, privateGroup, direct, livechat, search, chats, favorites } = this.state; if (search.length > 0) { return ( ); } return ( {this.renderServerSelector()} {this.renderSection(favorites, 'Favorites')} {this.renderSection(discussions, 'Discussions')} {this.renderSection(channels, 'Channels')} {this.renderSection(direct, 'Direct_Messages')} {this.renderSection(privateGroup, 'Private_Groups')} {this.renderSection(livechat, 'Livechat')} {this.renderSection(chats, 'Chats')} ); } renderScrollView = () => { const { mediaLoading, loading } = this.state; if (mediaLoading || loading) { return ; } return ( {this.renderListHeader()} {this.renderContent()} ); } renderError = () => { const { fileInfo: file } = this.state; const { FileUpload_MaxFileSize } = this.props; const errorMessage = (FileUpload_MaxFileSize < file.size) ? 'error-file-too-large' : 'error-invalid-file-type'; return ( {I18n.t(errorMessage)} { file.type } ); } render() { const showError = !this.canUploadFile(); return ( { showError ? this.renderError() : this.renderScrollView() } ); } }