/* * Wire * Copyright (C) 2022 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import {useEffect, useRef, useState} from 'react'; import {BackendErrorLabel} from '@wireapp/api-client/lib/http'; import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {partition} from 'underscore'; import * as Icon from 'Components/Icon'; import {UserList, UserlistMode} from 'Components/UserList'; import {UserRepository} from 'src/script/user/UserRepository'; import {t} from 'Util/LocalizerUtil'; import {getLogger} from 'Util/Logger'; import {safeWindowOpen} from 'Util/SanitizationUtil'; import {sortByPriority} from 'Util/StringUtil'; import {isBackendError} from 'Util/TypePredicateUtil'; import {TopPeople} from './components/TopPeople'; import {ConversationRepository} from '../../../../conversation/ConversationRepository'; import {ConversationState} from '../../../../conversation/ConversationState'; import {User} from '../../../../entity/User'; import {getManageTeamUrl} from '../../../../externalRoute'; import {useDebounce} from '../../../../hooks/useDebounce'; import {SearchRepository} from '../../../../search/SearchRepository'; import {TeamRepository} from '../../../../team/TeamRepository'; import {TeamState} from '../../../../team/TeamState'; import {UserState} from '../../../../user/UserState'; export type SearchResultsData = {contacts: User[]; others: User[]}; interface PeopleTabProps { canInviteTeamMembers: boolean; canSearchUnconnectedUsers: boolean; conversationRepository: ConversationRepository; conversationState: ConversationState; isFederated: boolean; isTeam: boolean; onClickContact: (user: User) => void; onClickUser: (user: User) => void; onSearchResults: (results: SearchResultsData | undefined) => void; searchQuery: string; searchRepository: SearchRepository; teamRepository: TeamRepository; teamState: TeamState; userRepository: UserRepository; userState: UserState; selfUser: User; } export const PeopleTab = ({ searchQuery, isTeam, isFederated, teamRepository, teamState, userState, selfUser, canInviteTeamMembers, canSearchUnconnectedUsers, conversationState, searchRepository, conversationRepository, userRepository, onClickContact, onClickUser, onSearchResults, }: PeopleTabProps) => { const logger = getLogger('PeopleSearch'); const [topPeople, setTopPeople] = useState([]); const teamSize = teamState.teamSize(); const [hasFederationError, setHasFederationError] = useState(false); const currentSearchQuery = useRef(''); const inTeam = teamState.isInTeam(selfUser); const getLocalUsers = (unfiltered?: boolean) => { const connectedUsers = conversationState.connectedUsers(); if (!canSearchUnconnectedUsers) { return connectedUsers; } let contacts: User[] = []; if (!isTeam) { contacts = userState.connectedUsers(); } else { const teamUsers = teamState.teamUsers(); contacts = unfiltered ? teamUsers : teamUsers.filter( user => conversationState.hasConversationWith(user) || teamRepository.isSelfConnectedTo(user.id), ); } return contacts.filter(user => user.isAvailable()); }; const [results, setResults] = useState({contacts: getLocalUsers(), others: []}); const searchOnFederatedDomain = () => ''; const hasResults = results.contacts.length + results.others.length > 0; const manageTeamUrl = getManageTeamUrl('client_landing'); const organizeTeamSearchResults = async ( remoteUsers: User[], searchResults: SearchResultsData, query: string, ): Promise => { const selfTeamId = selfUser.teamId; const [contacts, others] = partition(remoteUsers, user => user.teamId === selfTeamId); const nonExternalContacts = await teamRepository.filterExternals(contacts); return { ...searchResults, contacts: [...searchResults.contacts, ...nonExternalContacts].sort((userA, userB) => sortByPriority(userA.name(), userB.name(), query), ), others: others, }; }; const getTopPeople = () => { return conversationRepository .getMostActiveConversations() .then(conversationEntities => { return conversationEntities .filter(conversation => conversation.is1to1()) .slice(0, 6) .map(conversation => conversation.participating_user_ids()[0]); }) .then(userIds => userRepository.getUsersById(userIds)) .then(userEntities => userEntities.filter(user => !user.isBlocked())); }; useEffect(() => { if (!isTeam) { getTopPeople().then(setTopPeople); } }, []); useDebounce( async () => { setHasFederationError(false); const {query} = searchRepository.normalizeQuery(searchQuery); if (!query) { setResults({contacts: getLocalUsers(), others: []}); onSearchResults(undefined); return; } const localSearchSources = getLocalUsers(true); const contactResults = searchRepository.searchUserInSet(searchQuery, localSearchSources); const filteredResults = contactResults.filter( user => conversationState.hasConversationWith(user) || teamRepository.isSelfConnectedTo(user.id) || user.username() === query, ); const localSearchResults: SearchResultsData = { contacts: filteredResults, others: [], }; setResults(localSearchResults); onSearchResults(localSearchResults); if (canSearchUnconnectedUsers) { try { const userEntities = await searchRepository.searchByName(searchQuery, selfUser.teamId); const localUserIds = localSearchResults.contacts.map(({id}) => id); const onlyRemoteUsers = userEntities.filter(user => !localUserIds.includes(user.id)); const results = inTeam ? await organizeTeamSearchResults(onlyRemoteUsers, localSearchResults, query) : {...localSearchResults, others: onlyRemoteUsers}; if (currentSearchQuery.current === searchQuery) { // Only update the results if the query that has been processed correspond to the current search query onSearchResults(results); setResults(results); } } catch (error) { if (isBackendError(error)) { if (error.code === HTTP_STATUS.UNPROCESSABLE_ENTITY) { return setHasFederationError(true); } if (error.code === HTTP_STATUS.BAD_REQUEST && error.label === BackendErrorLabel.FEDERATION_NOT_ALLOWED) { return logger.warn(`Error searching for contacts: ${error.message}`); } } logger.error(`Error searching for contacts: ${(error as any).message}`, error); } } }, 300, [searchQuery], ); useEffect(() => { // keep track of the most up to date value of the search query (in order to cancel outdated queries) currentSearchQuery.current = searchQuery; return () => { currentSearchQuery.current = ''; onSearchResults(undefined); }; }, [searchQuery]); return ( <> {hasFederationError && (
{t('searchConnectWithOtherDomain')}
{t('searchFederatedDomainNotAvailable')} {/*@todo: re-enable when federation article is available */}
)} {searchQuery.length === 0 && ( <>
    {teamSize === 1 && canInviteTeamMembers && !!manageTeamUrl && (
  • )}
{topPeople.length > 0 && (

{t('searchTopPeople')}

)} )} {!hasFederationError && !hasResults && ( <> {!canSearchUnconnectedUsers ? (

{t('searchNoMatchesPartner')}

) : isFederated ? (
{t('searchTrySearchFederation')}
{/*@todo: re-enable when federation article is available */}
) : (

{t('searchTrySearch')}

)} )}
{results.contacts.length > 0 && (
{isTeam ? (

{t('searchContacts')}

) : (

{t('searchConnections')}

)}
onClickContact(user)} conversationRepository={conversationRepository} mode={UserlistMode.COMPACT} users={results.contacts} selfUser={selfUser} />
)} {results.others.length > 0 && (

{searchOnFederatedDomain() ? t('searchOthersFederation', searchOnFederatedDomain()) : t('searchOthers')}

)}
); };