// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; import {Keyboard, View} from 'react-native'; import {Navigation} from 'react-native-navigation'; import {SafeAreaView} from 'react-native-safe-area-context'; import {makeDirectChannel, makeGroupChannel} from '@actions/remote/channel'; import {fetchProfiles, fetchProfilesInTeam, searchProfiles} from '@actions/remote/user'; import CompassIcon from '@components/compass_icon'; import Loading from '@components/loading'; import SearchBar from '@components/search_bar'; import {General} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {debounce} from '@helpers/api/general'; import {t} from '@i18n'; import {dismissModal, setButtons} from '@screens/navigation'; import {alertErrorWithFallback} from '@utils/draft'; import { changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme, } from '@utils/theme'; import {displayUsername, filterProfilesMatchingTerm} from '@utils/user'; import SelectedUsers from './selected_users'; import UserList from './user_list'; const START_BUTTON = 'start-conversation'; const CLOSE_BUTTON = 'close-dms'; type Props = { componentId: string; currentTeamId: string; currentUserId: string; restrictDirectMessage: boolean; teammateNameDisplay: string; } const close = () => { Keyboard.dismiss(); dismissModal(); }; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { container: { flex: 1, }, searchBar: { marginHorizontal: 12, borderRadius: 8, marginTop: 12, marginBottom: 12, backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), }, searchBarInput: { color: theme.centerChannelColor, }, 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), }, }; }); export default function CreateDirectMessage({ componentId, currentTeamId, currentUserId, restrictDirectMessage, teammateNameDisplay, }: Props) { const serverUrl = useServerUrl(); const theme = useTheme(); const style = getStyleFromTheme(theme); const intl = useIntl(); const {formatMessage} = intl; const searchTimeoutId = useRef(null); const next = useRef(true); const page = useRef(-1); const mounted = useRef(false); const [profiles, setProfiles] = useState([]); const [searchResults, setSearchResults] = useState([]); const [loading, setLoading] = useState(false); const [term, setTerm] = useState(''); const [startingConversation, setStartingConversation] = useState(false); const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({}); const selectedCount = Object.keys(selectedIds).length; const isSearch = Boolean(term); const addToProfiles = useRef((newProfiles: UserProfile[]) => setProfiles([...profiles, ...newProfiles])); useEffect(() => { addToProfiles.current = (newProfiles: UserProfile[]) => setProfiles([...profiles, ...newProfiles]); }, [profiles]); const loadedProfiles = ({users}: {users?: UserProfile[]}) => { if (mounted.current) { if (users && !users.length) { next.current = false; } page.current += 1; setLoading(false); if (users && users.length) { addToProfiles.current(users); } } }; const clearSearch = useCallback(() => { setTerm(''); setSearchResults([]); }, []); const getProfiles = useCallback(debounce(() => { if (next.current && !loading && !term && mounted.current) { setLoading(true); if (restrictDirectMessage) { fetchProfilesInTeam(serverUrl, currentTeamId, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles); } else { fetchProfiles(serverUrl, page.current + 1, General.PROFILE_CHUNK_SIZE).then(loadedProfiles); } } }, 100), [loading, isSearch, restrictDirectMessage, serverUrl, currentTeamId]); const handleRemoveProfile = useCallback((id: string) => { const newSelectedIds = Object.assign({}, selectedIds); Reflect.deleteProperty(newSelectedIds, id); setSelectedIds(newSelectedIds); }, [selectedIds]); const createDirectChannel = useCallback(async (id: string): Promise => { const user = profiles.find((u) => u.id === id); const displayName = displayUsername(user, intl.locale, teammateNameDisplay); const result = await makeDirectChannel(serverUrl, id, displayName); 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; }, [profiles, intl.locale, teammateNameDisplay, serverUrl]); const createGroupChannel = useCallback(async (ids: string[]): Promise => { const result = await makeGroupChannel(serverUrl, ids); 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; }, [serverUrl]); const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => { if (startingConversation) { return; } setStartingConversation(true); const idsToUse = selectedId ? Object.keys(selectedId) : Object.keys(selectedIds); let success; if (idsToUse.length === 0) { success = false; } else if (idsToUse.length > 1) { success = await createGroupChannel(idsToUse); } else { success = await createDirectChannel(idsToUse[0]); } if (success) { close(); } else { setStartingConversation(false); } }, [startingConversation, selectedIds, createGroupChannel, createDirectChannel]); const handleSelectProfile = useCallback((user: UserProfile) => { if (selectedIds[user.id]) { handleRemoveProfile(user.id); return; } if (user.id === currentUserId) { const selectedId = { [currentUserId]: true, }; startConversation(selectedId); } else { const wasSelected = selectedIds[user.id]; if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM - 1) { return; } const newSelectedIds = Object.assign({}, selectedIds); if (!wasSelected) { newSelectedIds[user.id] = user; } setSelectedIds(newSelectedIds); clearSearch(); } }, [selectedIds, currentUserId, handleRemoveProfile, startConversation, clearSearch]); const searchUsers = useCallback(async (searchTerm: string) => { const lowerCasedTerm = searchTerm.toLowerCase(); setLoading(true); let results; if (restrictDirectMessage) { results = await searchProfiles(serverUrl, lowerCasedTerm); } else { results = await searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId}); } let data: UserProfile[] = []; if (results.data) { data = results.data; } setSearchResults(data); setLoading(false); }, [restrictDirectMessage, serverUrl, currentTeamId]); const onSearch = useCallback((text: string) => { if (text) { setTerm(text); if (searchTimeoutId.current) { clearTimeout(searchTimeoutId.current); } searchTimeoutId.current = setTimeout(() => { searchUsers(text); }, General.SEARCH_TIMEOUT_MILLISECONDS); } else { clearSearch(); } }, [searchUsers, clearSearch]); const updateNavigationButtons = useCallback(async (startEnabled: boolean) => { const closeIcon = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor); setButtons(componentId, { leftButtons: [{ id: CLOSE_BUTTON, icon: closeIcon, testID: 'close.more_direct_messages.button', }], rightButtons: [{ color: theme.sidebarHeaderTextColor, id: START_BUTTON, text: formatMessage({id: 'mobile.create_direct_message.start', defaultMessage: 'Start'}), showAsAction: 'always', enabled: startEnabled, testID: 'more_direct_messages.start.button', }], }); }, [intl.locale, theme]); useEffect(() => { const unsubscribe = Navigation.events().registerComponentListener({navigationButtonPressed: ({buttonId}) => { if (buttonId === START_BUTTON) { startConversation(); } else if (buttonId === CLOSE_BUTTON) { close(); } }}, componentId); return () => { unsubscribe.remove(); }; }, [startConversation]); useEffect(() => { mounted.current = true; updateNavigationButtons(false); getProfiles(); return () => { mounted.current = false; }; }, []); useEffect(() => { const canStart = selectedCount > 0 && !startingConversation; updateNavigationButtons(canStart); }, [selectedCount > 0, startingConversation, updateNavigationButtons]); const data = useMemo(() => { if (term) { const exactMatches: UserProfile[] = []; const filterByTerm = (p: UserProfile) => { if (selectedCount > 0 && p.id === currentUserId) { return false; } if (p.username === term || p.username.startsWith(term)) { exactMatches.push(p); return false; } return true; }; const results = filterProfilesMatchingTerm(searchResults, term).filter(filterByTerm); return [...exactMatches, ...results]; } return profiles; }, [term, isSearch && selectedCount, isSearch && searchResults, profiles]); if (startingConversation) { return ( ); } return ( {selectedCount > 0 && } ); }