// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import {Platform, View} from 'react-native'; import {Navigation} from 'react-native-navigation'; import {debounce} from '@mm-redux/actions/helpers'; import {General} from '@mm-redux/constants'; import EventEmitter from '@mm-redux/utils/event_emitter'; import {getGroupDisplayNameFromUserIds} from '@mm-redux/utils/channel_utils'; import {displayUsername, filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils'; import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing'; import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list'; import UserListRow from 'app/components/custom_list/user_list_row'; import FormattedText from 'app/components/formatted_text'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; import Loading from 'app/components/loading'; import SearchBar from 'app/components/search_bar'; import StatusBar from 'app/components/status_bar'; import {NavigationTypes} from 'app/constants'; import {alertErrorWithFallback} from 'app/utils/general'; import {createProfilesSections, loadingText} from 'app/utils/member_list'; import { changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme, } from 'app/utils/theme'; import {t} from 'app/utils/i18n'; import {dismissModal, setButtons} from 'app/actions/navigation'; import SelectedUsers from './selected_users'; const START_BUTTON = 'start-conversation'; const CLOSE_BUTTON = 'close-dms'; export default class MoreDirectMessages extends PureComponent { static propTypes = { actions: PropTypes.shape({ makeDirectChannel: PropTypes.func.isRequired, makeGroupChannel: PropTypes.func.isRequired, getProfiles: PropTypes.func.isRequired, getProfilesInTeam: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired, setChannelDisplayName: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, allProfiles: PropTypes.object.isRequired, currentDisplayName: PropTypes.string, currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, isGuest: PropTypes.object.isRequired, restrictDirectMessage: PropTypes.bool.isRequired, teammateNameDisplay: PropTypes.string, theme: PropTypes.object.isRequired, isLandscape: PropTypes.bool.isRequired, }; static contextTypes = { intl: intlShape.isRequired, }; constructor(props, context) { super(props, context); this.searchTimeoutId = 0; this.next = true; this.page = -1; this.mounted = false; this.state = { profiles: [], searchResults: [], loading: false, term: '', startingConversation: false, selectedIds: {}, selectedCount: 0, }; } componentDidMount() { this.navigationEventListener = Navigation.events().bindComponent(this); this.mounted = true; this.updateNavigationButtons(false); this.getProfiles(); } componentWillUnmount() { this.mounted = false; } componentDidUpdate() { const {selectedCount, startingConversation} = this.state; const canStart = selectedCount > 0 && !startingConversation; this.updateNavigationButtons(canStart); } navigationButtonPressed({buttonId}) { if (buttonId === START_BUTTON) { this.startConversation(); } else if (buttonId === CLOSE_BUTTON) { this.close(); } } setSearchBarRef = (ref) => { this.searchBarRef = ref; } close = () => { dismissModal(); }; clearSearch = () => { this.setState({term: '', searchResults: []}); }; getProfiles = debounce(() => { const {loading, term} = this.state; if (this.next && !loading && !term && this.mounted) { this.setState({loading: true}, () => { const {actions, currentTeamId, restrictDirectMessage} = this.props; if (restrictDirectMessage) { actions.getProfiles(this.page + 1, General.PROFILE_CHUNK_SIZE).then(this.loadedProfiles); } else { actions.getProfilesInTeam(currentTeamId, this.page + 1, General.PROFILE_CHUNK_SIZE).then(this.loadedProfiles); } }); } }, 100); handleSelectProfile = (id) => { const {currentUserId} = this.props; if (id === currentUserId) { const selectedId = {}; selectedId[currentUserId] = true; this.startConversation(selectedId); } else { this.setState((prevState) => { const {selectedIds} = prevState; const wasSelected = selectedIds[id]; // Prevent selecting too many users if (!wasSelected && Object.keys(selectedIds).length >= General.MAX_USERS_IN_GM - 1) { return {}; } const newSelectedIds = Object.assign({}, selectedIds); if (!wasSelected) { newSelectedIds[id] = true; } return { selectedIds: newSelectedIds, selectedCount: Object.keys(newSelectedIds).length, }; }); this.clearSearch(); } }; handleRemoveProfile = (id) => { this.setState((prevState) => { const {selectedIds} = prevState; const newSelectedIds = Object.assign({}, selectedIds); Reflect.deleteProperty(newSelectedIds, id); return { selectedIds: newSelectedIds, selectedCount: Object.keys(newSelectedIds).length, }; }); }; isStartEnabled = (state) => { if (state.startingConversation) { return false; } return state.selectedCount >= 1 && state.selectedCount <= General.MAX_USERS_IN_GM - 1; }; loadedProfiles = ({data}) => { if (this.mounted) { const {profiles} = this.state; if (data && !data.length) { this.next = false; } this.page += 1; this.setState({loading: false, profiles: [...profiles, ...data]}); } }; makeDirectChannel = async (id) => { const {intl} = this.context; const {actions, allProfiles, teammateNameDisplay} = this.props; const user = allProfiles[id]; const displayName = displayUsername(user, teammateNameDisplay); actions.setChannelDisplayName(displayName); const result = await actions.makeDirectChannel(id); if (result.error) { alertErrorWithFallback( intl, result.error, { id: 'mobile.open_dm.error', defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.", }, { displayName, }, ); } return !result.error; }; makeGroupChannel = async (ids) => { const {intl} = this.context; const { actions, allProfiles, currentUserId, teammateNameDisplay, } = this.props; const result = await actions.makeGroupChannel(ids); const displayName = getGroupDisplayNameFromUserIds(ids, allProfiles, currentUserId, teammateNameDisplay); actions.setChannelDisplayName(displayName); if (result.error) { alertErrorWithFallback( intl, result.error, { id: t('mobile.open_gm.error'), defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.", }, ); } return !result.error; }; onSearch = (text) => { if (text) { this.setState({term: text}); clearTimeout(this.searchTimeoutId); this.searchTimeoutId = setTimeout(() => { this.searchProfiles(text); }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { this.clearSearch(); } }; searchProfiles = (term) => { const lowerCasedTerm = term.toLowerCase(); const {actions, currentTeamId, restrictDirectMessage} = this.props; this.setState({loading: true}); if (restrictDirectMessage) { actions.searchProfiles(lowerCasedTerm).then(({data}) => { this.setState({searchResults: data, loading: false}); }); } else { actions.searchProfiles(lowerCasedTerm, {team_id: currentTeamId}).then(({data}) => { this.setState({searchResults: data, loading: false}); }); } }; startConversation = async (selectedId) => { const { currentDisplayName, actions, } = this.props; if (this.state.startingConversation) { return; } this.setState({ startingConversation: true, }); // Save the current channel display name in case it fails const currentChannelDisplayName = currentDisplayName; const selectedIds = selectedId ? Object.keys(selectedId) : Object.keys(this.state.selectedIds); let success; if (selectedIds.length === 0) { success = false; } else if (selectedIds.length > 1) { success = await this.makeGroupChannel(selectedIds); } else { success = await this.makeDirectChannel(selectedIds[0]); } if (success) { EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR); requestAnimationFrame(() => { this.close(); }); } else { this.setState({ startingConversation: false, }); actions.setChannelDisplayName(currentChannelDisplayName); } }; updateNavigationButtons = (startEnabled, context = this.context) => { const {componentId, theme} = this.props; const {formatMessage} = context.intl; setButtons(componentId, { rightButtons: [{ color: theme.sidebarHeaderTextColor, id: START_BUTTON, text: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}), showAsAction: 'always', enabled: startEnabled, }], }); }; renderItem = (props) => { // The list will re-render when the selection changes because it's passed into the list as extraData const selected = this.state.selectedIds[props.id]; return ( ); }; filterUnknownUsers = (u) => Boolean(this.props.allProfiles[u.id]) renderLoading = () => { const {theme} = this.props; const {loading} = this.state; const style = getStyleFromTheme(theme); if (!loading) { return null; } return ( ); }; renderNoResults = () => { const {loading} = this.state; const {theme} = this.props; const style = getStyleFromTheme(theme); if (loading || this.page === -1) { return null; } return ( ); }; render() { const {formatMessage} = this.context.intl; const {isGuest, currentUserId, theme, isLandscape} = this.props; const { loading, profiles, searchResults, selectedIds, selectedCount, startingConversation, term, } = this.state; const style = getStyleFromTheme(theme); if (startingConversation) { return ( ); } const searchBarInput = { backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), color: theme.centerChannelColor, fontSize: 15, ...Platform.select({ android: { marginBottom: -5, }, }), }; let data; let listType; if (term) { const exactMatches = []; const filterByTerm = (p) => { if (selectedCount > 0 && p.id === currentUserId) { return false; } if (p.username === term || p.username.startsWith(term)) { exactMatches.push(p); return false; } return true; }; let results; if (isGuest) { results = filterProfilesMatchingTerm(searchResults, term).filter((u) => filterByTerm(u) && this.filterUnknownUsers(u)); } else { results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm); } data = [...exactMatches, ...results]; listType = FLATLIST; } else { if (isGuest) { data = createProfilesSections(profiles.filter(this.filterUnknownUsers)); } else { data = createProfilesSections(profiles); } listType = SECTIONLIST; } return ( ); } } const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { container: { flex: 1, }, searchBar: { marginVertical: 5, }, loadingContainer: { alignItems: 'center', backgroundColor: theme.centerChannelBg, height: 70, justifyContent: 'center', }, loadingText: { color: changeOpacity(theme.centerChannelColor, 0.6), }, noResultContainer: { flexGrow: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, noResultText: { fontSize: 26, color: changeOpacity(theme.centerChannelColor, 0.5), }, }; });