import React from 'react'; import { StackNavigationProp } from '@react-navigation/stack'; import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View, Rationale } from 'react-native'; import ShareExtension from 'rn-extensions-share'; import * as FileSystem from 'expo-file-system'; import { connect } from 'react-redux'; import * as mime from 'react-native-mime-types'; import { dequal } from 'dequal'; import { Q } from '@nozbe/watermelondb'; import database from '../../lib/database'; import { isAndroid, isIOS } from '../../utils/deviceInfo'; import I18n from '../../i18n'; import DirectoryItem, { ROW_HEIGHT } from '../../containers/DirectoryItem'; import ServerItem from '../../containers/ServerItem'; import * as HeaderButton from '../../containers/HeaderButton'; import ActivityIndicator from '../../containers/ActivityIndicator'; import * as List from '../../containers/List'; import { themes } from '../../lib/constants'; import { animateNextTransition } from '../../utils/layoutAnimation'; import { TSupportedThemes, withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import RocketChat from '../../lib/rocketchat'; import { sanitizeLikeString } from '../../lib/database/utils'; import styles from './styles'; import ShareListHeader from './Header'; import { IServerInfo } from '../../definitions'; interface IDataFromShare { value: string; type: string; } interface IFileToShare { filename: string; description: string; size: number; mime: any; path: string; } interface IChat { rid: string; t: string; name: string; fname: string; blocked: boolean; blocker: boolean; prid: string; uids: string[]; usernames: string[]; topic: string; description: string; } interface IState { searching: boolean; searchText: string; searchResults: IChat[]; chats: IChat[]; serversCount: number; attachments: IFileToShare[]; text: string; loading: boolean; serverInfo: IServerInfo; needsPermission: boolean; } interface INavigationOption { navigation: StackNavigationProp; } interface IShareListViewProps extends INavigationOption { server: string; token: string; userId: string; theme: TSupportedThemes; } const permission: Rationale = { title: I18n.t('Read_External_Permission'), message: I18n.t('Read_External_Permission_Message'), buttonPositive: 'Ok' }; const getItemLayout = (data: any, index: number) => ({ length: data.length, offset: ROW_HEIGHT * index, index }); const keyExtractor = (item: IChat) => item.rid; class ShareListView extends React.Component { private unsubscribeFocus: (() => void) | undefined; private unsubscribeBlur: (() => void) | undefined; constructor(props: IShareListViewProps) { super(props); this.state = { searching: false, searchText: '', searchResults: [], chats: [], serversCount: 0, attachments: [], text: '', loading: true, serverInfo: {} as IServerInfo, needsPermission: isAndroid || false }; this.setHeader(); if (isAndroid) { this.unsubscribeFocus = props.navigation.addListener('focus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress) ); this.unsubscribeBlur = props.navigation.addListener('blur', () => BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress) ); } } async componentDidMount() { const { server } = this.props; try { const data = (await ShareExtension.data()) as IDataFromShare[]; if (isAndroid) { await this.askForPermission(data); } const info = await Promise.all( data .filter(item => item.type === 'media') .map(file => FileSystem.getInfoAsync(this.uriToPath(file.value), { size: true })) ); const attachments = info.map(file => ({ filename: decodeURIComponent(file.uri.substring(file.uri.lastIndexOf('/') + 1)), description: '', size: file.size, mime: mime.lookup(file.uri), path: file.uri })) as IFileToShare[]; const text = data.filter(item => item.type === 'text').reduce((acc, item) => `${item.value}\n${acc}`, ''); this.setState({ text, attachments }); } catch { // Do nothing } this.getSubscriptions(server); } UNSAFE_componentWillReceiveProps(nextProps: IShareListViewProps) { const { server } = this.props; if (nextProps.server !== server) { this.getSubscriptions(nextProps.server); } } shouldComponentUpdate(nextProps: IShareListViewProps, nextState: IState) { const { searching, needsPermission } = this.state; if (nextState.searching !== searching) { return true; } if (nextState.needsPermission !== needsPermission) { return true; } const { server, userId } = this.props; if (server !== nextProps.server) { return true; } if (userId !== nextProps.userId) { return true; } const { searchResults } = this.state; if (nextState.searching) { if (!dequal(nextState.searchResults, searchResults)) { return true; } } return false; } componentWillUnmount() { if (this.unsubscribeFocus) { this.unsubscribeFocus(); } if (this.unsubscribeBlur) { this.unsubscribeBlur(); } } setHeader = () => { const { searching } = this.state; const { navigation, theme } = this.props; if (isIOS) { navigation.setOptions({ header: () => ( ) }); return; } navigation.setOptions({ headerLeft: () => searching ? ( ) : ( ), headerTitle: () => , headerRight: () => searching ? null : ( ) }); }; internalSetState = (...args: object[]) => { const { navigation } = this.props; if (navigation.isFocused()) { animateNextTransition(); } // @ts-ignore this.setState(...args); }; query = async (text?: string) => { const db = database.active; const defaultWhereClause = [ Q.where('archived', false), Q.where('open', true), Q.experimentalSkip(0), Q.experimentalTake(20), Q.experimentalSortBy('room_updated_at', Q.desc) ] as (Q.WhereDescription | Q.Skip | Q.Take | Q.SortBy | Q.Or)[]; if (text) { const likeString = sanitizeLikeString(text); defaultWhereClause.push(Q.or(Q.where('name', Q.like(`%${likeString}%`)), Q.where('fname', Q.like(`%${likeString}%`)))); } const data = (await db .get('subscriptions') .query(...defaultWhereClause) .fetch()) as IChat[]; return data.map(item => ({ rid: item.rid, t: item.t, name: item.name, fname: item.fname, blocked: item.blocked, blocker: item.blocker, prid: item.prid, uids: item.uids, usernames: item.usernames, topic: item.topic })); }; getSubscriptions = async (server: string) => { const serversDB = database.servers; if (server) { const chats = await this.query(); const serversCollection = serversDB.get('servers'); const serversCount = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetchCount(); let serverInfo = {}; try { serverInfo = await serversCollection.find(server); } catch (error) { // Do nothing } this.internalSetState({ chats: chats ?? [], serversCount, loading: false, serverInfo }); this.forceUpdate(); } }; askForPermission = async (data: IDataFromShare[]) => { const mediaIndex = data.findIndex(item => item.type === 'media'); if (mediaIndex !== -1) { const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, permission); if (result !== PermissionsAndroid.RESULTS.GRANTED) { this.setState({ needsPermission: true }); return Promise.reject(); } } this.setState({ needsPermission: false }); return Promise.resolve(); }; uriToPath = (uri: string) => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri); getRoomTitle = (item: IChat) => { const { serverInfo } = this.state; return ((item.prid || serverInfo?.useRealName) && item.fname) || item.name; }; shareMessage = (room: IChat) => { const { attachments, text, serverInfo } = this.state; const { navigation } = this.props; navigation.navigate('ShareView', { room, text, attachments, serverInfo, isShareExtension: true }); }; search = async (text: string) => { const result = await this.query(text); this.internalSetState({ searchResults: result, searchText: text }); }; initSearch = () => { const { chats } = this.state; this.setState({ searching: true, searchResults: chats }, () => this.setHeader()); }; cancelSearch = () => { this.internalSetState({ searching: false, searchResults: [], searchText: '' }, () => this.setHeader()); Keyboard.dismiss(); }; handleBackPress = () => { const { searching } = this.state; if (searching) { this.cancelSearch(); return true; } return false; }; renderSectionHeader = (header: string) => { const { searching } = this.state; const { theme } = this.props; if (searching) { return null; } return ( <> {I18n.t(header)} ); }; renderItem = ({ item }: { item: IChat }) => { const { serverInfo } = this.state; let description; switch (item.t) { case 'c': description = item.topic || item.description; break; case 'p': description = item.topic || item.description; break; case 'd': description = serverInfo?.useRealName ? item.name : item.fname; break; default: description = item.fname; break; } return ( this.shareMessage(item)} testID={`share-extension-item-${item.name}`} /> ); }; renderSelectServer = () => { const { serverInfo } = this.state; const { navigation } = this.props; return ( <> {this.renderSectionHeader('Select_Server')} navigation.navigate('SelectServerView')} item={serverInfo} /> ); }; renderEmptyComponent = () => { const { theme } = this.props; return ( {I18n.t('No_results_found')} ); }; renderHeader = () => { const { searching, serversCount } = this.state; if (searching) { return null; } if (serversCount === 1) { return this.renderSectionHeader('Chats'); } return ( <> {this.renderSelectServer()} {this.renderSectionHeader('Chats')} ); }; render = () => { const { chats, loading, searchResults, searching, searchText, needsPermission } = this.state; const { theme } = this.props; if (loading) { return ; } if (needsPermission) { return ( {permission.title} {permission.message} ); } return ( 0 ? : null} ListEmptyComponent={searching && searchText ? this.renderEmptyComponent : null} removeClippedSubviews keyboardShouldPersistTaps='always' /> ); }; } const mapStateToProps = ({ share }: any) => ({ userId: share.user && share.user.id, token: share.user && share.user.token, server: share.server.server }); export default connect(mapStateToProps)(withTheme(ShareListView));